import 'dart:async'; import 'package:flutter/material.dart'; import 'package:gwid/screens/chats_screen.dart'; import 'package:gwid/screens/phone_entry_screen.dart'; import 'package:gwid/api/api_service.dart'; import 'package:gwid/screens/settings/reconnection_screen.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:gwid/services/version_checker.dart'; import 'package:app_links/app_links.dart'; import 'package:gwid/models/chat.dart'; import 'package:gwid/models/contact.dart'; import 'package:gwid/models/profile.dart'; import 'package:gwid/screens/chat_screen.dart'; import 'package:provider/provider.dart'; import 'package:gwid/utils/theme_provider.dart'; class HomeScreen extends StatefulWidget { const HomeScreen({super.key}); @override State createState() => _HomeScreenState(); } class _HomeScreenState extends State { late Future> _chatsFuture; Profile? _myProfile; bool _isProfileLoading = true; String? _connectionStatus; StreamSubscription? _connectionSubscription; StreamSubscription? _messageSubscription; late final AppLinks _appLinks; StreamSubscription? _linkSubscription; Uri? _initialUri; @override void initState() { super.initState(); _loadMyProfile(); _chatsFuture = (() async { try { await ApiService.instance.waitUntilOnline(); return ApiService.instance.getChatsAndContacts(); } catch (e) { print('Ошибка получения чатов в HomeScreen: $e'); if (e.toString().contains('Auth token not found') || e.toString().contains('FAIL_WRONG_PASSWORD')) {} rethrow; } })(); _checkVersionInBackground(); _initDeepLinking(); _showSpoofUpdateDialogIfNeeded(); _connectionSubscription = ApiService.instance.connectionStatus.listen(( status, ) { if (mounted) { setState(() => _connectionStatus = status); Future.delayed(const Duration(seconds: 3), () { if (mounted) { setState(() => _connectionStatus = null); } }); } }); _messageSubscription = ApiService.instance.messages.listen((message) { if (message['type'] == 'session_terminated' && mounted) { _handleSessionTerminated(message['message']); } else if (message['type'] == 'invalid_token' && mounted) { _handleInvalidToken(message['message']); } else if (message['type'] == 'group_join_success' && mounted) { _handleGroupJoinSuccess(message); } else if (message['cmd'] == 3 && message['opcode'] == 57 && mounted) { _handleGroupJoinError(message); } }); } Future _loadMyProfile() async { if (!mounted) return; setState(() => _isProfileLoading = true); try { final cachedProfile = ApiService.instance.lastChatsPayload?['profile']; if (cachedProfile != null) { if (mounted) { setState(() { _myProfile = Profile.fromJson(cachedProfile); _isProfileLoading = false; }); } } else { final result = await ApiService.instance.getChatsAndContacts( force: false, ); if (mounted) { final profileJson = result['profile']; if (profileJson != null) { setState(() { _myProfile = Profile.fromJson(profileJson); _isProfileLoading = false; }); } else { setState(() => _isProfileLoading = false); } } } } catch (e) { if (mounted) setState(() => _isProfileLoading = false); print("Ошибка загрузки профиля в _HomeScreenState: $e"); } } Future _showUpdateDialog( BuildContext context, String newVersion, ) async { await showDialog( context: context, barrierDismissible: false, // Зачем давать им выбор оло builder: (BuildContext dialogContext) { return AlertDialog( title: const Text('Доступно обновление'), content: Text( 'Найдена новая версия приложения: $newVersion. Рекомендуется обновить данные сессии, чтобы соответствовать последней версии.', ), actions: [ TextButton( child: const Text('Отменить'), onPressed: () { Navigator.of(dialogContext).pop(); }, ), FilledButton( child: const Text('Обновить'), onPressed: () async { final prefs = await SharedPreferences.getInstance(); await prefs.setString('spoof_appversion', newVersion); try { await ApiService.instance.performFullReconnection(); print("Переподключение выполнено успешно"); } catch (e) { print("Ошибка переподключения: $e"); } if (mounted) { Navigator.of(dialogContext).pop(); } if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Версия сессии обновлена до $newVersion!'), backgroundColor: Colors.green.shade700, ), ); } }, ), ], ); }, ); } Future _checkVersionInBackground() async { try { final prefs = await SharedPreferences.getInstance(); final isWebVersionCheckEnabled = prefs.getBool('enable_web_version_check') ?? false; if (!isWebVersionCheckEnabled) { print("Web version checking is disabled, skipping check"); return; } final isAutoUpdateEnabled = prefs.getBool('auto_update_enabled') ?? false; final showUpdateNotification = prefs.getBool('show_update_notification') ?? true; final currentVersion = prefs.getString('spoof_appversion') ?? '0.0.0'; final latestVersion = await VersionChecker.getLatestVersion(); if (latestVersion != currentVersion) { if (isAutoUpdateEnabled) { await prefs.setString('spoof_appversion', latestVersion); print("Версия сессии автоматически обновлена до $latestVersion"); try { await ApiService.instance.performFullReconnection(); print("Переподключение выполнено успешно"); } catch (e) { print("Ошибка переподключения: $e"); } if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( 'Спуф сессии автоматически обновлен до версии $latestVersion', ), backgroundColor: Colors.green.shade700, behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), margin: const EdgeInsets.all(10), ), ); } } else if (showUpdateNotification) { if (mounted) { _showUpdateDialog(context, latestVersion); } } } } catch (e) { print("Фоновая проверка версии не удалась: $e"); } } Future _initDeepLinking() async { _appLinks = AppLinks(); Uri? initialUriFromLaunch; try { initialUriFromLaunch = await _appLinks.getInitialLink(); if (initialUriFromLaunch != null) { print('Получена ссылка (initial): $initialUriFromLaunch'); if (mounted) { _handleJoinLink(initialUriFromLaunch); } } } catch (e) { print('Ошибка получения initial link: $e'); } _linkSubscription = _appLinks.uriLinkStream.listen((uri) { print('Получена ссылка (stream): $uri'); if (uri == initialUriFromLaunch) { print('Ссылка из stream совпадает с initial, игнорируем.'); initialUriFromLaunch = null; return; } if (mounted) { _handleJoinLink(uri); } }); } void _handleJoinLink(Uri uri) { if (uri.host != 'max.ru') return; String fullLink = uri.toString(); // На всякий случай убираем возможный префикс '@' перед ссылкой if (fullLink.startsWith('@')) { fullLink = fullLink.substring(1); } final bool isGroupLink = uri.path.startsWith('/join/'); final bool isChannelLink = !isGroupLink && uri.pathSegments.isNotEmpty && uri.pathSegments.first.startsWith('id'); if (!isGroupLink && !isChannelLink) { return; } if (isGroupLink) { final String processedLink = _extractJoinLink(fullLink); if (!processedLink.contains('join/')) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text( 'Неверный формат ссылки. Ссылка должна содержать "join/"', ), backgroundColor: Colors.orange, behavior: SnackBarBehavior.floating, margin: EdgeInsets.all(10), ), ); return; } ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Загрузка информации о группе...'), behavior: SnackBarBehavior.floating, margin: EdgeInsets.all(10), duration: Duration(seconds: 10), ), ); ApiService.instance.waitUntilOnline().then((_) { ApiService.instance .getChatInfoByLink(processedLink) .then((chatInfo) { ScaffoldMessenger.of(context).hideCurrentSnackBar(); if (mounted) { _showJoinConfirmationDialog(chatInfo, processedLink); } }) .catchError((error) { ScaffoldMessenger.of(context).hideCurrentSnackBar(); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Ошибка: ${error.toString()}'), backgroundColor: Theme.of(context).colorScheme.error, behavior: SnackBarBehavior.floating, margin: const EdgeInsets.all(10), ), ); } }); }); } else if (isChannelLink) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Загрузка информации о канале...'), behavior: SnackBarBehavior.floating, margin: EdgeInsets.all(10), duration: Duration(seconds: 10), ), ); ApiService.instance.waitUntilOnline().then((_) { ApiService.instance .getChatInfoByLink(fullLink) .then((chatInfo) { ScaffoldMessenger.of(context).hideCurrentSnackBar(); if (mounted) { _showChannelSubscribeDialog(chatInfo, fullLink); } }) .catchError((error) { ScaffoldMessenger.of(context).hideCurrentSnackBar(); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Ошибка: ${error.toString()}'), backgroundColor: Theme.of(context).colorScheme.error, behavior: SnackBarBehavior.floating, margin: const EdgeInsets.all(10), ), ); } }); }); } } void _showJoinConfirmationDialog( Map chatInfo, String linkToJoin, ) { final String title = chatInfo['title'] ?? 'Без названия'; final String? iconUrl = chatInfo['baseIconUrl']; int joinState = 0; String? errorMessage; showDialog( context: context, barrierDismissible: false, builder: (dialogContext) { return StatefulBuilder( builder: (context, setState) { Widget content; List actions = []; if (joinState == 1) { content = Column( mainAxisSize: MainAxisSize.min, children: [ const SizedBox(height: 32), const CircularProgressIndicator(), const SizedBox(height: 24), Text( 'Присоединение...', style: Theme.of(context).textTheme.titleLarge, textAlign: TextAlign.center, ), const SizedBox(height: 32), ], ); actions = []; } else if (joinState == 2) { content = Column( mainAxisSize: MainAxisSize.min, children: [ const SizedBox(height: 32), const Icon( Icons.check_circle_outline, color: Colors.green, size: 60, ), const SizedBox(height: 24), Text( 'Вы вступили в группу!', style: Theme.of(context).textTheme.titleLarge, textAlign: TextAlign.center, ), const SizedBox(height: 32), ], ); actions = [ FilledButton( child: const Text('Отлично'), onPressed: () { Navigator.of(dialogContext).pop(); }, ), ]; } else if (joinState == 3) { content = Column( mainAxisSize: MainAxisSize.min, children: [ const SizedBox(height: 32), Icon( Icons.error_outline, color: Theme.of(context).colorScheme.error, size: 60, ), const SizedBox(height: 24), Text( 'Ошибка', style: Theme.of(context).textTheme.titleLarge, textAlign: TextAlign.center, ), const SizedBox(height: 8), Text( errorMessage ?? 'Не удалось вступить в группу.', textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodySmall, ), const SizedBox(height: 32), ], ); actions = [ TextButton( child: const Text('Закрыть'), onPressed: () { Navigator.of(dialogContext).pop(); }, ), ]; } else { content = Column( mainAxisSize: MainAxisSize.min, children: [ if (iconUrl != null && iconUrl.isNotEmpty) CircleAvatar( radius: 60, backgroundImage: NetworkImage(iconUrl), onBackgroundImageError: (e, s) { print("Ошибка загрузки аватара: $e"); }, backgroundColor: Colors.grey.shade300, ) else CircleAvatar( radius: 60, backgroundColor: Colors.grey.shade300, child: const Icon( Icons.group, size: 60, color: Colors.white, ), ), const SizedBox(height: 24), Text( title, style: Theme.of(context).textTheme.titleLarge, textAlign: TextAlign.center, ), const SizedBox(height: 8), const Text( 'Вы действительно хотите вступить в эту группу?', textAlign: TextAlign.center, ), ], ); actions = [ TextButton( child: const Text('Отмена'), onPressed: () { Navigator.of(dialogContext).pop(); }, ), FilledButton( child: const Text('Вступить'), onPressed: () async { setState(() { joinState = 1; }); try { await ApiService.instance.joinGroupByLink(linkToJoin); setState(() { joinState = 2; }); ApiService.instance.clearChatsCache(); await Future.delayed(const Duration(seconds: 2)); if (mounted) { Navigator.of(dialogContext).pop(); } } catch (e) { setState(() { joinState = 3; errorMessage = e.toString().replaceFirst( "Exception: ", "", ); }); } }, ), ]; } return AlertDialog( title: joinState == 0 ? const Text('Вступить в группу?') : null, content: AnimatedSize( duration: const Duration(milliseconds: 250), curve: Curves.easeInOut, child: AnimatedSwitcher( duration: const Duration(milliseconds: 250), transitionBuilder: (Widget child, Animation animation) { final slideAnimation = Tween( begin: const Offset(0, 0.2), end: Offset.zero, ).animate( CurvedAnimation( parent: animation, curve: Curves.easeOutQuart, ), ); return FadeTransition( opacity: animation, child: SlideTransition( position: slideAnimation, child: child, ), ); }, child: Container( key: ValueKey(joinState), child: content, ), ), ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), actionsAlignment: MainAxisAlignment.center, actions: actions, ); }, ); }, ); } void _showChannelSubscribeDialog( Map chatInfo, String linkToJoin, ) { final String title = chatInfo['title'] ?? 'Канал'; final String? iconUrl = chatInfo['baseIconUrl'] ?? chatInfo['baseUrl'] ?? chatInfo['iconUrl']; int subscribeState = 0; String? errorMessage; showDialog( context: context, barrierDismissible: false, builder: (dialogContext) { return StatefulBuilder( builder: (context, setState) { Widget content; List actions = []; if (subscribeState == 1) { content = Column( mainAxisSize: MainAxisSize.min, children: [ const SizedBox(height: 32), const CircularProgressIndicator(), const SizedBox(height: 24), Text( 'Подписка...', style: Theme.of(context).textTheme.titleLarge, textAlign: TextAlign.center, ), const SizedBox(height: 32), ], ); actions = []; } else if (subscribeState == 2) { content = Column( mainAxisSize: MainAxisSize.min, children: [ const SizedBox(height: 32), const Icon( Icons.check_circle_outline, color: Colors.green, size: 60, ), const SizedBox(height: 24), Text( 'Вы подписались на канал!', style: Theme.of(context).textTheme.titleLarge, textAlign: TextAlign.center, ), const SizedBox(height: 32), ], ); actions = [ FilledButton( child: const Text('Отлично'), onPressed: () { Navigator.of(dialogContext).pop(); }, ), ]; } else if (subscribeState == 3) { content = Column( mainAxisSize: MainAxisSize.min, children: [ const SizedBox(height: 32), Icon( Icons.error_outline, color: Theme.of(context).colorScheme.error, size: 60, ), const SizedBox(height: 24), Text( 'Ошибка', style: Theme.of(context).textTheme.titleLarge, textAlign: TextAlign.center, ), const SizedBox(height: 8), Text( errorMessage ?? 'Не удалось подписаться на канал.', textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodySmall, ), const SizedBox(height: 32), ], ); actions = [ TextButton( child: const Text('Закрыть'), onPressed: () { Navigator.of(dialogContext).pop(); }, ), ]; } else { content = Column( mainAxisSize: MainAxisSize.min, children: [ if (iconUrl != null && iconUrl.isNotEmpty) CircleAvatar( radius: 60, backgroundImage: NetworkImage(iconUrl), onBackgroundImageError: (e, s) { print("Ошибка загрузки аватара канала: $e"); }, backgroundColor: Colors.grey.shade300, ) else CircleAvatar( radius: 60, backgroundColor: Colors.grey.shade300, child: const Icon( Icons.campaign, size: 60, color: Colors.white, ), ), const SizedBox(height: 24), Text( title, style: Theme.of(context).textTheme.titleLarge, textAlign: TextAlign.center, ), const SizedBox(height: 8), const Text( 'Вы действительно хотите подписаться на этот канал?', textAlign: TextAlign.center, ), ], ); actions = [ TextButton( child: const Text('Отмена'), onPressed: () { Navigator.of(dialogContext).pop(); }, ), FilledButton( child: const Text('Подписаться'), onPressed: () async { setState(() { subscribeState = 1; }); try { await ApiService.instance.subscribeToChannel(linkToJoin); setState(() { subscribeState = 2; }); ApiService.instance.clearChatsCache(); await Future.delayed(const Duration(seconds: 2)); if (mounted) { Navigator.of(dialogContext).pop(); } } catch (e) { setState(() { subscribeState = 3; errorMessage = e.toString().replaceFirst( "Exception: ", "", ); }); } }, ), ]; } return AlertDialog( title: subscribeState == 0 ? const Text('Подписаться на канал?') : null, content: AnimatedSize( duration: const Duration(milliseconds: 250), curve: Curves.easeInOut, child: AnimatedSwitcher( duration: const Duration(milliseconds: 250), transitionBuilder: (Widget child, Animation animation) { final slideAnimation = Tween( begin: const Offset(0, 0.2), end: Offset.zero, ).animate( CurvedAnimation( parent: animation, curve: Curves.easeOutQuart, ), ); return FadeTransition( opacity: animation, child: SlideTransition( position: slideAnimation, child: child, ), ); }, child: Container( key: ValueKey(subscribeState), child: content, ), ), ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), actionsAlignment: MainAxisAlignment.center, actions: actions, ); }, ); }, ); } String _extractJoinLink(String inputLink) { final link = inputLink.trim(); if (link.startsWith('join/')) { print('Ссылка уже в правильном формате: $link'); return link; } final joinIndex = link.indexOf('join/'); if (joinIndex != -1) { final extractedLink = link.substring(joinIndex); print('Извлечена ссылка из полной ссылки: $link -> $extractedLink'); return extractedLink; } print('Не найдено "join/" в ссылке: $link'); return link; } void _handleGroupJoinSuccess(Map message) { final payload = message['payload']; final chat = payload['chat']; final chatTitle = chat?['title'] ?? 'Группа'; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Успешно присоединились к группе "$chatTitle"!'), backgroundColor: Colors.green, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), behavior: SnackBarBehavior.floating, margin: const EdgeInsets.all(10), ), ); } Future _showSpoofUpdateDialogIfNeeded() async { final prefs = await SharedPreferences.getInstance(); final shouldShow = prefs.getBool('show_spoof_update_dialog') ?? true; if (!shouldShow || !mounted) return; Future.delayed(const Duration(milliseconds: 500), () { if (!mounted) return; showDialog( context: context, barrierDismissible: false, builder: (BuildContext context) { bool dontShowAgain = false; return StatefulBuilder( builder: (context, setState) { return AlertDialog( title: const Text('Проверка обновлений'), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('Хотите проверить обновления спуфа?'), const SizedBox(height: 16), Row( children: [ Checkbox( value: dontShowAgain, onChanged: (value) { setState(() { dontShowAgain = value ?? false; }); }, ), const Expanded(child: Text('Больше не показывать')), ], ), ], ), actions: [ TextButton( onPressed: () async { if (dontShowAgain) { final prefs = await SharedPreferences.getInstance(); await prefs.setBool('show_spoof_update_dialog', false); } Navigator.of(context).pop(); }, child: const Text('Нет'), ), TextButton( onPressed: () async { if (dontShowAgain) { final prefs = await SharedPreferences.getInstance(); await prefs.setBool('show_spoof_update_dialog', false); } Navigator.of(context).pop(); await _checkSpoofUpdateManually(); }, child: const Text('Ок!'), ), ], ); }, ); }, ); }); } Future _checkSpoofUpdateManually() async { try { final prefs = await SharedPreferences.getInstance(); final isAutoUpdateEnabled = prefs.getBool('auto_update_enabled') ?? false; final currentVersion = prefs.getString('spoof_appversion') ?? '0.0.0'; final latestVersion = await VersionChecker.getLatestVersion(); if (latestVersion != currentVersion) { if (isAutoUpdateEnabled) { await prefs.setString('spoof_appversion', latestVersion); print("Версия сессии обновлена до $latestVersion"); try { await ApiService.instance.performFullReconnection(); print("Переподключение выполнено успешно"); } catch (e) { print("Ошибка переподключения: $e"); } if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Спуф сессии обновлен до версии $latestVersion'), backgroundColor: Colors.green.shade700, behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), margin: const EdgeInsets.all(10), ), ); } } else { if (mounted) { _showUpdateDialog(context, latestVersion); } } } else { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: const Text('Версия спуфа актуальна'), backgroundColor: Colors.blue.shade700, behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), margin: const EdgeInsets.all(10), ), ); } } } catch (e) { print("Проверка версии спуфа не удалась: $e"); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Ошибка проверки обновлений: $e'), backgroundColor: Colors.red.shade700, behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), margin: const EdgeInsets.all(10), ), ); } } } void _handleGroupJoinError(Map message) { final errorPayload = message['payload']; String errorMessage = 'Неизвестная ошибка'; if (errorPayload != null) { if (errorPayload['localizedMessage'] != null) { errorMessage = errorPayload['localizedMessage']; } else if (errorPayload['message'] != null) { errorMessage = errorPayload['message']; } } ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(errorMessage), backgroundColor: Theme.of(context).colorScheme.error, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), behavior: SnackBarBehavior.floating, margin: const EdgeInsets.all(10), ), ); } Future _checkAndConnect() async { final hasToken = await ApiService.instance.hasToken(); if (hasToken) { print("В HomeScreen: токен найден, проверяем подключение..."); try { await ApiService.instance.connect(); print("В HomeScreen: подключение к WebSocket успешно"); } catch (e) { print("В HomeScreen: ошибка подключения к WebSocket: $e"); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Ошибка подключения к серверу: $e'), backgroundColor: Colors.red, duration: const Duration(seconds: 5), ), ); } } } else { print("В HomeScreen: токен не найден, пользователь не авторизован"); } } void _handleSessionTerminated(String message) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(message), backgroundColor: Theme.of(context).colorScheme.error, duration: const Duration(seconds: 3), ), ); Future.delayed(const Duration(seconds: 2), () { if (mounted) { Navigator.of(context).pushAndRemoveUntil( PageRouteBuilder( pageBuilder: (context, animation, secondaryAnimation) => const PhoneEntryScreen(), transitionsBuilder: (context, animation, secondaryAnimation, child) { return FadeTransition(opacity: animation, child: child); }, transitionDuration: const Duration(milliseconds: 500), ), (route) => false, ); } }); } void _showReconnectionScreen() { Navigator.of(context).push( MaterialPageRoute( builder: (context) => const ReconnectionScreen(), fullscreenDialog: true, ), ); } void _handleInvalidToken(String message) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(message), backgroundColor: Colors.red, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), behavior: SnackBarBehavior.floating, margin: const EdgeInsets.all(10), ), ); Navigator.of(context).pushAndRemoveUntil( MaterialPageRoute(builder: (context) => const PhoneEntryScreen()), (route) => false, ); } @override void dispose() { _connectionSubscription?.cancel(); _messageSubscription?.cancel(); _linkSubscription?.cancel(); super.dispose(); } static const double kDesktopLayoutBreakpoint = 700.0; @override Widget build(BuildContext context) { return Consumer( builder: (context, themeProvider, child) { return LayoutBuilder( builder: (context, constraints) { final shouldUseDesktopLayout = themeProvider.useDesktopLayout && constraints.maxWidth >= kDesktopLayoutBreakpoint; if (shouldUseDesktopLayout) { return const _DesktopLayout(); } else { return const ChatsScreen(); } }, ); }, ); } } class _DesktopLayout extends StatefulWidget { const _DesktopLayout({super.key}); @override State<_DesktopLayout> createState() => _DesktopLayoutState(); } class _DesktopLayoutState extends State<_DesktopLayout> { Chat? _selectedChat; Contact? _selectedContact; bool _isGroupChat = false; bool _isChannel = false; int? _participantCount; Profile? _myProfile; bool _isProfileLoading = true; final ValueNotifier _leftPanelWidth = ValueNotifier(320.0); static const double _minPanelWidth = 280.0; static const double _maxPanelWidth = 500.0; @override void initState() { super.initState(); _loadMyProfile(); } Future _loadMyProfile() async { if (!mounted) return; setState(() => _isProfileLoading = true); try { final result = await ApiService.instance.getChatsAndContacts( force: false, ); if (mounted) { final profileJson = result['profile']; if (profileJson != null) { setState(() { _myProfile = Profile.fromJson(profileJson); _isProfileLoading = false; }); } } } catch (e) { if (mounted) setState(() => _isProfileLoading = false); print("Ошибка загрузки профиля в _DesktopLayout: $e"); } } void _onChatSelected( Chat chat, Contact contact, bool isGroup, bool isChannel, int? participantCount, ) { setState(() { _selectedChat = chat; _selectedContact = contact; _isGroupChat = isGroup; _isChannel = isChannel; _participantCount = participantCount; }); } @override Widget build(BuildContext context) { final colors = Theme.of(context).colorScheme; return Scaffold( body: Row( children: [ ValueListenableBuilder( valueListenable: _leftPanelWidth, builder: (context, width, child) { return SizedBox( width: width, child: ChatsScreen(onChatSelected: _onChatSelected), ); }, ), GestureDetector( onPanUpdate: (details) { final newWidth = _leftPanelWidth.value + details.delta.dx; if (newWidth >= _minPanelWidth && newWidth <= _maxPanelWidth) { _leftPanelWidth.value = newWidth; } }, child: MouseRegion( cursor: SystemMouseCursors.resizeLeftRight, child: Container( width: 4.0, color: colors.outline.withOpacity(0.3), ), ), ), Expanded( child: (_selectedChat == null || _selectedContact == null || _isProfileLoading) ? Center( child: _isProfileLoading ? const CircularProgressIndicator() : Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.message, size: 80, color: colors.primary.withOpacity(0.5), ), const SizedBox(height: 16), Text( 'Выберите чат, чтобы начать общение', style: TextStyle( fontSize: 16, color: colors.onSurfaceVariant, ), ), ], ), ) : ChatScreen( key: ValueKey(_selectedChat!.id), chatId: _selectedChat!.id, contact: _selectedContact!, myId: _myProfile?.id ?? 0, isGroupChat: _isGroupChat, isChannel: _isChannel, participantCount: _participantCount, isDesktopMode: true, onChatUpdated: () {}, ), ), ], ), ); } }