import 'dart:async'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../../core/config/debug_test.dart'; import '../../core/utils/update_checker.dart'; import '../screens/chats/chat_list_screen.dart'; import '../screens/chats/chat_screen.dart'; import 'update_dialog.dart'; class AdaptiveShell extends StatefulWidget { const AdaptiveShell({super.key}); @override State createState() => _AdaptiveShellState(); } class DesktopChatSelection { final int chatId; final String name; final String imageUrl; final String chatType; final String? initialMessageId; final int? initialMessageTime; const DesktopChatSelection({ required this.chatId, required this.name, required this.imageUrl, required this.chatType, this.initialMessageId, this.initialMessageTime, }); } class _AdaptiveShellState extends State { static const double _breakpoint = 900; static const double _defaultListWidth = 380; static const double _minListWidth = 280; static const double _maxListWidth = 560; static const double _minChatPaneWidth = 360; static const double _dividerHitWidth = 10; static const double _dividerLineWidth = 1; static const String _prefsKey = 'desktop_list_width'; double _listWidth = _defaultListWidth; DesktopChatSelection? _selected; @override void initState() { super.initState(); _loadListWidth(); WidgetsBinding.instance.addPostFrameCallback((_) => _maybeCheckUpdate()); } Future _maybeCheckUpdate() async { if (DebugTest.enabled) return; final update = await UpdateChecker.check(); if (update == null || !mounted) return; await showUpdateDialog(context, update); } Future _loadListWidth() async { final prefs = await SharedPreferences.getInstance(); final saved = prefs.getDouble(_prefsKey); if (saved == null || !mounted) return; setState(() { _listWidth = saved.clamp(_minListWidth, _maxListWidth); }); } Future _persistListWidth() async { final prefs = await SharedPreferences.getInstance(); await prefs.setDouble(_prefsKey, _listWidth); } void _onChatSelected(DesktopChatSelection chat) { if (chat.imageUrl.isNotEmpty) { unawaited( precacheImage( CachedNetworkImageProvider( chat.imageUrl, maxWidth: 144, maxHeight: 144, ), context, ), ); } setState(() => _selected = chat); } void _closeChat() { setState(() => _selected = null); } void _onDrag(double dx, double totalWidth) { final maxAllowedByPane = totalWidth - _minChatPaneWidth - _dividerHitWidth; final upperBound = maxAllowedByPane < _maxListWidth ? maxAllowedByPane : _maxListWidth; final lower = _minListWidth; final next = (_listWidth + dx).clamp(lower, upperBound); if (next == _listWidth) return; setState(() => _listWidth = next); } @override Widget build(BuildContext context) { return LayoutBuilder( builder: (context, constraints) { if (constraints.maxWidth < _breakpoint) { return const ChatListScreen(); } final totalWidth = constraints.maxWidth; final effectiveListWidth = _listWidth.clamp( _minListWidth, (totalWidth - _minChatPaneWidth - _dividerHitWidth).clamp( _minListWidth, _maxListWidth, ), ); final cs = Theme.of(context).colorScheme; return Scaffold( backgroundColor: cs.surface, body: Row( children: [ SizedBox( width: effectiveListWidth, child: ChatListScreen(onChatSelected: _onChatSelected), ), _ResizeDivider( hitWidth: _dividerHitWidth, lineWidth: _dividerLineWidth, color: cs.outlineVariant.withValues(alpha: 0.35), onDrag: (dx) => _onDrag(dx, totalWidth), onDragEnd: _persistListWidth, ), Expanded( child: _selected == null ? _EmptyChatPane(colorScheme: cs) : ChatScreen( key: ValueKey( '${_selected!.chatId}:${_selected!.initialMessageId ?? ''}', ), chatId: _selected!.chatId, name: _selected!.name, imageUrl: _selected!.imageUrl, chatType: _selected!.chatType, initialMessageId: _selected!.initialMessageId, initialMessageTime: _selected!.initialMessageTime, embedded: true, onClose: _closeChat, ), ), ], ), ); }, ); } } class _ResizeDivider extends StatefulWidget { final double hitWidth; final double lineWidth; final Color color; final ValueChanged onDrag; final Future Function() onDragEnd; const _ResizeDivider({ required this.hitWidth, required this.lineWidth, required this.color, required this.onDrag, required this.onDragEnd, }); @override State<_ResizeDivider> createState() => _ResizeDividerState(); } class _ResizeDividerState extends State<_ResizeDivider> { bool _hovering = false; bool _dragging = false; @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final highlight = _dragging || _hovering; return MouseRegion( cursor: SystemMouseCursors.resizeColumn, onEnter: (_) => setState(() => _hovering = true), onExit: (_) => setState(() => _hovering = false), child: GestureDetector( behavior: HitTestBehavior.opaque, onHorizontalDragStart: (_) => setState(() => _dragging = true), onHorizontalDragUpdate: (d) => widget.onDrag(d.delta.dx), onHorizontalDragEnd: (_) async { setState(() => _dragging = false); await widget.onDragEnd(); }, onHorizontalDragCancel: () => setState(() => _dragging = false), child: SizedBox( width: widget.hitWidth, child: Center( child: AnimatedContainer( duration: const Duration(milliseconds: 140), width: widget.lineWidth, color: highlight ? cs.primary.withValues(alpha: 0.6) : widget.color, ), ), ), ), ); } } class _EmptyChatPane extends StatelessWidget { final ColorScheme colorScheme; const _EmptyChatPane({required this.colorScheme}); @override Widget build(BuildContext context) { return ColoredBox( color: colorScheme.surfaceContainerLow, child: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon( Symbols.chat_bubble, size: 56, color: colorScheme.onSurfaceVariant.withValues(alpha: 0.5), weight: 300, ), const SizedBox(height: 14), Text( 'Выберите чат', style: TextStyle( color: colorScheme.onSurfaceVariant, fontSize: 15, fontWeight: FontWeight.w500, ), ), ], ), ), ); } }