From 9ca9f0c2d472e3f57c0853d78365052e14c000ab Mon Sep 17 00:00:00 2001 From: jganenok Date: Fri, 5 Dec 2025 18:36:45 +0700 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB=20?= =?UTF-8?q?=D0=BA=D0=B0=D1=81=D1=82=D0=BE=D0=BC=D0=B8=D0=B7=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D1=8E+,=20=D1=83=D0=B1=D1=80=D0=B0=D0=BB=20=D0=B6=D0=B5?= =?UTF-8?q?=D1=81=D1=82=D1=8B=20=D0=BF=D0=BE=D1=82=D0=BE=D0=BC=D1=83=20?= =?UTF-8?q?=D1=87=D1=82=D0=BE=20=D0=BE=D0=BD=D0=B8=20=D0=BC=D0=BD=D0=B5=20?= =?UTF-8?q?=D0=B6=D0=B8=D0=B7=D0=BD=D1=8C=20=D1=81=D0=BB=D0=BE=D0=BC=D0=B0?= =?UTF-8?q?=D0=BB=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/screens/chats_screen.dart | 361 +++++++++++----- lib/screens/profile_menu_dialog.dart | 27 +- .../settings/customization_screen.dart | 385 +++++++++++++++-- lib/services/avatar_cache_service.dart | 63 ++- lib/utils/theme_provider.dart | 388 ++++++++++++++++++ lib/widgets/chat_message_bubble.dart | 141 ++++--- lib/widgets/group_avatars.dart | 129 ++++-- 7 files changed, 1234 insertions(+), 260 deletions(-) diff --git a/lib/screens/chats_screen.dart b/lib/screens/chats_screen.dart index 751af6b..434e869 100644 --- a/lib/screens/chats_screen.dart +++ b/lib/screens/chats_screen.dart @@ -2000,10 +2000,40 @@ class _ChatsScreenState extends State if (widget.hasScaffold) { return Builder( builder: (context) { + final theme = context.watch(); + + BoxDecoration? chatsListDecoration; + if (theme.chatsListBackgroundType == ChatsListBackgroundType.gradient) { + chatsListDecoration = BoxDecoration( + gradient: LinearGradient( + colors: [ + theme.chatsListGradientColor1, + theme.chatsListGradientColor2, + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + ); + } else if (theme.chatsListBackgroundType == ChatsListBackgroundType.image && + theme.chatsListImagePath != null && + theme.chatsListImagePath!.isNotEmpty) { + chatsListDecoration = BoxDecoration( + image: DecorationImage( + image: FileImage(File(theme.chatsListImagePath!)), + fit: BoxFit.cover, + ), + ); + } + return Scaffold( appBar: _buildAppBar(context), drawer: _buildAppDrawer(context), - body: Row(children: [Expanded(child: bodyContent)]), + body: chatsListDecoration != null + ? Container( + decoration: chatsListDecoration, + child: Row(children: [Expanded(child: bodyContent)]), + ) + : Row(children: [Expanded(child: bodyContent)]), floatingActionButton: FloatingActionButton( onPressed: () { _showAddMenu(context); @@ -2048,7 +2078,30 @@ class _ChatsScreenState extends State right: 16.0, bottom: 16.0, ), - decoration: BoxDecoration(color: colors.primaryContainer), + decoration: () { + if (themeProvider.drawerBackgroundType == DrawerBackgroundType.gradient) { + return BoxDecoration( + gradient: LinearGradient( + colors: [ + themeProvider.drawerGradientColor1, + themeProvider.drawerGradientColor2, + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + ); + } else if (themeProvider.drawerBackgroundType == DrawerBackgroundType.image && + themeProvider.drawerImagePath != null && + themeProvider.drawerImagePath!.isNotEmpty) { + return BoxDecoration( + image: DecorationImage( + image: FileImage(File(themeProvider.drawerImagePath!)), + fit: BoxFit.cover, + ), + ); + } + return BoxDecoration(color: colors.primaryContainer); + }(), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -2273,18 +2326,32 @@ class _ChatsScreenState extends State ); }).toList(), - ListTile( - leading: const Icon(Icons.add_circle_outline), - title: const Text('Добавить аккаунт'), - onTap: () { - Navigator.pop(context); - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => - const PhoneEntryScreen(), - ), - ); - }, + Container( + decoration: themeProvider.useGradientForAddAccountButton + ? BoxDecoration( + gradient: LinearGradient( + colors: [ + themeProvider.addAccountButtonGradientColor1, + themeProvider.addAccountButtonGradientColor2, + ], + begin: Alignment.centerLeft, + end: Alignment.centerRight, + ), + ) + : null, + child: ListTile( + leading: const Icon(Icons.add_circle_outline), + title: const Text('Добавить аккаунт'), + onTap: () { + Navigator.pop(context); + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => + const PhoneEntryScreen(), + ), + ); + }, + ), ), ], ) @@ -2296,89 +2363,90 @@ class _ChatsScreenState extends State }, ), Expanded( - child: Column( - children: [ - _buildAccountsSection(context, colors), - ListTile( - leading: const Icon(Icons.person_outline), - title: const Text('Мой профиль'), - onTap: () { - Navigator.pop(context); // Закрыть Drawer - _navigateToProfileEdit(); // Этот метод у вас уже есть - }, - ), - ListTile( - leading: const Icon(Icons.call_outlined), - title: const Text('Звонки'), - onTap: () { - Navigator.pop(context); // Закрыть Drawer - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const CallsScreen(), - ), - ); - }, - ), - ListTile( - leading: const Icon(Icons.music_note), - title: const Text('Музыка'), - onTap: () { - Navigator.pop(context); - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const MusicLibraryScreen(), - ), - ); - }, - ), - ListTile( - leading: _isReconnecting - ? SizedBox( - width: 24, - height: 24, - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation( - colors.primary, + child: () { + final menuColumn = Column( + children: [ + _buildAccountsSection(context, colors), + ListTile( + leading: const Icon(Icons.person_outline), + title: const Text('Мой профиль'), + onTap: () { + Navigator.pop(context); + _navigateToProfileEdit(); + }, + ), + ListTile( + leading: const Icon(Icons.call_outlined), + title: const Text('Звонки'), + onTap: () { + Navigator.pop(context); + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => const CallsScreen(), + ), + ); + }, + ), + ListTile( + leading: const Icon(Icons.music_note), + title: const Text('Музыка'), + onTap: () { + Navigator.pop(context); + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => const MusicLibraryScreen(), + ), + ); + }, + ), + ListTile( + leading: _isReconnecting + ? SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation( + colors.primary, + ), ), - ), - ) - : const Icon(Icons.refresh), - title: const Text('Переподключиться'), - enabled: !_isReconnecting, - onTap: () async { - if (_isReconnecting) return; + ) + : const Icon(Icons.refresh), + title: const Text('Переподключиться'), + enabled: !_isReconnecting, + onTap: () async { + if (_isReconnecting) return; - setState(() { - _isReconnecting = true; - }); + setState(() { + _isReconnecting = true; + }); - try { - await ApiService.instance.performFullReconnection(); - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: const Text( - 'Переподключение выполнено успешно', + try { + await ApiService.instance.performFullReconnection(); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Text( + 'Переподключение выполнено успешно', + ), + backgroundColor: colors.primaryContainer, + duration: const Duration(seconds: 2), ), - backgroundColor: colors.primaryContainer, - duration: const Duration(seconds: 2), - ), - ); - Navigator.pop(context); - } - } catch (e) { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Ошибка переподключения: $e'), - backgroundColor: colors.error, - duration: const Duration(seconds: 3), - ), - ); - } - } finally { - if (mounted) { + ); + Navigator.pop(context); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Ошибка переподключения: $e'), + backgroundColor: colors.error, + duration: const Duration(seconds: 3), + ), + ); + } + } finally { + if (mounted) { setState(() { _isReconnecting = false; }); @@ -2428,21 +2496,49 @@ class _ChatsScreenState extends State } }, ), - const Spacer(), - const Divider(height: 1, indent: 16, endIndent: 16), ListTile( leading: Icon(Icons.logout, color: colors.error), title: Text('Выйти', style: TextStyle(color: colors.error)), onTap: () { - Navigator.pop(context); // Закрыть Drawer + Navigator.pop(context); _showLogoutDialog(); }, ), - const SizedBox(height: 8), // Небольшой отступ снизу + const SizedBox(height: 8), ], - ), + ); + + if (themeProvider.drawerBackgroundType == DrawerBackgroundType.gradient) { + return Container( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + themeProvider.drawerGradientColor2, + themeProvider.drawerGradientColor1, + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + ), + child: menuColumn, + ); + } else if (themeProvider.drawerBackgroundType == DrawerBackgroundType.image && + themeProvider.drawerImagePath != null && + themeProvider.drawerImagePath!.isNotEmpty) { + return Container( + decoration: BoxDecoration( + image: DecorationImage( + image: FileImage(File(themeProvider.drawerImagePath!)), + fit: BoxFit.cover, + ), + ), + child: menuColumn, + ); + } + return menuColumn; + }(), ), ], ), @@ -3035,14 +3131,46 @@ class _ChatsScreenState extends State ), ]; - return Container( - height: 48, - decoration: BoxDecoration( - color: colors.surface, + final themeProvider = context.watch(); + + BoxDecoration? folderTabsDecoration; + if (themeProvider.folderTabsBackgroundType == FolderTabsBackgroundType.gradient) { + folderTabsDecoration = BoxDecoration( + gradient: LinearGradient( + colors: [ + themeProvider.folderTabsGradientColor1, + themeProvider.folderTabsGradientColor2, + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), border: Border( bottom: BorderSide(color: colors.outline.withOpacity(0.2), width: 1), ), - ), + ); + } else if (themeProvider.folderTabsBackgroundType == FolderTabsBackgroundType.image && + themeProvider.folderTabsImagePath != null && + themeProvider.folderTabsImagePath!.isNotEmpty) { + folderTabsDecoration = BoxDecoration( + image: DecorationImage( + image: FileImage(File(themeProvider.folderTabsImagePath!)), + fit: BoxFit.cover, + ), + border: Border( + bottom: BorderSide(color: colors.outline.withOpacity(0.2), width: 1), + ), + ); + } + + return Container( + height: 48, + decoration: folderTabsDecoration ?? + BoxDecoration( + color: colors.surface, + border: Border( + bottom: BorderSide(color: colors.outline.withOpacity(0.2), width: 1), + ), + ), child: Stack( children: [ Row( @@ -3751,9 +3879,36 @@ class _ChatsScreenState extends State AppBar _buildAppBar(BuildContext context) { final colors = Theme.of(context).colorScheme; + final themeProvider = context.watch(); + + BoxDecoration? appBarDecoration; + if (themeProvider.appBarBackgroundType == AppBarBackgroundType.gradient) { + appBarDecoration = BoxDecoration( + gradient: LinearGradient( + colors: [ + themeProvider.appBarGradientColor1, + themeProvider.appBarGradientColor2, + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ); + } else if (themeProvider.appBarBackgroundType == AppBarBackgroundType.image && + themeProvider.appBarImagePath != null && + themeProvider.appBarImagePath!.isNotEmpty) { + appBarDecoration = BoxDecoration( + image: DecorationImage( + image: FileImage(File(themeProvider.appBarImagePath!)), + fit: BoxFit.cover, + ), + ); + } return AppBar( titleSpacing: 4.0, + flexibleSpace: appBarDecoration != null + ? Container(decoration: appBarDecoration) + : null, leading: _isSearchExpanded ? IconButton( diff --git a/lib/screens/profile_menu_dialog.dart b/lib/screens/profile_menu_dialog.dart index a71031c..9c64453 100644 --- a/lib/screens/profile_menu_dialog.dart +++ b/lib/screens/profile_menu_dialog.dart @@ -6,6 +6,7 @@ import 'package:gwid/screens/settings/settings_screen.dart'; import 'package:gwid/screens/phone_entry_screen.dart'; import 'package:provider/provider.dart'; import 'package:gwid/utils/theme_provider.dart'; +import 'package:gwid/api/api_service.dart'; class ProfileMenuDialog extends StatefulWidget { final Profile? myProfile; @@ -195,12 +196,26 @@ class _ProfileMenuDialogState extends State { onTap: () async { if (context.mounted) { Navigator.of(context).pop(); - Navigator.of(context).pushAndRemoveUntil( - MaterialPageRoute( - builder: (_) => const PhoneEntryScreen(), - ), - (route) => false, - ); + try { + await ApiService.instance.logout(); + if (context.mounted) { + Navigator.of(context).pushAndRemoveUntil( + MaterialPageRoute( + builder: (_) => const PhoneEntryScreen(), + ), + (route) => false, + ); + } + } catch (e) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Ошибка при выходе: $e'), + backgroundColor: Theme.of(context).colorScheme.error, + ), + ); + } + } } }, ), diff --git a/lib/screens/settings/customization_screen.dart b/lib/screens/settings/customization_screen.dart index e0f9d76..f059636 100644 --- a/lib/screens/settings/customization_screen.dart +++ b/lib/screens/settings/customization_screen.dart @@ -242,7 +242,6 @@ class _CustomizationScreenState extends State { title: const Text("Выбрать видео"), trailing: const Icon(Icons.chevron_right), onTap: () async { - final result = await FilePicker.platform.pickFiles( type: FileType.video, ); @@ -278,7 +277,7 @@ class _CustomizationScreenState extends State { // Предпросмотр баблов const _MessageBubblesPreview(), const SizedBox(height: 16), - + // Прозрачность (сворачиваемый, по умолчанию свернут) _ExpandableSection( title: "Прозрачность", @@ -292,7 +291,8 @@ class _CustomizationScreenState extends State { max: 1.0, divisions: 18, onChanged: (value) => theme.setMessageTextOpacity(value), - displayValue: "${(theme.messageTextOpacity * 100).round()}%", + displayValue: + "${(theme.messageTextOpacity * 100).round()}%", ), _SliderTile( icon: Icons.blur_circular, @@ -301,7 +301,8 @@ class _CustomizationScreenState extends State { min: 0.0, max: 0.5, divisions: 10, - onChanged: (value) => theme.setMessageShadowIntensity(value), + onChanged: (value) => + theme.setMessageShadowIntensity(value), displayValue: "${(theme.messageShadowIntensity * 100).round()}%", ), @@ -313,7 +314,8 @@ class _CustomizationScreenState extends State { max: 1.0, divisions: 18, onChanged: (value) => theme.setMessageMenuOpacity(value), - displayValue: "${(theme.messageMenuOpacity * 100).round()}%", + displayValue: + "${(theme.messageMenuOpacity * 100).round()}%", ), _SliderTile( icon: Icons.blur_on, @@ -340,7 +342,7 @@ class _CustomizationScreenState extends State { ], ), const SizedBox(height: 8), - + // Вид (сворачиваемый) _ExpandableSection( title: "Вид", @@ -368,7 +370,8 @@ class _CustomizationScreenState extends State { value: theme.messageBubbleType, underline: const SizedBox.shrink(), onChanged: (value) { - if (value != null) theme.setMessageBubbleType(value); + if (value != null) + theme.setMessageBubbleType(value); }, items: MessageBubbleType.values.map((type) { return DropdownMenuItem( @@ -390,7 +393,8 @@ class _CustomizationScreenState extends State { opacity: isSystemTheme ? 0.5 : 1.0, child: GestureDetector( onTap: () async { - final initial = myBubbleColorToShow ?? myBubbleFallback; + final initial = + myBubbleColorToShow ?? myBubbleFallback; _showColorPicker( context, initialColor: initial, @@ -425,14 +429,16 @@ class _CustomizationScreenState extends State { _showColorPicker( context, initialColor: initial, - onColorChanged: (color) => theirBubbleSetter(color), + onColorChanged: (color) => + theirBubbleSetter(color), ); }, child: Container( width: 40, height: 40, decoration: BoxDecoration( - color: theirBubbleColorToShow ?? theirBubbleFallback, + color: + theirBubbleColorToShow ?? theirBubbleFallback, borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.grey), ), @@ -457,7 +463,8 @@ class _CustomizationScreenState extends State { title: "Цвет панели ответа", subtitle: "Фиксированный цвет", color: theme.customReplyColor ?? Colors.blue, - onColorChanged: (color) => theme.setCustomReplyColor(color), + onColorChanged: (color) => + theme.setCustomReplyColor(color), ), ], ], @@ -471,7 +478,7 @@ class _CustomizationScreenState extends State { // Предпросмотр всплывающего окна _DialogPreview(), const SizedBox(height: 16), - + // Развернуть настройки _ExpandableSection( title: "Настройки", @@ -485,7 +492,8 @@ class _CustomizationScreenState extends State { max: 1.0, divisions: 20, onChanged: (value) => theme.setProfileDialogOpacity(value), - displayValue: "${(theme.profileDialogOpacity * 100).round()}%", + displayValue: + "${(theme.profileDialogOpacity * 100).round()}%", ), _SliderTile( icon: Icons.blur_on, @@ -517,13 +525,330 @@ class _CustomizationScreenState extends State { ], ), const SizedBox(height: 24), + _ExpandableSection( + title: "Кастомизация+", + initiallyExpanded: false, + children: [ + _CustomSettingTile( + icon: Icons.format_color_fill, + title: "Фон списка чатов", + subtitle: "Выберите тип фона для списка чатов", + child: DropdownButton( + value: theme.chatsListBackgroundType, + underline: const SizedBox.shrink(), + onChanged: (value) { + if (value != null) theme.setChatsListBackgroundType(value); + }, + items: ChatsListBackgroundType.values.map((type) { + return DropdownMenuItem( + value: type, + child: Text(type.displayName), + ); + }).toList(), + ), + ), + if (theme.chatsListBackgroundType == + ChatsListBackgroundType.gradient) ...[ + const SizedBox(height: 16), + _ColorPickerTile( + title: "Цвет 1", + subtitle: "Начальный цвет градиента", + color: theme.chatsListGradientColor1, + onColorChanged: (color) => + theme.setChatsListGradientColor1(color), + ), + const SizedBox(height: 16), + _ColorPickerTile( + title: "Цвет 2", + subtitle: "Конечный цвет градиента", + color: theme.chatsListGradientColor2, + onColorChanged: (color) => + theme.setChatsListGradientColor2(color), + ), + ], + if (theme.chatsListBackgroundType == + ChatsListBackgroundType.image) ...[ + const Divider(height: 24), + ListTile( + contentPadding: EdgeInsets.zero, + leading: const Icon(Icons.photo_library_outlined), + title: const Text("Выбрать изображение"), + trailing: const Icon(Icons.chevron_right), + onTap: () async { + final picker = ImagePicker(); + final image = await picker.pickImage( + source: ImageSource.gallery, + ); + if (image != null) { + theme.setChatsListImagePath(image.path); + } + }, + ), + if (theme.chatsListImagePath?.isNotEmpty == true) ...[ + ListTile( + contentPadding: EdgeInsets.zero, + leading: const Icon( + Icons.delete_outline, + color: Colors.redAccent, + ), + title: const Text( + "Удалить изображение", + style: TextStyle(color: Colors.redAccent), + ), + onTap: () => theme.setChatsListImagePath(null), + ), + ], + ], + const Divider(height: 24), + _CustomSettingTile( + icon: Icons.view_sidebar, + title: "Фон боковой панели", + subtitle: "Выберите тип фона для боковой панели", + child: DropdownButton( + value: theme.drawerBackgroundType, + underline: const SizedBox.shrink(), + onChanged: (value) { + if (value != null) theme.setDrawerBackgroundType(value); + }, + items: DrawerBackgroundType.values.map((type) { + return DropdownMenuItem( + value: type, + child: Text(type.displayName), + ); + }).toList(), + ), + ), + if (theme.drawerBackgroundType == + DrawerBackgroundType.gradient) ...[ + const SizedBox(height: 16), + _ColorPickerTile( + title: "Цвет 1", + subtitle: "Начальный цвет градиента", + color: theme.drawerGradientColor1, + onColorChanged: (color) => + theme.setDrawerGradientColor1(color), + ), + const SizedBox(height: 16), + _ColorPickerTile( + title: "Цвет 2", + subtitle: "Конечный цвет градиента", + color: theme.drawerGradientColor2, + onColorChanged: (color) => + theme.setDrawerGradientColor2(color), + ), + ], + if (theme.drawerBackgroundType == DrawerBackgroundType.image) ...[ + const Divider(height: 24), + ListTile( + contentPadding: EdgeInsets.zero, + leading: const Icon(Icons.photo_library_outlined), + title: const Text("Выбрать изображение"), + trailing: const Icon(Icons.chevron_right), + onTap: () async { + final picker = ImagePicker(); + final image = await picker.pickImage( + source: ImageSource.gallery, + ); + if (image != null) { + theme.setDrawerImagePath(image.path); + } + }, + ), + if (theme.drawerImagePath?.isNotEmpty == true) ...[ + ListTile( + contentPadding: EdgeInsets.zero, + leading: const Icon( + Icons.delete_outline, + color: Colors.redAccent, + ), + title: const Text( + "Удалить изображение", + style: TextStyle(color: Colors.redAccent), + ), + onTap: () => theme.setDrawerImagePath(null), + ), + ], + ], + const Divider(height: 24), + _CustomSettingTile( + icon: Icons.person_add, + title: "Градиент для кнопки добавления аккаунта", + subtitle: "Применить градиент к кнопке в drawer", + child: Switch( + value: theme.useGradientForAddAccountButton, + onChanged: (value) => + theme.setUseGradientForAddAccountButton(value), + ), + ), + if (theme.useGradientForAddAccountButton) ...[ + const SizedBox(height: 16), + _ColorPickerTile( + title: "Цвет 1", + subtitle: "Начальный цвет градиента", + color: theme.addAccountButtonGradientColor1, + onColorChanged: (color) => + theme.setAddAccountButtonGradientColor1(color), + ), + const SizedBox(height: 16), + _ColorPickerTile( + title: "Цвет 2", + subtitle: "Конечный цвет градиента", + color: theme.addAccountButtonGradientColor2, + onColorChanged: (color) => + theme.setAddAccountButtonGradientColor2(color), + ), + ], + const Divider(height: 24), + _CustomSettingTile( + icon: Icons.view_headline, + title: "Фон верхней панели", + subtitle: "Выберите тип фона для AppBar (поиск, Сферум и т.д.)", + child: DropdownButton( + value: theme.appBarBackgroundType, + underline: const SizedBox.shrink(), + onChanged: (value) { + if (value != null) theme.setAppBarBackgroundType(value); + }, + items: AppBarBackgroundType.values.map((type) { + return DropdownMenuItem( + value: type, + child: Text(type.displayName), + ); + }).toList(), + ), + ), + if (theme.appBarBackgroundType == + AppBarBackgroundType.gradient) ...[ + const SizedBox(height: 16), + _ColorPickerTile( + title: "Цвет 1", + subtitle: "Начальный цвет градиента", + color: theme.appBarGradientColor1, + onColorChanged: (color) => + theme.setAppBarGradientColor1(color), + ), + const SizedBox(height: 16), + _ColorPickerTile( + title: "Цвет 2", + subtitle: "Конечный цвет градиента", + color: theme.appBarGradientColor2, + onColorChanged: (color) => + theme.setAppBarGradientColor2(color), + ), + ], + if (theme.appBarBackgroundType == AppBarBackgroundType.image) ...[ + const Divider(height: 24), + ListTile( + contentPadding: EdgeInsets.zero, + leading: const Icon(Icons.photo_library_outlined), + title: const Text("Выбрать изображение"), + trailing: const Icon(Icons.chevron_right), + onTap: () async { + final picker = ImagePicker(); + final image = await picker.pickImage( + source: ImageSource.gallery, + ); + if (image != null) { + theme.setAppBarImagePath(image.path); + } + }, + ), + if (theme.appBarImagePath?.isNotEmpty == true) ...[ + ListTile( + contentPadding: EdgeInsets.zero, + leading: const Icon( + Icons.delete_outline, + color: Colors.redAccent, + ), + title: const Text( + "Удалить изображение", + style: TextStyle(color: Colors.redAccent), + ), + onTap: () => theme.setAppBarImagePath(null), + ), + ], + ], + const Divider(height: 24), + _CustomSettingTile( + icon: Icons.folder, + title: "Фон панели папок", + subtitle: "Выберите тип фона для панели с именами папок", + child: DropdownButton( + value: theme.folderTabsBackgroundType, + underline: const SizedBox.shrink(), + onChanged: (value) { + if (value != null) theme.setFolderTabsBackgroundType(value); + }, + items: FolderTabsBackgroundType.values.map((type) { + return DropdownMenuItem( + value: type, + child: Text(type.displayName), + ); + }).toList(), + ), + ), + if (theme.folderTabsBackgroundType == + FolderTabsBackgroundType.gradient) ...[ + const SizedBox(height: 16), + _ColorPickerTile( + title: "Цвет 1", + subtitle: "Начальный цвет градиента", + color: theme.folderTabsGradientColor1, + onColorChanged: (color) => + theme.setFolderTabsGradientColor1(color), + ), + const SizedBox(height: 16), + _ColorPickerTile( + title: "Цвет 2", + subtitle: "Конечный цвет градиента", + color: theme.folderTabsGradientColor2, + onColorChanged: (color) => + theme.setFolderTabsGradientColor2(color), + ), + ], + if (theme.folderTabsBackgroundType == + FolderTabsBackgroundType.image) ...[ + const Divider(height: 24), + ListTile( + contentPadding: EdgeInsets.zero, + leading: const Icon(Icons.photo_library_outlined), + title: const Text("Выбрать изображение"), + trailing: const Icon(Icons.chevron_right), + onTap: () async { + final picker = ImagePicker(); + final image = await picker.pickImage( + source: ImageSource.gallery, + ); + if (image != null) { + theme.setFolderTabsImagePath(image.path); + } + }, + ), + if (theme.folderTabsImagePath?.isNotEmpty == true) ...[ + ListTile( + contentPadding: EdgeInsets.zero, + leading: const Icon( + Icons.delete_outline, + color: Colors.redAccent, + ), + title: const Text( + "Удалить изображение", + style: TextStyle(color: Colors.redAccent), + ), + onTap: () => theme.setFolderTabsImagePath(null), + ), + ], + ], + ], + ), + const SizedBox(height: 24), _ModernSection( title: "Панели чата", children: [ // Предпросмотр панелей _PanelsPreview(), const SizedBox(height: 16), - + // Галочка включения эффекта стекла _CustomSettingTile( icon: Icons.tune, @@ -535,7 +860,7 @@ class _CustomizationScreenState extends State { ), ), const SizedBox(height: 8), - + // Развернуть настройки _ExpandableSection( title: "Настройки", @@ -1388,7 +1713,6 @@ class _ChatWallpaperPreview extends StatelessWidget { ); } case ChatWallpaperType.video: - if (Platform.isWindows) { return Container( color: isDarkTheme ? Colors.grey[850] : Colors.grey[200], @@ -1572,7 +1896,10 @@ class _ExpandableSectionState extends State<_ExpandableSection> { onTap: () => setState(() => _isExpanded = !_isExpanded), borderRadius: BorderRadius.circular(8), child: Padding( - padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 4.0), + padding: const EdgeInsets.symmetric( + vertical: 12.0, + horizontal: 4.0, + ), child: Row( children: [ Text( @@ -1591,10 +1918,7 @@ class _ExpandableSectionState extends State<_ExpandableSection> { ), ), ), - if (_isExpanded) ...[ - const SizedBox(height: 8), - ...widget.children, - ], + if (_isExpanded) ...[const SizedBox(height: 8), ...widget.children], ], ); } @@ -1646,10 +1970,7 @@ class _MessageBubblesPreview extends StatelessWidget { children: [ const Spacer(), Expanded( - child: ChatMessageBubble( - message: mockMyMessage, - isMe: true, - ), + child: ChatMessageBubble(message: mockMyMessage, isMe: true), ), ], ), @@ -1708,16 +2029,10 @@ class _DialogPreview extends StatelessWidget { decoration: BoxDecoration( color: colors.surface.withOpacity(theme.profileDialogOpacity), borderRadius: BorderRadius.circular(12), - border: Border.all( - color: colors.outline.withOpacity(0.2), - ), + border: Border.all(color: colors.outline.withOpacity(0.2)), ), child: Center( - child: Icon( - Icons.person, - color: colors.onSurface, - size: 32, - ), + child: Icon(Icons.person, color: colors.onSurface, size: 32), ), ), ), @@ -1831,7 +2146,9 @@ class _PanelsPreview extends StatelessWidget { ), child: Container( height: 30, - color: colors.surface.withOpacity(theme.bottomBarOpacity), + color: colors.surface.withOpacity( + theme.bottomBarOpacity, + ), child: Row( children: [ const SizedBox(width: 12), diff --git a/lib/services/avatar_cache_service.dart b/lib/services/avatar_cache_service.dart index 9ea5cf1..d86ed99 100644 --- a/lib/services/avatar_cache_service.dart +++ b/lib/services/avatar_cache_service.dart @@ -40,7 +40,12 @@ class AvatarCacheService { final timestamp = _imageCacheTimestamps[cacheKey]; if (timestamp != null && !_isExpired(timestamp, _imageTTL)) { final imageData = _imageMemoryCache[cacheKey]!; - return MemoryImage(imageData); + if (_isValidImageData(imageData)) { + return MemoryImage(imageData); + } else { + _imageMemoryCache.remove(cacheKey); + _imageCacheTimestamps.remove(cacheKey); + } } else { _imageMemoryCache.remove(cacheKey); _imageCacheTimestamps.remove(cacheKey); @@ -52,20 +57,25 @@ class AvatarCacheService { customKey: cacheKey, ); if (cachedFile != null && await cachedFile.exists()) { - final imageData = await cachedFile.readAsBytes(); + try { + final imageData = await cachedFile.readAsBytes(); + if (_isValidImageData(imageData)) { + _imageMemoryCache[cacheKey] = imageData; + _imageCacheTimestamps[cacheKey] = DateTime.now(); - _imageMemoryCache[cacheKey] = imageData; - _imageCacheTimestamps[cacheKey] = DateTime.now(); + if (_imageMemoryCache.length > _maxMemoryImages) { + await _evictOldestImages(); + } - if (_imageMemoryCache.length > _maxMemoryImages) { - await _evictOldestImages(); + return MemoryImage(imageData); + } + } catch (e) { + print('Ошибка чтения кешированного файла аватарки: $e'); } - - return MemoryImage(imageData); } final imageData = await _downloadImage(avatarUrl); - if (imageData != null) { + if (imageData != null && _isValidImageData(imageData)) { await _cacheService.cacheFile(avatarUrl, customKey: cacheKey); _imageMemoryCache[cacheKey] = imageData; @@ -73,6 +83,8 @@ class AvatarCacheService { return MemoryImage(imageData); } + + return NetworkImage(avatarUrl); } catch (e) { print('Ошибка получения аватарки: $e'); } @@ -111,6 +123,11 @@ class AvatarCacheService { return null; } + if (!_isValidImageData(imageData)) { + print('Невалидные данные изображения для $url'); + return null; + } + return imageData; } } catch (e) { @@ -119,6 +136,34 @@ class AvatarCacheService { return null; } + bool _isValidImageData(Uint8List data) { + if (data.isEmpty) return false; + if (data.length < 4) return false; + + final header = data.sublist(0, 4); + final pngHeader = [0x89, 0x50, 0x4E, 0x47]; + final jpegHeader = [0xFF, 0xD8, 0xFF]; + final gifHeader = [0x47, 0x49, 0x46, 0x38]; + final webpHeader = [0x52, 0x49, 0x46, 0x46]; + + bool isValid = false; + if (header[0] == pngHeader[0] && header[1] == pngHeader[1] && + header[2] == pngHeader[2] && header[3] == pngHeader[3]) { + isValid = true; + } else if (header[0] == jpegHeader[0] && header[1] == jpegHeader[1] && + header[2] == jpegHeader[2]) { + isValid = true; + } else if (header[0] == gifHeader[0] && header[1] == gifHeader[1] && + header[2] == gifHeader[2] && header[3] == gifHeader[3]) { + isValid = true; + } else if (header[0] == webpHeader[0] && header[1] == webpHeader[1] && + header[2] == webpHeader[2] && header[3] == webpHeader[3]) { + isValid = true; + } + + return isValid; + } + String _generateCacheKey(String url, int? userId) { if (userId != null) { return 'avatar_${userId}_${_hashUrl(url)}'; diff --git a/lib/utils/theme_provider.dart b/lib/utils/theme_provider.dart index 8923963..d00a5c5 100644 --- a/lib/utils/theme_provider.dart +++ b/lib/utils/theme_provider.dart @@ -6,6 +6,14 @@ enum AppTheme { system, light, dark, black } enum ChatWallpaperType { solid, gradient, image, video } +enum FolderTabsBackgroundType { none, gradient, image } + +enum DrawerBackgroundType { none, gradient, image } + +enum ChatsListBackgroundType { none, gradient, image } + +enum AppBarBackgroundType { none, gradient, image } + enum TransitionOption { systemDefault, slide } enum UIMode { both, burgerOnly, panelOnly } @@ -47,6 +55,58 @@ extension ChatWallpaperTypeExtension on ChatWallpaperType { } } +extension FolderTabsBackgroundTypeExtension on FolderTabsBackgroundType { + String get displayName { + switch (this) { + case FolderTabsBackgroundType.none: + return 'Нет'; + case FolderTabsBackgroundType.gradient: + return 'Градиент'; + case FolderTabsBackgroundType.image: + return 'Фото'; + } + } +} + +extension DrawerBackgroundTypeExtension on DrawerBackgroundType { + String get displayName { + switch (this) { + case DrawerBackgroundType.none: + return 'Нет'; + case DrawerBackgroundType.gradient: + return 'Градиент'; + case DrawerBackgroundType.image: + return 'Фото'; + } + } +} + +extension ChatsListBackgroundTypeExtension on ChatsListBackgroundType { + String get displayName { + switch (this) { + case ChatsListBackgroundType.none: + return 'Нет'; + case ChatsListBackgroundType.gradient: + return 'Градиент'; + case ChatsListBackgroundType.image: + return 'Фото'; + } + } +} + +extension AppBarBackgroundTypeExtension on AppBarBackgroundType { + String get displayName { + switch (this) { + case AppBarBackgroundType.none: + return 'Нет'; + case AppBarBackgroundType.gradient: + return 'Градиент'; + case AppBarBackgroundType.image: + return 'Фото'; + } + } +} + class CustomThemePreset { String id; String name; @@ -105,6 +165,30 @@ class CustomThemePreset { bool useAutoReplyColor; Color? customReplyColor; + bool useGradientForChatsList; + ChatsListBackgroundType chatsListBackgroundType; + String? chatsListImagePath; + bool useGradientForDrawer; + DrawerBackgroundType drawerBackgroundType; + String? drawerImagePath; + bool useGradientForAddAccountButton; + bool useGradientForAppBar; + AppBarBackgroundType appBarBackgroundType; + String? appBarImagePath; + bool useGradientForFolderTabs; + FolderTabsBackgroundType folderTabsBackgroundType; + String? folderTabsImagePath; + Color chatsListGradientColor1; + Color chatsListGradientColor2; + Color drawerGradientColor1; + Color drawerGradientColor2; + Color addAccountButtonGradientColor1; + Color addAccountButtonGradientColor2; + Color appBarGradientColor1; + Color appBarGradientColor2; + Color folderTabsGradientColor1; + Color folderTabsGradientColor2; + CustomThemePreset({ required this.id, required this.name, @@ -155,6 +239,29 @@ class CustomThemePreset { this.useDesktopLayout = true, this.useAutoReplyColor = true, this.customReplyColor, + this.useGradientForChatsList = false, + this.chatsListBackgroundType = ChatsListBackgroundType.none, + this.chatsListImagePath, + this.useGradientForDrawer = false, + this.drawerBackgroundType = DrawerBackgroundType.none, + this.drawerImagePath, + this.useGradientForAddAccountButton = false, + this.useGradientForAppBar = false, + this.appBarBackgroundType = AppBarBackgroundType.none, + this.appBarImagePath, + this.useGradientForFolderTabs = false, + this.folderTabsBackgroundType = FolderTabsBackgroundType.none, + this.folderTabsImagePath, + this.chatsListGradientColor1 = const Color(0xFF1E1E1E), + this.chatsListGradientColor2 = const Color(0xFF2D2D2D), + this.drawerGradientColor1 = const Color(0xFF1E1E1E), + this.drawerGradientColor2 = const Color(0xFF2D2D2D), + this.addAccountButtonGradientColor1 = const Color(0xFF1E1E1E), + this.addAccountButtonGradientColor2 = const Color(0xFF2D2D2D), + this.appBarGradientColor1 = const Color(0xFF1E1E1E), + this.appBarGradientColor2 = const Color(0xFF2D2D2D), + this.folderTabsGradientColor1 = const Color(0xFF1E1E1E), + this.folderTabsGradientColor2 = const Color(0xFF2D2D2D), }); factory CustomThemePreset.createDefault() { @@ -211,6 +318,29 @@ class CustomThemePreset { bool? useDesktopLayout, bool? useAutoReplyColor, Color? customReplyColor, + bool? useGradientForChatsList, + ChatsListBackgroundType? chatsListBackgroundType, + String? chatsListImagePath, + bool? useGradientForDrawer, + DrawerBackgroundType? drawerBackgroundType, + String? drawerImagePath, + bool? useGradientForAddAccountButton, + bool? useGradientForAppBar, + AppBarBackgroundType? appBarBackgroundType, + String? appBarImagePath, + bool? useGradientForFolderTabs, + FolderTabsBackgroundType? folderTabsBackgroundType, + String? folderTabsImagePath, + Color? chatsListGradientColor1, + Color? chatsListGradientColor2, + Color? drawerGradientColor1, + Color? drawerGradientColor2, + Color? addAccountButtonGradientColor1, + Color? addAccountButtonGradientColor2, + Color? appBarGradientColor1, + Color? appBarGradientColor2, + Color? folderTabsGradientColor1, + Color? folderTabsGradientColor2, }) { return CustomThemePreset( id: id ?? this.id, @@ -271,6 +401,29 @@ class CustomThemePreset { useDesktopLayout: useDesktopLayout ?? this.useDesktopLayout, useAutoReplyColor: useAutoReplyColor ?? this.useAutoReplyColor, customReplyColor: customReplyColor ?? this.customReplyColor, + useGradientForChatsList: useGradientForChatsList ?? this.useGradientForChatsList, + chatsListBackgroundType: chatsListBackgroundType ?? this.chatsListBackgroundType, + chatsListImagePath: chatsListImagePath ?? this.chatsListImagePath, + useGradientForDrawer: useGradientForDrawer ?? this.useGradientForDrawer, + drawerBackgroundType: drawerBackgroundType ?? this.drawerBackgroundType, + drawerImagePath: drawerImagePath ?? this.drawerImagePath, + useGradientForAddAccountButton: useGradientForAddAccountButton ?? this.useGradientForAddAccountButton, + useGradientForAppBar: useGradientForAppBar ?? this.useGradientForAppBar, + appBarBackgroundType: appBarBackgroundType ?? this.appBarBackgroundType, + appBarImagePath: appBarImagePath ?? this.appBarImagePath, + useGradientForFolderTabs: useGradientForFolderTabs ?? this.useGradientForFolderTabs, + folderTabsBackgroundType: folderTabsBackgroundType ?? this.folderTabsBackgroundType, + folderTabsImagePath: folderTabsImagePath ?? this.folderTabsImagePath, + chatsListGradientColor1: chatsListGradientColor1 ?? this.chatsListGradientColor1, + chatsListGradientColor2: chatsListGradientColor2 ?? this.chatsListGradientColor2, + drawerGradientColor1: drawerGradientColor1 ?? this.drawerGradientColor1, + drawerGradientColor2: drawerGradientColor2 ?? this.drawerGradientColor2, + addAccountButtonGradientColor1: addAccountButtonGradientColor1 ?? this.addAccountButtonGradientColor1, + addAccountButtonGradientColor2: addAccountButtonGradientColor2 ?? this.addAccountButtonGradientColor2, + appBarGradientColor1: appBarGradientColor1 ?? this.appBarGradientColor1, + appBarGradientColor2: appBarGradientColor2 ?? this.appBarGradientColor2, + folderTabsGradientColor1: folderTabsGradientColor1 ?? this.folderTabsGradientColor1, + folderTabsGradientColor2: folderTabsGradientColor2 ?? this.folderTabsGradientColor2, ); } @@ -325,6 +478,29 @@ class CustomThemePreset { 'useDesktopLayout': useDesktopLayout, 'useAutoReplyColor': useAutoReplyColor, 'customReplyColor': customReplyColor?.value, + 'useGradientForChatsList': useGradientForChatsList, + 'chatsListBackgroundType': chatsListBackgroundType.index, + 'chatsListImagePath': chatsListImagePath, + 'useGradientForDrawer': useGradientForDrawer, + 'drawerBackgroundType': drawerBackgroundType.index, + 'drawerImagePath': drawerImagePath, + 'useGradientForAddAccountButton': useGradientForAddAccountButton, + 'useGradientForAppBar': useGradientForAppBar, + 'appBarBackgroundType': appBarBackgroundType.index, + 'appBarImagePath': appBarImagePath, + 'useGradientForFolderTabs': useGradientForFolderTabs, + 'folderTabsBackgroundType': folderTabsBackgroundType.index, + 'folderTabsImagePath': folderTabsImagePath, + 'chatsListGradientColor1': chatsListGradientColor1.value, + 'chatsListGradientColor2': chatsListGradientColor2.value, + 'drawerGradientColor1': drawerGradientColor1.value, + 'drawerGradientColor2': drawerGradientColor2.value, + 'addAccountButtonGradientColor1': addAccountButtonGradientColor1.value, + 'addAccountButtonGradientColor2': addAccountButtonGradientColor2.value, + 'appBarGradientColor1': appBarGradientColor1.value, + 'appBarGradientColor2': appBarGradientColor2.value, + 'folderTabsGradientColor1': folderTabsGradientColor1.value, + 'folderTabsGradientColor2': folderTabsGradientColor2.value, }; } @@ -422,6 +598,57 @@ class CustomThemePreset { customReplyColor: json['customReplyColor'] != null ? Color(json['customReplyColor'] as int) : null, + useGradientForChatsList: json['useGradientForChatsList'] as bool? ?? false, + chatsListBackgroundType: ChatsListBackgroundType.values[ + json['chatsListBackgroundType'] as int? ?? 0 + ], + chatsListImagePath: json['chatsListImagePath'] as String?, + useGradientForDrawer: json['useGradientForDrawer'] as bool? ?? false, + drawerBackgroundType: DrawerBackgroundType.values[ + json['drawerBackgroundType'] as int? ?? 0 + ], + drawerImagePath: json['drawerImagePath'] as String?, + useGradientForAddAccountButton: json['useGradientForAddAccountButton'] as bool? ?? false, + useGradientForAppBar: json['useGradientForAppBar'] as bool? ?? false, + appBarBackgroundType: AppBarBackgroundType.values[ + json['appBarBackgroundType'] as int? ?? 0 + ], + appBarImagePath: json['appBarImagePath'] as String?, + useGradientForFolderTabs: json['useGradientForFolderTabs'] as bool? ?? false, + folderTabsBackgroundType: FolderTabsBackgroundType.values[ + json['folderTabsBackgroundType'] as int? ?? 0 + ], + folderTabsImagePath: json['folderTabsImagePath'] as String?, + chatsListGradientColor1: Color( + json['chatsListGradientColor1'] as int? ?? const Color(0xFF1E1E1E).value, + ), + chatsListGradientColor2: Color( + json['chatsListGradientColor2'] as int? ?? const Color(0xFF2D2D2D).value, + ), + drawerGradientColor1: Color( + json['drawerGradientColor1'] as int? ?? const Color(0xFF1E1E1E).value, + ), + drawerGradientColor2: Color( + json['drawerGradientColor2'] as int? ?? const Color(0xFF2D2D2D).value, + ), + addAccountButtonGradientColor1: Color( + json['addAccountButtonGradientColor1'] as int? ?? const Color(0xFF1E1E1E).value, + ), + addAccountButtonGradientColor2: Color( + json['addAccountButtonGradientColor2'] as int? ?? const Color(0xFF2D2D2D).value, + ), + appBarGradientColor1: Color( + json['appBarGradientColor1'] as int? ?? const Color(0xFF1E1E1E).value, + ), + appBarGradientColor2: Color( + json['appBarGradientColor2'] as int? ?? const Color(0xFF2D2D2D).value, + ), + folderTabsGradientColor1: Color( + json['folderTabsGradientColor1'] as int? ?? const Color(0xFF1E1E1E).value, + ), + folderTabsGradientColor2: Color( + json['folderTabsGradientColor2'] as int? ?? const Color(0xFF2D2D2D).value, + ), ); } } @@ -552,6 +779,29 @@ class ThemeProvider with ChangeNotifier { bool get useDesktopLayout => _activeTheme.useDesktopLayout; bool get useAutoReplyColor => _activeTheme.useAutoReplyColor; Color? get customReplyColor => _activeTheme.customReplyColor; + bool get useGradientForChatsList => _activeTheme.useGradientForChatsList; + ChatsListBackgroundType get chatsListBackgroundType => _activeTheme.chatsListBackgroundType; + String? get chatsListImagePath => _activeTheme.chatsListImagePath; + bool get useGradientForDrawer => _activeTheme.useGradientForDrawer; + DrawerBackgroundType get drawerBackgroundType => _activeTheme.drawerBackgroundType; + String? get drawerImagePath => _activeTheme.drawerImagePath; + bool get useGradientForAddAccountButton => _activeTheme.useGradientForAddAccountButton; + bool get useGradientForAppBar => _activeTheme.useGradientForAppBar; + AppBarBackgroundType get appBarBackgroundType => _activeTheme.appBarBackgroundType; + String? get appBarImagePath => _activeTheme.appBarImagePath; + bool get useGradientForFolderTabs => _activeTheme.useGradientForFolderTabs; + FolderTabsBackgroundType get folderTabsBackgroundType => _activeTheme.folderTabsBackgroundType; + String? get folderTabsImagePath => _activeTheme.folderTabsImagePath; + Color get chatsListGradientColor1 => _activeTheme.chatsListGradientColor1; + Color get chatsListGradientColor2 => _activeTheme.chatsListGradientColor2; + Color get drawerGradientColor1 => _activeTheme.drawerGradientColor1; + Color get drawerGradientColor2 => _activeTheme.drawerGradientColor2; + Color get addAccountButtonGradientColor1 => _activeTheme.addAccountButtonGradientColor1; + Color get addAccountButtonGradientColor2 => _activeTheme.addAccountButtonGradientColor2; + Color get appBarGradientColor1 => _activeTheme.appBarGradientColor1; + Color get appBarGradientColor2 => _activeTheme.appBarGradientColor2; + Color get folderTabsGradientColor1 => _activeTheme.folderTabsGradientColor1; + Color get folderTabsGradientColor2 => _activeTheme.folderTabsGradientColor2; bool get highQualityPhotos => _highQualityPhotos; bool get blockBypass => _blockBypass; @@ -1218,6 +1468,144 @@ class ThemeProvider with ChangeNotifier { await _saveActiveTheme(); } + Future setUseGradientForChatsList(bool value) async { + _activeTheme = _activeTheme.copyWith(useGradientForChatsList: value); + notifyListeners(); + await _saveActiveTheme(); + } + + Future setChatsListBackgroundType(ChatsListBackgroundType type) async { + _activeTheme = _activeTheme.copyWith(chatsListBackgroundType: type); + notifyListeners(); + await _saveActiveTheme(); + } + + Future setChatsListImagePath(String? path) async { + _activeTheme = _activeTheme.copyWith(chatsListImagePath: path); + notifyListeners(); + await _saveActiveTheme(); + } + + Future setUseGradientForDrawer(bool value) async { + _activeTheme = _activeTheme.copyWith(useGradientForDrawer: value); + notifyListeners(); + await _saveActiveTheme(); + } + + Future setDrawerBackgroundType(DrawerBackgroundType type) async { + _activeTheme = _activeTheme.copyWith(drawerBackgroundType: type); + notifyListeners(); + await _saveActiveTheme(); + } + + Future setDrawerImagePath(String? path) async { + _activeTheme = _activeTheme.copyWith(drawerImagePath: path); + notifyListeners(); + await _saveActiveTheme(); + } + + Future setChatsListGradientColor1(Color color) async { + _activeTheme = _activeTheme.copyWith(chatsListGradientColor1: color); + notifyListeners(); + await _saveActiveTheme(); + } + + Future setChatsListGradientColor2(Color color) async { + _activeTheme = _activeTheme.copyWith(chatsListGradientColor2: color); + notifyListeners(); + await _saveActiveTheme(); + } + + Future setDrawerGradientColor1(Color color) async { + _activeTheme = _activeTheme.copyWith(drawerGradientColor1: color); + notifyListeners(); + await _saveActiveTheme(); + } + + Future setDrawerGradientColor2(Color color) async { + _activeTheme = _activeTheme.copyWith(drawerGradientColor2: color); + notifyListeners(); + await _saveActiveTheme(); + } + + Future setUseGradientForAddAccountButton(bool value) async { + _activeTheme = _activeTheme.copyWith(useGradientForAddAccountButton: value); + notifyListeners(); + await _saveActiveTheme(); + } + + Future setAddAccountButtonGradientColor1(Color color) async { + _activeTheme = _activeTheme.copyWith(addAccountButtonGradientColor1: color); + notifyListeners(); + await _saveActiveTheme(); + } + + Future setAddAccountButtonGradientColor2(Color color) async { + _activeTheme = _activeTheme.copyWith(addAccountButtonGradientColor2: color); + notifyListeners(); + await _saveActiveTheme(); + } + + Future setUseGradientForAppBar(bool value) async { + _activeTheme = _activeTheme.copyWith(useGradientForAppBar: value); + notifyListeners(); + await _saveActiveTheme(); + } + + Future setAppBarGradientColor1(Color color) async { + _activeTheme = _activeTheme.copyWith(appBarGradientColor1: color); + notifyListeners(); + await _saveActiveTheme(); + } + + Future setAppBarGradientColor2(Color color) async { + _activeTheme = _activeTheme.copyWith(appBarGradientColor2: color); + notifyListeners(); + await _saveActiveTheme(); + } + + Future setAppBarBackgroundType(AppBarBackgroundType type) async { + _activeTheme = _activeTheme.copyWith(appBarBackgroundType: type); + notifyListeners(); + await _saveActiveTheme(); + } + + Future setAppBarImagePath(String? path) async { + _activeTheme = _activeTheme.copyWith(appBarImagePath: path); + notifyListeners(); + await _saveActiveTheme(); + } + + Future setUseGradientForFolderTabs(bool value) async { + _activeTheme = _activeTheme.copyWith(useGradientForFolderTabs: value); + notifyListeners(); + await _saveActiveTheme(); + } + + Future setFolderTabsGradientColor1(Color color) async { + _activeTheme = _activeTheme.copyWith(folderTabsGradientColor1: color); + notifyListeners(); + await _saveActiveTheme(); + } + + Future setFolderTabsGradientColor2(Color color) async { + _activeTheme = _activeTheme.copyWith(folderTabsGradientColor2: color); + notifyListeners(); + await _saveActiveTheme(); + } + + Future setFolderTabsBackgroundType(FolderTabsBackgroundType type) async { + _activeTheme = _activeTheme.copyWith(folderTabsBackgroundType: type); + notifyListeners(); + await _saveActiveTheme(); + } + + Future setFolderTabsImagePath(String? path) async { + _activeTheme = _activeTheme.copyWith(folderTabsImagePath: path); + notifyListeners(); + await _saveActiveTheme(); + } + void toggleTheme() { if (appTheme == AppTheme.light) { setTheme(AppTheme.dark); diff --git a/lib/widgets/chat_message_bubble.dart b/lib/widgets/chat_message_bubble.dart index 787e8ab..8cc9afa 100644 --- a/lib/widgets/chat_message_bubble.dart +++ b/lib/widgets/chat_message_bubble.dart @@ -657,9 +657,7 @@ class ChatMessageBubble extends StatelessWidget { Uint8List? lowQualityBytes, int? videoType, }) { - // Логика открытия плеера void openFullScreenVideo() async { - // Показываем индикатор загрузки, пока получаем URL showDialog( context: context, barrierDismissible: false, @@ -669,12 +667,12 @@ class ChatMessageBubble extends StatelessWidget { try { final videoUrl = await ApiService.instance.getVideoUrl( videoId, - chatId!, // chatId из `build` + chatId!, messageId, ); - if (!context.mounted) return; // [!code ++] Проверка правильным способом - Navigator.pop(context); // Убираем индикатор + if (!context.mounted) return; + Navigator.pop(context); Navigator.push( context, MaterialPageRoute( @@ -682,8 +680,8 @@ class ChatMessageBubble extends StatelessWidget { ), ); } catch (e) { - if (!context.mounted) return; // [!code ++] Проверка правильным способом - Navigator.pop(context); // Убираем индикатор + if (!context.mounted) return; + Navigator.pop(context); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Не удалось загрузить видео: $e'), @@ -705,55 +703,58 @@ class ChatMessageBubble extends StatelessWidget { ); } - return GestureDetector( - onTap: openFullScreenVideo, - child: AspectRatio( - aspectRatio: 16 / 9, - child: ClipRRect( - borderRadius: BorderRadius.circular(12), - child: Stack( - alignment: Alignment.center, - fit: StackFit.expand, - children: [ - // Если у нас есть ХОТЬ ЧТО-ТО (блюр или URL), показываем ProgressiveImage - (highQualityUrl != null && highQualityUrl.isNotEmpty) || - (lowQualityBytes != null) - ? _ProgressiveNetworkImage( - url: highQualityUrl ?? '', - previewBytes: lowQualityBytes, - width: 220, - height: 160, - fit: BoxFit.cover, - keepAlive: false, - ) - : Container( - color: Colors.black26, - child: const Center( - child: Icon( - Icons.video_library_outlined, - color: Colors.white, - size: 40, + return RepaintBoundary( + key: ValueKey('video_preview_boundary_${messageId}_$videoId'), + child: GestureDetector( + onTap: openFullScreenVideo, + child: AspectRatio( + aspectRatio: 16 / 9, + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Stack( + alignment: Alignment.center, + fit: StackFit.expand, + children: [ + (highQualityUrl != null && highQualityUrl.isNotEmpty) || + (lowQualityBytes != null) + ? _ProgressiveNetworkImage( + key: ValueKey('video_preview_image_${messageId}_$videoId'), + url: highQualityUrl ?? '', + previewBytes: lowQualityBytes, + width: 220, + height: 160, + fit: BoxFit.cover, + keepAlive: true, + ) + : Container( + color: Colors.black26, + child: const Center( + child: Icon( + Icons.video_library_outlined, + color: Colors.white, + size: 40, + ), ), ), - ), - Container( - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.15), + Container( + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.15), + ), + child: Icon( + Icons.play_circle_filled_outlined, + color: Colors.white.withOpacity(0.95), + size: 50, + shadows: const [ + Shadow( + color: Colors.black38, + blurRadius: 4, + offset: Offset(0, 2), + ), + ], + ), ), - child: Icon( - Icons.play_circle_filled_outlined, - color: Colors.white.withOpacity(0.95), - size: 50, - shadows: const [ - Shadow( - color: Colors.black38, - blurRadius: 4, - offset: Offset(0, 2), - ), - ], - ), - ), - ], + ], + ), ), ), ), @@ -2004,13 +2005,16 @@ class ChatMessageBubble extends StatelessWidget { padding: const EdgeInsets.only(bottom: 4.0), child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 300), - child: _buildVideoPreview( - context: context, - videoId: videoId, - messageId: message.id, - highQualityUrl: highQualityThumbnailUrl, - lowQualityBytes: previewBytes, - videoType: videoType, + child: RepaintBoundary( + key: ValueKey('video_preview_${message.id}_$videoId'), + child: _buildVideoPreview( + context: context, + videoId: videoId, + messageId: message.id, + highQualityUrl: highQualityThumbnailUrl, + lowQualityBytes: previewBytes, + videoType: videoType, + ), ), ), ), @@ -2179,13 +2183,16 @@ class ChatMessageBubble extends StatelessWidget { widgets.add( Padding( padding: const EdgeInsets.only(bottom: 4.0), - child: _buildVideoPreview( - context: context, - videoId: videoId, - messageId: message.id, - highQualityUrl: highQualityThumbnailUrl, - lowQualityBytes: previewBytes, - videoType: videoType, + child: RepaintBoundary( + key: ValueKey('video_preview_${message.id}_$videoId'), + child: _buildVideoPreview( + context: context, + videoId: videoId, + messageId: message.id, + highQualityUrl: highQualityThumbnailUrl, + lowQualityBytes: previewBytes, + videoType: videoType, + ), ), ), ); diff --git a/lib/widgets/group_avatars.dart b/lib/widgets/group_avatars.dart index ecefb6f..450c4bf 100644 --- a/lib/widgets/group_avatars.dart +++ b/lib/widgets/group_avatars.dart @@ -2,6 +2,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:gwid/models/chat.dart'; import 'package:gwid/models/contact.dart'; +import 'package:gwid/services/avatar_cache_service.dart'; class GroupAvatars extends StatelessWidget { final Chat chat; @@ -39,13 +40,13 @@ class GroupAvatars extends StatelessWidget { double adaptiveAvatarSize; if (totalParticipants <= 2) { adaptiveAvatarSize = - avatarSize * 1.5; // Большие аватары для 1-2 участников + avatarSize * 1.5; } else if (totalParticipants <= 4) { adaptiveAvatarSize = - avatarSize * 1.2; // Средние аватары для 3-4 участников + avatarSize * 1.2; } else { adaptiveAvatarSize = - avatarSize * 0.8; // Маленькие аватары для 5+ участников + avatarSize * 0.8; } return SizedBox( @@ -114,45 +115,91 @@ class GroupAvatars extends StatelessWidget { ) { final colors = Theme.of(context).colorScheme; - return Container( - width: size, - height: size, - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all(color: colors.surface, width: 2), - boxShadow: [ - BoxShadow( - color: colors.shadow.withOpacity(0.3), - blurRadius: 4, - offset: const Offset(0, 2), + if (contact == null || contact.photoBaseUrl == null || contact.photoBaseUrl!.isEmpty) { + return Container( + width: size, + height: size, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: colors.surface, width: 2), + boxShadow: [ + BoxShadow( + color: colors.shadow.withOpacity(0.3), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: CircleAvatar( + radius: size / 2, + backgroundColor: contact != null + ? colors.primaryContainer + : colors.secondaryContainer, + child: Text( + contact?.name.isNotEmpty == true + ? contact!.name[0].toUpperCase() + : participantId.toString().substring( + participantId.toString().length - 1, + ), + style: TextStyle( + color: contact != null + ? colors.onPrimaryContainer + : colors.onSecondaryContainer, + fontSize: size * 0.5, + fontWeight: FontWeight.w600, + ), ), - ], - ), - child: CircleAvatar( - radius: size / 2, - backgroundColor: contact != null - ? colors.primaryContainer - : colors.secondaryContainer, - backgroundImage: contact?.photoBaseUrl != null - ? NetworkImage(contact!.photoBaseUrl!) - : null, - child: contact?.photoBaseUrl == null - ? Text( - contact?.name.isNotEmpty == true - ? contact!.name[0].toUpperCase() - : participantId.toString().substring( - participantId.toString().length - 1, - ), // Последняя цифра ID - style: TextStyle( - color: contact != null - ? colors.onPrimaryContainer - : colors.onSecondaryContainer, - fontSize: size * 0.5, - fontWeight: FontWeight.w600, - ), - ) - : null, - ), + ), + ); + } + + return FutureBuilder( + future: AvatarCacheService().getAvatar(contact.photoBaseUrl, userId: participantId), + builder: (context, snapshot) { + ImageProvider? imageProvider; + if (snapshot.hasData && snapshot.data != null) { + imageProvider = snapshot.data; + } else { + imageProvider = NetworkImage(contact.photoBaseUrl!); + } + + return Container( + width: size, + height: size, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: colors.surface, width: 2), + boxShadow: [ + BoxShadow( + color: colors.shadow.withOpacity(0.3), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: CircleAvatar( + radius: size / 2, + backgroundColor: colors.primaryContainer, + backgroundImage: imageProvider, + onBackgroundImageError: (exception, stackTrace) { + }, + child: imageProvider == null + ? Text( + contact.name.isNotEmpty + ? contact.name[0].toUpperCase() + : participantId.toString().substring( + participantId.toString().length - 1, + ), + style: TextStyle( + color: colors.onPrimaryContainer, + fontSize: size * 0.5, + fontWeight: FontWeight.w600, + ), + ) + : null, + ), + ); + }, ); }