import 'dart:math'; import 'package:flutter/material.dart'; import 'package:material_symbols_icons/symbols.dart'; import '../../../main.dart' show accountModule; import '../../../backend/modules/account.dart' show PrivacyConfig, BlockedContact; import '../../../core/storage/app_database.dart'; import '../../widgets/custom_notification.dart'; import 'password_entry_screen.dart'; class SecurityScreen extends StatefulWidget { const SecurityScreen({super.key}); @override State createState() => _SecurityScreenState(); } class _SecurityScreenState extends State with SingleTickerProviderStateMixin { bool _isLoading = true; bool _isSaving = false; bool _is2faEnabled = false; PrivacyConfig? _privacyConfig; List _blockedContacts = []; late AnimationController _shimmerController; @override void initState() { super.initState(); _shimmerController = AnimationController( vsync: this, duration: const Duration(milliseconds: 1500), )..repeat(); _loadData(); } @override void dispose() { _shimmerController.dispose(); super.dispose(); } Future _loadData() async { try { final results = await Future.wait([ accountModule.getPrivacyConfig(), accountModule.getBlockedContacts(), AppDatabase.loadActiveProfile(), ]); if (mounted) { setState(() { _privacyConfig = results[0] as PrivacyConfig; _blockedContacts = results[1] as List; final profile = results[2] as ProfileData?; _is2faEnabled = profile?.profileOptions?.contains(2) ?? false; _isLoading = false; }); } } catch (e) { if (mounted) { showCustomNotification(context, 'Ошибка загрузки: $e'); setState(() => _isLoading = false); } } } Future _updateSetting(String key, dynamic value) async { if (_isSaving) return; setState(() => _isSaving = true); try { final newConfig = await accountModule.updatePrivacyConfig({key: value}); if (mounted) { setState(() => _privacyConfig = newConfig); } } catch (e) { if (mounted) { showCustomNotification(context, 'Ошибка сохранения: $e'); } } finally { if (mounted) { setState(() => _isSaving = false); } } } @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; return Scaffold( backgroundColor: cs.surface, body: SafeArea( bottom: false, child: _isLoading ? _buildShimmer(cs) : CustomScrollView( physics: const BouncingScrollPhysics(), slivers: [ SliverToBoxAdapter(child: _buildAppBar(context, cs)), SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 0), child: _buildTopSection(cs), ), ), SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.fromLTRB(16, 12, 16, 0), child: _buildPrivacySettings(cs), ), ), SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.fromLTRB(16, 20, 16, 0), child: _buildInfoLabel(cs), ), ), SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 0), child: _buildConfidentialSection(cs), ), ), SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.fromLTRB(16, 12, 16, 120), child: _buildBlacklistSection(cs), ), ), ], ), ), ); } Widget _buildShimmer(ColorScheme cs) { return SingleChildScrollView( physics: const BouncingScrollPhysics(), child: Column( children: [ _buildAppBar(context, cs), _buildShimmerSection(cs, height: 104), const SizedBox(height: 12), _buildShimmerSection(cs, height: 280), const SizedBox(height: 20), _buildShimmerSection(cs, height: 220), const SizedBox(height: 12), _buildShimmerSection(cs, height: 120), ], ), ); } Widget _buildShimmerSection(ColorScheme cs, {required double height}) { return AnimatedBuilder( animation: _shimmerController, builder: (context, child) { final opacity = 0.3 + 0.2 * sin(_shimmerController.value * pi * 2); return Opacity( opacity: opacity, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Container( height: height, decoration: BoxDecoration( color: cs.surfaceContainerHighest, borderRadius: BorderRadius.circular(20), ), ), ), ); }, ); } Widget _buildAppBar(BuildContext context, ColorScheme cs) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 4), child: Row( children: [ IconButton( icon: Icon( Symbols.arrow_back, color: cs.onSurface, size: 24, weight: 400, ), onPressed: () => Navigator.pop(context), ), const SizedBox(width: 4), Text( 'Безопасность', style: TextStyle( color: cs.onSurface, fontSize: 20, fontWeight: FontWeight.w700, fontFamily: 'Outfit', ), ), const Spacer(), if (_isSaving) Padding( padding: const EdgeInsets.only(right: 16), child: SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, color: cs.primary, ), ), ), ], ), ); } String _getPrivacyLabel(String value) { switch (value) { case 'ALL': return 'Все'; case 'CONTACTS': return 'Мои контакты'; case 'NONE': case 'NOBODY': return 'Никто'; default: return value; } } Widget _buildTopSection(ColorScheme cs) { return Container( decoration: BoxDecoration( color: cs.surfaceContainerHigh, borderRadius: BorderRadius.circular(20), ), child: Column( children: [ _buildPasswordRow(cs), _buildNavRow( cs, icon: Symbols.shield, label: 'Семейная защита', subtitle: _privacyConfig?.familyProtection == 'ON' ? 'Включена' : 'Отключена', isLast: true, ), ], ), ); } Widget _buildPasswordRow(ColorScheme cs) { return Column( children: [ Material( color: Colors.transparent, child: InkWell( onTap: () { Navigator.push( context, MaterialPageRoute( builder: (context) => const PasswordEntryScreen(), ), ); }, borderRadius: BorderRadius.zero, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 17), child: Row( children: [ Icon( Symbols.key, color: cs.onSurfaceVariant, size: 22, weight: 400, ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Пароль для входа', style: TextStyle( color: cs.onSurface, fontSize: 16, fontWeight: FontWeight.w500, ), ), const SizedBox(height: 2), Text( _is2faEnabled ? 'Включён' : 'Отключён', style: TextStyle( color: cs.onSurfaceVariant, fontSize: 13, ), ), ], ), ), _buildWarningBadge(cs), const SizedBox(width: 4), Icon( Symbols.chevron_right, color: cs.outline, size: 20, weight: 400, ), ], ), ), ), ), Padding( padding: const EdgeInsets.only(left: 58), child: Divider( height: 1, thickness: 1, color: cs.outlineVariant.withValues(alpha: 0.35), ), ), ], ); } Widget _buildPrivacySettings(ColorScheme cs) { final isSafeMode = _privacyConfig?.safeMode ?? false; return Container( decoration: BoxDecoration( color: cs.surfaceContainerHigh, borderRadius: BorderRadius.circular(20), ), child: Column( children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 17), child: Row( children: [ Icon( Symbols.lock, color: cs.onSurfaceVariant, size: 22, weight: 400, ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Безопасный режим', style: TextStyle( color: cs.onSurface, fontSize: 16, fontWeight: FontWeight.w500, ), ), const SizedBox(height: 2), Text( 'Скрывает личную информацию', style: TextStyle( color: cs.onSurfaceVariant, fontSize: 13, ), ), ], ), ), Switch( value: isSafeMode, onChanged: (v) => showCustomNotification( context, 'Изменение настроек пока недоступно', ), ), ], ), ), if (isSafeMode) ...[ Padding( padding: const EdgeInsets.only(left: 58), child: Divider( height: 1, thickness: 1, color: cs.outlineVariant.withValues(alpha: 0.35), ), ), _buildSubRow( cs, label: 'Найти меня по номеру', value: _getPrivacyLabel(_privacyConfig?.searchByPhone ?? 'ALL'), isLast: false, ), _buildSubRow( cs, label: 'Кто может мне звонить', value: _getPrivacyLabel( _privacyConfig?.incomingCall ?? 'CONTACTS', ), isLast: false, ), _buildSubRow( cs, label: 'Кто может приглашать в чаты', value: _getPrivacyLabel( _privacyConfig?.chatsInvite ?? 'CONTACTS', ), isLast: false, ), _buildSubRow( cs, label: 'Показывать контакт', value: _privacyConfig?.contentLevelAccess == true ? 'Безопасный' : 'Весь', isLast: true, ), ], if (!isSafeMode) ...[ Padding( padding: const EdgeInsets.only(left: 20), child: Divider( height: 1, thickness: 1, color: cs.outlineVariant.withValues(alpha: 0.35), ), ), _buildOptionRow( cs, icon: Symbols.phone, label: 'Кто может мне звонить', value: _getPrivacyLabel( _privacyConfig?.incomingCall ?? 'CONTACTS', ), isLast: false, onTap: () => _showOptionSheet( context, cs, title: 'Кто может мне звонить', currentValue: _privacyConfig?.incomingCall ?? 'CONTACTS', options: const [('ALL', 'Все'), ('CONTACTS', 'Мои контакты')], onSelect: (value) => _updateSetting('INCOMING_CALL', value), ), ), _buildOptionRow( cs, icon: Symbols.group, label: 'Кто может приглашать в чаты', value: _getPrivacyLabel( _privacyConfig?.chatsInvite ?? 'CONTACTS', ), isLast: false, onTap: () => _showOptionSheet( context, cs, title: 'Кто может приглашать в чаты', currentValue: _privacyConfig?.chatsInvite ?? 'CONTACTS', options: const [('ALL', 'Все'), ('CONTACTS', 'Мои контакты')], onSelect: (value) => _updateSetting('CHATS_INVITE', value), ), ), _buildOptionRow( cs, icon: Symbols.contact_phone, label: 'Найти меня по номеру', value: _getPrivacyLabel(_privacyConfig?.searchByPhone ?? 'ALL'), isLast: false, onTap: () => _showOptionSheet( context, cs, title: 'Найти меня по номеру', currentValue: _privacyConfig?.searchByPhone ?? 'ALL', options: const [('ALL', 'Все'), ('CONTACTS', 'Мои контакты')], onSelect: (value) => _updateSetting('SEARCH_BY_PHONE', value), ), ), _buildOptionRow( cs, icon: Icons.visibility_off_outlined, label: 'Видеть статус «в сети»', value: _privacyConfig?.hidden == true ? 'Никто' : 'Мои контакты', isLast: false, onTap: () => _showHiddenStatusSheet(context, cs), ), _buildOptionRow( cs, icon: Symbols.contact_page, label: 'Видеть мой номер', value: _getPrivacyLabel( _privacyConfig?.phoneNumberPrivacy ?? 'ALL', ), isLast: true, onTap: () => _showOptionSheet( context, cs, title: 'Видеть мой номер', currentValue: _privacyConfig?.phoneNumberPrivacy ?? 'ALL', options: const [ ('ALL', 'Все'), ('CONTACTS', 'Мои контакты'), ('NOBODY', 'Никто'), ], onSelect: (value) => _updateSetting('PHONE_NUMBER_PRIVACY', value), ), ), ], ], ), ); } void _showOptionSheet( BuildContext context, ColorScheme cs, { required String title, required String currentValue, required List<(String, String)> options, required void Function(String) onSelect, }) { showModalBottomSheet( context: context, backgroundColor: cs.surface, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), builder: (context) { return SafeArea( child: Column( mainAxisSize: MainAxisSize.min, children: [ const SizedBox(height: 8), Container( width: 36, height: 4, decoration: BoxDecoration( color: cs.onSurfaceVariant.withValues(alpha: 0.3), borderRadius: BorderRadius.circular(2), ), ), const SizedBox(height: 16), Text( title, style: TextStyle( color: cs.onSurface, fontSize: 17, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 8), ...options.map((option) { final isSelected = option.$1 == currentValue; return Material( color: Colors.transparent, child: InkWell( onTap: () { Navigator.pop(context); onSelect(option.$1); }, child: Padding( padding: const EdgeInsets.symmetric( horizontal: 20, vertical: 16, ), child: Row( children: [ Expanded( child: Text( option.$2, style: TextStyle( color: cs.onSurface, fontSize: 16, ), ), ), if (isSelected) Icon(Symbols.check, color: cs.primary, size: 20), ], ), ), ), ); }), const SizedBox(height: 16), ], ), ); }, ); } void _showHiddenStatusSheet(BuildContext context, ColorScheme cs) { final currentValue = _privacyConfig?.hidden == true ? 'NONE' : 'CONTACTS'; if (currentValue == 'NONE') { showDialog( context: context, builder: (context) => AlertDialog( backgroundColor: cs.surfaceContainerHigh, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), ), title: Text('Вы уверены?', style: TextStyle(color: cs.onSurface)), content: Text( 'Вы не сможете видеть статусы посещения других пользователей.', style: TextStyle(color: cs.onSurfaceVariant), ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: Text('Отмена', style: TextStyle(color: cs.primary)), ), TextButton( onPressed: () { Navigator.pop(context); _updateSetting('HIDDEN', false); }, child: Text('Да', style: TextStyle(color: cs.primary)), ), ], ), ); return; } showModalBottomSheet( context: context, backgroundColor: cs.surface, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), builder: (context) { return SafeArea( child: Column( mainAxisSize: MainAxisSize.min, children: [ const SizedBox(height: 8), Container( width: 36, height: 4, decoration: BoxDecoration( color: cs.onSurfaceVariant.withValues(alpha: 0.3), borderRadius: BorderRadius.circular(2), ), ), const SizedBox(height: 16), Text( 'Видеть статус «в сети»', style: TextStyle( color: cs.onSurface, fontSize: 17, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 8), _buildOptionSheetItem( cs, 'Мои контакты', currentValue == 'CONTACTS', () { Navigator.pop(context); _updateSetting('HIDDEN', false); }, ), _buildOptionSheetItem(cs, 'Никто', currentValue == 'NONE', () { Navigator.pop(context); _showHiddenStatusConfirmDialog(context, cs); }, isLast: true), const SizedBox(height: 16), ], ), ); }, ); } void _showHiddenStatusConfirmDialog(BuildContext context, ColorScheme cs) { showDialog( context: context, builder: (context) => AlertDialog( backgroundColor: cs.surfaceContainerHigh, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), title: Text('Вы уверены?', style: TextStyle(color: cs.onSurface)), content: Text( 'Вы не сможете видеть статусы посещения других пользователей.', style: TextStyle(color: cs.onSurfaceVariant), ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: Text('Отмена', style: TextStyle(color: cs.primary)), ), TextButton( onPressed: () { Navigator.pop(context); _updateSetting('HIDDEN', true); }, child: Text('Да', style: TextStyle(color: cs.primary)), ), ], ), ); } Widget _buildOptionSheetItem( ColorScheme cs, String label, bool isSelected, VoidCallback onTap, { bool isLast = false, }) { return Column( children: [ Material( color: Colors.transparent, child: InkWell( onTap: onTap, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), child: Row( children: [ Expanded( child: Text( label, style: TextStyle(color: cs.onSurface, fontSize: 16), ), ), if (isSelected) Icon(Symbols.check, color: cs.primary, size: 20), ], ), ), ), ), if (!isLast) Padding( padding: const EdgeInsets.only(left: 20), child: Divider( height: 1, color: cs.outlineVariant.withValues(alpha: 0.3), ), ), ], ); } Widget _buildInfoLabel(ColorScheme cs) { return Padding( padding: const EdgeInsets.only(left: 4, bottom: 0), child: Text( 'КОНФИДЕНЦИАЛЬНОСТЬ', style: TextStyle( color: cs.onSurfaceVariant.withValues(alpha: 0.6), fontSize: 12, fontWeight: FontWeight.w600, letterSpacing: 0.8, ), ), ); } Widget _buildConfidentialSection(ColorScheme cs) { return Container( decoration: BoxDecoration( color: cs.surfaceContainerHigh, borderRadius: BorderRadius.circular(20), ), child: Column( children: [ _buildSwitchRow( cs, icon: Symbols.description, label: 'Галочки «Прочитано»', value: _privacyConfig?.showReadMark ?? true, isLast: false, onChanged: (v) => _updateSetting('SHOW_READ_MARK', v), ), _buildSwitchRow( cs, icon: Symbols.keyboard_alt, label: 'Альтернативная клавиатура', value: _privacyConfig?.altKeyboard ?? false, isLast: false, onChanged: (v) => _updateSetting('ALT_KEYBOARD', v), ), _buildSwitchRow( cs, icon: Symbols.warning, label: 'Принимать опасные файлы', value: _privacyConfig?.unsafeFiles ?? true, isLast: false, onChanged: (v) => _updateSetting('UNSAFE_FILES', v), ), _buildSwitchRow( cs, icon: Icons.mic_none_outlined, label: 'Транскрибация аудио', value: _privacyConfig?.audioTranscriptionEnabled ?? true, isLast: true, onChanged: (v) => _updateSetting('AUDIO_TRANSCRIPTION_ENABLED', v), ), ], ), ); } Widget _buildBlacklistSection(ColorScheme cs) { final count = _blockedContacts.length; return Container( decoration: BoxDecoration( color: cs.surfaceContainerHigh, borderRadius: BorderRadius.circular(20), ), child: Material( color: Colors.transparent, child: InkWell( onTap: () => showCustomNotification( context, 'Чёрный список: $count контактов', ), borderRadius: BorderRadius.circular(20), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 17), child: Row( children: [ Icon( Symbols.block, color: cs.onSurfaceVariant, size: 22, weight: 400, ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Чёрный список', style: TextStyle( color: cs.onSurface, fontSize: 16, fontWeight: FontWeight.w500, ), ), const SizedBox(height: 2), Text( '$count ${_getBlockedCountText(count)}', style: TextStyle( color: cs.onSurfaceVariant, fontSize: 13, ), ), ], ), ), const SizedBox(width: 8), Icon( Symbols.chevron_right, color: cs.outline, size: 20, weight: 400, ), ], ), ), ), ), ); } String _getBlockedCountText(int count) { if (count == 0) return 'контактов'; final mod = count % 10; if (mod == 1 && count != 11) return 'контакт'; if (mod >= 2 && mod <= 4 && (count < 10 || count > 20)) return 'контакта'; return 'контактов'; } Widget _buildNavRow( ColorScheme cs, { required IconData icon, required String label, String? subtitle, String? value, Widget? trailing, required bool isLast, }) { return Column( children: [ Material( color: Colors.transparent, child: InkWell( onTap: () => showCustomNotification(context, label), borderRadius: isLast ? const BorderRadius.vertical(bottom: Radius.circular(20)) : BorderRadius.zero, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 17), child: Row( children: [ Icon(icon, color: cs.onSurfaceVariant, size: 22, weight: 400), const SizedBox(width: 16), Expanded( child: subtitle != null ? Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, style: TextStyle( color: cs.onSurface, fontSize: 16, fontWeight: FontWeight.w500, ), ), const SizedBox(height: 2), Text( subtitle, style: TextStyle( color: cs.onSurfaceVariant, fontSize: 13, ), ), ], ) : Text( label, style: TextStyle( color: cs.onSurface, fontSize: 16, fontWeight: FontWeight.w500, ), ), ), if (value != null) Text( value, style: TextStyle( color: cs.onSurfaceVariant, fontSize: 14, ), ), ?trailing, const SizedBox(width: 4), Icon( Symbols.chevron_right, color: cs.outline, size: 20, weight: 400, ), ], ), ), ), ), if (!isLast) Padding( padding: const EdgeInsets.only(left: 58), child: Divider( height: 1, thickness: 1, color: cs.outlineVariant.withValues(alpha: 0.35), ), ), ], ); } Widget _buildOptionRow( ColorScheme cs, { required IconData icon, required String label, required String value, required bool isLast, required VoidCallback onTap, }) { return Column( children: [ Material( color: Colors.transparent, child: InkWell( onTap: onTap, borderRadius: isLast ? const BorderRadius.vertical(bottom: Radius.circular(20)) : BorderRadius.zero, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 17), child: Row( children: [ Icon(icon, color: cs.onSurfaceVariant, size: 22, weight: 400), const SizedBox(width: 16), Expanded( child: Text( label, style: TextStyle( color: cs.onSurface, fontSize: 16, fontWeight: FontWeight.w500, ), ), ), Text( value, style: TextStyle(color: cs.onSurfaceVariant, fontSize: 14), ), const SizedBox(width: 4), Icon( Symbols.chevron_right, color: cs.outline, size: 20, weight: 400, ), ], ), ), ), ), if (!isLast) Padding( padding: const EdgeInsets.only(left: 58), child: Divider( height: 1, thickness: 1, color: cs.outlineVariant.withValues(alpha: 0.35), ), ), ], ); } Widget _buildSubRow( ColorScheme cs, { required String label, required String value, required bool isLast, }) { return Column( children: [ Material( color: Colors.transparent, child: InkWell( onTap: () => showCustomNotification(context, label), borderRadius: isLast ? const BorderRadius.vertical(bottom: Radius.circular(20)) : BorderRadius.zero, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), child: Row( children: [ Expanded( child: Text( label, style: TextStyle(color: cs.onSurface, fontSize: 15), ), ), Text( value, style: TextStyle(color: cs.onSurfaceVariant, fontSize: 14), ), const SizedBox(width: 4), Icon( Symbols.chevron_right, color: cs.outline, size: 18, weight: 400, ), ], ), ), ), ), if (!isLast) Divider( height: 1, thickness: 1, color: cs.outlineVariant.withValues(alpha: 0.35), indent: 20, endIndent: 20, ), ], ); } Widget _buildSwitchRow( ColorScheme cs, { required IconData icon, required String label, required bool value, required bool isLast, required void Function(bool) onChanged, }) { return Column( children: [ Material( color: Colors.transparent, child: InkWell( onTap: () => onChanged(!value), borderRadius: isLast ? const BorderRadius.vertical(bottom: Radius.circular(20)) : BorderRadius.zero, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14), child: Row( children: [ Icon(icon, color: cs.onSurfaceVariant, size: 22, weight: 400), const SizedBox(width: 16), Expanded( child: Text( label, style: TextStyle( color: cs.onSurface, fontSize: 16, fontWeight: FontWeight.w500, ), ), ), Switch(value: value, onChanged: onChanged), ], ), ), ), ), if (!isLast) Padding( padding: const EdgeInsets.only(left: 58), child: Divider( height: 1, thickness: 1, color: cs.outlineVariant.withValues(alpha: 0.35), ), ), ], ); } Widget _buildWarningBadge(ColorScheme cs) { return Container( width: 22, height: 22, decoration: BoxDecoration(color: cs.error, shape: BoxShape.circle), child: Icon( Symbols.priority_high, color: cs.onError, size: 14, weight: 700, ), ); } }