From 44658bec3bf8b260b8191979eec32832ac8f5f08 Mon Sep 17 00:00:00 2001 From: torvalds Date: Sun, 28 Jun 2026 16:30:37 +0700 Subject: [PATCH] =?UTF-8?q?fix:=20=D0=B7=D0=B0=D0=B4=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D0=B5=20=D0=BE=D1=82=20=D1=81=D1=82=D0=B0=D1=81=D0=B8=D0=BA?= =?UTF-8?q?=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/core/config/app_show_extra_info.dart | 20 ++ lib/frontend/screens/calls/calls_tab.dart | 42 ++++ .../screens/chats/chat_info_screen.dart | 115 +++++++---- .../screens/contacts/contacts_tab.dart | 63 ++++-- .../screens/profile/debug_menu_screen.dart | 61 ++++++ .../screens/profile/notifications_screen.dart | 24 +++ .../screens/profile/settings_tab.dart | 185 +++++++----------- lib/frontend/widgets/chat_menu_overlay.dart | 51 +++-- lib/main.dart | 3 + 9 files changed, 376 insertions(+), 188 deletions(-) create mode 100644 lib/core/config/app_show_extra_info.dart diff --git a/lib/core/config/app_show_extra_info.dart b/lib/core/config/app_show_extra_info.dart new file mode 100644 index 0000000..3f95d45 --- /dev/null +++ b/lib/core/config/app_show_extra_info.dart @@ -0,0 +1,20 @@ +import 'package:flutter/foundation.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class AppShowExtraInfo { + static const prefKey = 'dev_show_extra_info'; + static const bool defaultValue = false; + + static final ValueNotifier current = ValueNotifier(defaultValue); + + static Future load() async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getBool(prefKey) ?? defaultValue; + } + + static Future save(bool value) async { + current.value = value; + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool(prefKey, value); + } +} diff --git a/lib/frontend/screens/calls/calls_tab.dart b/lib/frontend/screens/calls/calls_tab.dart index 3d0993a..e2fcdc1 100644 --- a/lib/frontend/screens/calls/calls_tab.dart +++ b/lib/frontend/screens/calls/calls_tab.dart @@ -9,6 +9,7 @@ import '../../../core/utils/format.dart'; import '../../../backend/modules/calls.dart'; import '../../widgets/komet_avatar.dart'; import '../../widgets/connection_status.dart'; +import '../../widgets/chat_menu_overlay.dart'; class CallsTab extends StatefulWidget { const CallsTab({super.key}); @@ -206,6 +207,24 @@ class _CallsTabState extends State { fontSize: 12, ), ), + const SizedBox(width: 4), + Builder( + builder: (btnContext) => IconButton( + icon: Icon( + Symbols.more_vert, + color: cs.onSurfaceVariant, + size: 20, + weight: 400, + ), + padding: EdgeInsets.zero, + visualDensity: VisualDensity.compact, + constraints: const BoxConstraints( + minWidth: 36, + minHeight: 36, + ), + onPressed: () => _showCallMenu(btnContext, call), + ), + ), ], ), ), @@ -213,6 +232,29 @@ class _CallsTabState extends State { ); } + void _showCallMenu(BuildContext anchorContext, CallLogEntry call) { + final box = anchorContext.findRenderObject() as RenderBox?; + if (box == null || !box.hasSize) return; + final anchorRect = box.localToGlobal(Offset.zero) & box.size; + showChatMenu( + context: context, + anchorRect: anchorRect, + items: [ + ChatMenuItem( + icon: Symbols.delete, + label: 'Удалить', + destructive: true, + onTap: () {}, + ), + ChatMenuItem( + icon: Symbols.call, + label: 'Перезвонить', + onTap: () {}, + ), + ], + ); + } + Widget _buildTabItem(String label, int index, ColorScheme cs) { final isSelected = _selectedTabIndex == index; return GestureDetector( diff --git a/lib/frontend/screens/chats/chat_info_screen.dart b/lib/frontend/screens/chats/chat_info_screen.dart index 42de619..24d67e6 100644 --- a/lib/frontend/screens/chats/chat_info_screen.dart +++ b/lib/frontend/screens/chats/chat_info_screen.dart @@ -4,11 +4,14 @@ import 'package:flutter/material.dart'; import 'package:material_symbols_icons/symbols.dart'; import '../../../backend/modules/messages.dart' show ContactCache; import '../../../core/cache/info_cache.dart'; +import '../../../core/config/app_show_extra_info.dart'; import '../../../core/storage/app_database.dart'; import '../../../core/utils/format.dart'; import '../../widgets/connection_status.dart'; import '../../widgets/glossy_pill.dart'; import '../../widgets/komet_avatar.dart'; +import '../../widgets/swipe_route.dart'; +import 'chat_screen.dart'; class _MemberInfo { final int id; @@ -34,12 +37,15 @@ class ChatInfoScreen extends StatefulWidget { final String imageUrl; final String chatType; + final int? dialogPeerId; + const ChatInfoScreen({ super.key, required this.chatId, required this.name, required this.imageUrl, required this.chatType, + this.dialogPeerId, }); @override @@ -81,11 +87,26 @@ class _ChatInfoScreenState extends State { } List get _tabs { + final showInfo = AppShowExtraInfo.current.value; switch (widget.chatType) { case 'DIALOG': - return _isBot - ? ['Info', 'Медиа', 'Файлы', 'Голосовые', 'Ссылки'] - : ['Общие чаты', 'Медиа', 'Info', 'Файлы', 'Голосовые', 'Ссылки']; + if (_isBot) { + return [ + if (showInfo) 'Info', + 'Медиа', + 'Файлы', + 'Голосовые', + 'Ссылки', + ]; + } + return [ + 'Общие чаты', + 'Медиа', + if (showInfo) 'Info', + 'Файлы', + 'Голосовые', + 'Ссылки', + ]; case 'CHAT': return ['Участники', 'Info', 'Медиа', 'Файлы', 'Голосовые', 'Ссылки']; case 'CHANNEL': @@ -101,19 +122,18 @@ class _ChatInfoScreenState extends State { final info = await ChatInfoFetch.get(widget.chatId); if (!mounted) return; - if (info == null) { - setState(() => _isLoading = false); - return; - } _chatData = info; if (widget.chatType == 'DIALOG') { - final parts = _chatData!['participants'] as Map? ?? {}; - for (final key in parts.keys) { - final id = key is int ? key : int.tryParse(key.toString()); - if (id != null && id != _myId) { - _otherId = id; - break; + _otherId = widget.dialogPeerId; + if (_otherId == null && info != null) { + final parts = info['participants'] as Map? ?? {}; + for (final key in parts.keys) { + final id = key is int ? key : int.tryParse(key.toString()); + if (id != null && id != _myId) { + _otherId = id; + break; + } } } @@ -133,6 +153,9 @@ class _ChatInfoScreenState extends State { _isOnline = st == 1; } } + } else if (info == null) { + setState(() => _isLoading = false); + return; } else if (widget.chatType == 'CHAT') { final parts = _chatData!['participants'] as Map? ?? {}; final admins = _chatData!['adminParticipants'] as Map? ?? {}; @@ -186,11 +209,9 @@ class _ChatInfoScreenState extends State { @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; - final isDark = cs.brightness == Brightness.dark; - final bg = isDark ? Colors.black : cs.surface; return Scaffold( - backgroundColor: bg, + backgroundColor: cs.surface, floatingActionButtonLocation: FloatingActionButtonLocation.startFloat, floatingActionButton: const ConnectionSpinner(), body: SafeArea( @@ -292,31 +313,31 @@ class _ChatInfoScreenState extends State { // ─── ACTION BUTTONS ────────────────────────────────────────────────────── Widget _buildActions(ColorScheme cs) { - final List<({IconData icon, String label})> btns; + final List<({IconData icon, String label, VoidCallback? onTap})> btns; if (widget.chatType == 'DIALOG') { if (_isBot) { btns = [ - (icon: Icons.chat_bubble, label: 'Чат'), - (icon: Icons.notifications, label: 'Звук'), + (icon: Icons.chat_bubble, label: 'Чат', onTap: _openChat), + (icon: Icons.notifications, label: 'Звук', onTap: null), ]; } else { btns = [ - (icon: Icons.chat_bubble, label: 'Чат'), - (icon: Icons.notifications, label: 'Звук'), - (icon: Icons.call, label: 'Звонок'), + (icon: Icons.chat_bubble, label: 'Чат', onTap: _openChat), + (icon: Icons.notifications, label: 'Звук', onTap: null), + (icon: Icons.call, label: 'Звонок', onTap: null), ]; } } else if (widget.chatType == 'CHANNEL') { btns = [ - (icon: Icons.notifications, label: 'Звук'), - (icon: Icons.exit_to_app, label: 'Покинуть'), + (icon: Icons.notifications, label: 'Звук', onTap: null), + (icon: Icons.exit_to_app, label: 'Покинуть', onTap: null), ]; } else { btns = [ - (icon: Icons.chat_bubble, label: 'Чат'), - (icon: Icons.notifications, label: 'Звук'), - (icon: Icons.exit_to_app, label: 'Покинуть'), + (icon: Icons.chat_bubble, label: 'Чат', onTap: null), + (icon: Icons.notifications, label: 'Звук', onTap: null), + (icon: Icons.exit_to_app, label: 'Покинуть', onTap: null), ]; } @@ -325,7 +346,7 @@ class _ChatInfoScreenState extends State { child: Row( children: [ for (int i = 0; i < btns.length; i++) ...[ - _actionBtn(cs, btns[i].icon, btns[i].label), + _actionBtn(cs, btns[i].icon, btns[i].label, btns[i].onTap), if (i < btns.length - 1) const SizedBox(width: 8), ], ], @@ -333,9 +354,27 @@ class _ChatInfoScreenState extends State { ); } - Widget _actionBtn(ColorScheme cs, IconData icon, String label) { + void _openChat() { + pushSwipeable( + context, + (_) => ChatScreen( + chatId: widget.chatId, + name: widget.name, + imageUrl: widget.imageUrl, + chatType: 'DIALOG', + ), + ); + } + + Widget _actionBtn( + ColorScheme cs, + IconData icon, + String label, [ + VoidCallback? onTap, + ]) { return Expanded( child: GlossyPill( + onTap: onTap, color: cs.surfaceContainerHigh, borderRadius: BorderRadius.circular(14), padding: const EdgeInsets.symmetric(vertical: 10), @@ -377,6 +416,13 @@ class _ChatInfoScreenState extends State { _simpleInfoCard(cs, 'Номер телефона', formatPhone(phoneInt)!), ); } + final bio = + (_contactData?['description'] as String?) ?? + (_contactData?['about'] as String?); + if (bio != null && bio.isNotEmpty) { + if (items.isNotEmpty) items.add(const SizedBox(height: 8)); + items.add(_simpleInfoCard(cs, 'О себе', bio)); + } } } else if (widget.chatType == 'CHANNEL') { final link = _chatData?['link'] as String?; @@ -645,17 +691,6 @@ class _ChatInfoScreenState extends State { Widget _buildInfoTabContent(ColorScheme cs) { final items = []; - if (widget.chatType == 'DIALOG' && !_isBot) { - final bio = - (_contactData?['description'] as String?) ?? - (_contactData?['about'] as String?); - if (bio != null && bio.isNotEmpty) { - items - ..add(_infoCard(cs, 'О себе', bio)) - ..add(const SizedBox(height: 8)); - } - } - if (widget.chatType == 'CHAT') { final desc = _chatData?['description'] as String?; if (desc != null && desc.isNotEmpty) { diff --git a/lib/frontend/screens/contacts/contacts_tab.dart b/lib/frontend/screens/contacts/contacts_tab.dart index cf0ad73..1b62570 100644 --- a/lib/frontend/screens/contacts/contacts_tab.dart +++ b/lib/frontend/screens/contacts/contacts_tab.dart @@ -3,14 +3,41 @@ import 'package:material_symbols_icons/symbols.dart'; import '../../../core/protocol/opcode_map.dart'; import '../../../core/protocol/packet.dart'; import '../../../core/storage/app_database.dart'; +import '../../../core/storage/token_storage.dart'; import '../../../backend/modules/contacts.dart'; import '../../../main.dart'; import '../../widgets/komet_avatar.dart'; import '../../widgets/connection_status.dart'; import '../../widgets/sheet_helpers.dart'; -import 'contact_profile_screen.dart'; +import '../chats/chat_info_screen.dart'; import 'nfc_exchange_sheet.dart'; +Future openContactDialogProfile( + BuildContext context, { + required int contactId, + required String name, + String? avatarUrl, +}) async { + final accountId = await TokenStorage.getActiveAccountId(); + final existing = accountId == null + ? null + : await AppDatabase.findDialogChatByParticipant(accountId, contactId); + final chatId = existing ?? ((accountId ?? 0) ^ contactId); + if (!context.mounted) return; + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => ChatInfoScreen( + chatId: chatId, + name: name, + imageUrl: avatarUrl ?? '', + chatType: 'DIALOG', + dialogPeerId: contactId, + ), + ), + ); +} + class ContactsTab extends StatefulWidget { const ContactsTab({super.key}); @@ -100,18 +127,12 @@ class _ContactsTabState extends State { return Material( color: Colors.transparent, child: InkWell( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (_) => ContactProfileScreen( - contactId: contact.id, - initialName: nameToDisplay, - initialAvatarUrl: contact.baseUrl, - ), - ), - ); - }, + onTap: () => openContactDialogProfile( + context, + contactId: contact.id, + name: nameToDisplay, + avatarUrl: contact.baseUrl, + ), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), child: Row( @@ -311,13 +332,21 @@ class _SearchContactSheetState extends State<_SearchContactSheet> { } if (!mounted) return; final navigator = Navigator.of(context); + final accountId = await TokenStorage.getActiveAccountId(); + final existing = accountId == null + ? null + : await AppDatabase.findDialogChatByParticipant(accountId, id); + final chatId = existing ?? ((accountId ?? 0) ^ id); + if (!mounted) return; navigator.pop(); navigator.push( MaterialPageRoute( - builder: (_) => ContactProfileScreen( - contactId: id, - initialName: name, - initialAvatarUrl: raw['baseUrl'] as String?, + builder: (_) => ChatInfoScreen( + chatId: chatId, + name: name ?? 'User #$id', + imageUrl: raw['baseUrl'] as String? ?? '', + chatType: 'DIALOG', + dialogPeerId: id, ), ), ); diff --git a/lib/frontend/screens/profile/debug_menu_screen.dart b/lib/frontend/screens/profile/debug_menu_screen.dart index ff88fe3..5573f6c 100644 --- a/lib/frontend/screens/profile/debug_menu_screen.dart +++ b/lib/frontend/screens/profile/debug_menu_screen.dart @@ -8,6 +8,7 @@ import '../../../core/config/app_pranks.dart'; import '../../../core/config/app_stories.dart'; import '../../../core/config/app_commands.dart'; import '../../../core/config/app_link_preview.dart'; +import '../../../core/config/app_show_extra_info.dart'; import '../../../core/config/app_digital_id_mode.dart'; import '../../../core/config/app_media_cache.dart'; import '../../../core/protocol/opcode_map.dart'; @@ -905,6 +906,66 @@ class _DebugMenuScreenState extends State { ), ), ), + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 0), + child: ValueListenableBuilder( + valueListenable: AppShowExtraInfo.current, + builder: (context, extraInfoOn, _) { + return GlossyPill( + color: cs.surfaceContainerHigh, + borderRadius: BorderRadius.circular(20), + depth: 6, + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 17, + ), + child: Row( + children: [ + Icon( + Symbols.info, + 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( + 'Раздел «Info» в настройках и вкладка с ' + 'технической информацией в профиле собеседника', + style: TextStyle( + color: cs.onSurfaceVariant, + fontSize: 13, + ), + ), + ], + ), + ), + Switch( + value: extraInfoOn, + onChanged: (v) { + AppShowExtraInfo.save(v); + }, + ), + ], + ), + ); + }, + ), + ), + ), SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.fromLTRB(16, 12, 16, 0), diff --git a/lib/frontend/screens/profile/notifications_screen.dart b/lib/frontend/screens/profile/notifications_screen.dart index c211f12..7060d00 100644 --- a/lib/frontend/screens/profile/notifications_screen.dart +++ b/lib/frontend/screens/profile/notifications_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:material_symbols_icons/symbols.dart'; +import '../../../core/utils/haptics.dart'; import '../../widgets/connection_status.dart'; import '../../widgets/glossy_pill.dart'; @@ -19,8 +20,15 @@ class _NotificationsScreenState extends State { bool _personalChatsEnabled = true; bool _groupsEnabled = true; bool _channelsEnabled = true; + bool _hapticsEnabled = Haptics.enabled; String _selectedSound = 'По умолчанию'; + Future _setHaptics(bool value) async { + await Haptics.setEnabled(value); + if (value) Haptics.success(); + if (mounted) setState(() => _hapticsEnabled = value); + } + static const List _sounds = [ 'По умолчанию', 'Колокольчик', @@ -162,6 +170,22 @@ class _NotificationsScreenState extends State { onTap: _pickSound, ), ]), + const SizedBox(height: 20), + const SectionHeader( + 'Тактильная отдача', + padding: EdgeInsets.fromLTRB(8, 0, 8, 8), + fontSize: 14, + ), + _card(cs, [ + _toggleRow( + cs, + icon: Symbols.vibration, + label: 'Тактильная отдача', + subtitle: 'Виброотклик при действиях в приложении', + value: _hapticsEnabled, + onChanged: _setHaptics, + ), + ]), ], ), ), diff --git a/lib/frontend/screens/profile/settings_tab.dart b/lib/frontend/screens/profile/settings_tab.dart index 33ea1cd..02275a1 100644 --- a/lib/frontend/screens/profile/settings_tab.dart +++ b/lib/frontend/screens/profile/settings_tab.dart @@ -7,10 +7,10 @@ import '../../../backend/modules/chats.dart'; import '../../../backend/modules/messages.dart'; import '../../../core/cache/self_presence.dart'; import '../../../core/config/komet_settings.dart'; +import '../../../core/config/app_show_extra_info.dart'; import '../../../core/storage/app_database.dart'; import '../../../core/storage/token_storage.dart'; import '../../../core/utils/format.dart'; -import '../../../core/utils/haptics.dart'; import '../../../l10n/app_localizations.dart'; import '../../../main.dart'; import '../../widgets/connection_status.dart'; @@ -27,7 +27,6 @@ import '../digital_id/digital_id_web_screen.dart'; import '../webapp/web_app_screen.dart'; import 'cloud_storage_screen.dart'; import 'customization_screen.dart'; -import 'performance_screen.dart'; import 'debug_menu_screen.dart'; import 'devices_screen.dart'; import 'edit_profile_screen.dart'; @@ -52,7 +51,6 @@ class _SettingsTabState extends State { int _versionSecretTapCount = 0; Timer? _versionSecretTapResetTimer; StreamSubscription? _profileUpdateSub; - bool _hapticsEnabled = Haptics.enabled; @override void initState() { @@ -106,13 +104,6 @@ class _SettingsTabState extends State { }); } - Future _setHaptics(bool value) async { - await Haptics.setEnabled(value); - // Let the user *feel* the confirmation the instant they switch it on. - if (value) Haptics.success(); - if (mounted) setState(() => _hapticsEnabled = value); - } - Future _openCloudStorage(BuildContext context) async { final cs = Theme.of(context).colorScheme; final ok = await showInfoActionSheet( @@ -256,67 +247,60 @@ class _SettingsTabState extends State { SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 0), - child: _buildSection( - context, - cs, - items: [ - _SettingsItem( - icon: Symbols.badge, - label: 'Цифровой ID', - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - AppDigitalIdNative.current.value || - !webViewSupported - ? const DigitalIdScreen() - : const DigitalIdWebScreen(), + child: ValueListenableBuilder( + valueListenable: AppShowExtraInfo.current, + builder: (context, showExtraInfo, _) { + return _buildSection( + context, + cs, + items: [ + _SettingsItem( + icon: Symbols.badge, + label: 'Цифровой ID', + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + AppDigitalIdNative.current.value || + !webViewSupported + ? const DigitalIdScreen() + : const DigitalIdWebScreen(), + ), + ); + }, + ), + _SettingsItem( + icon: Symbols.language, + label: 'Войти в Сферум', + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => WebAppScreen( + title: 'Сферум', + loader: () => webAppModule.fetchSferum(), + ), + ), + ); + }, + ), + if (showExtraInfo) + _SettingsItem( + icon: Symbols.info, + label: 'Info', + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const InfoScreen(), + ), + ); + }, ), - ); - }, - ), - _SettingsItem( - icon: Symbols.language, - label: 'Войти в Сферум', - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => WebAppScreen( - title: 'Сферум', - loader: () => webAppModule.fetchSferum(), - ), - ), - ); - }, - ), - _SettingsItem( - // Иконку кометы блять дайте!!!!!!!1 - icon: Symbols.auto_awesome, - label: 'Komet', - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const KometSettingsScreen(), - ), - ); - }, - ), - _SettingsItem( - icon: Symbols.info, - label: 'Info', - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const InfoScreen(), - ), - ); - }, - ), - ], + ], + ); + }, ), ), ), @@ -339,18 +323,6 @@ class _SettingsTabState extends State { ); }, ), - _SettingsItem( - icon: Symbols.speed, - label: 'Производительность', - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const PerformanceScreen(), - ), - ); - }, - ), ], ), ), @@ -374,12 +346,6 @@ class _SettingsTabState extends State { ); }, ), - _SettingsItem( - icon: Symbols.vibration, - label: 'Тактильная отдача', - toggleValue: _hapticsEnabled, - onToggle: _setHaptics, - ), _SettingsItem( icon: Symbols.cloud, label: 'Облачное хранилище [BETA]', @@ -506,6 +472,18 @@ class _SettingsTabState extends State { tintColor: cs.error, onTap: _confirmLogout, ), + _SettingsItem( + icon: Symbols.auto_awesome, + label: 'Komet', + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const KometSettingsScreen(), + ), + ); + }, + ), ], ), ), @@ -737,9 +715,7 @@ class _SettingsTabState extends State { Material( color: Colors.transparent, child: InkWell( - onTap: item.isToggle - ? () => item.onToggle!(!(item.toggleValue ?? false)) - : (item.onTap ?? () {}), + onTap: item.onTap ?? () {}, borderRadius: isLast ? const BorderRadius.vertical(bottom: Radius.circular(20)) : null, @@ -764,18 +740,12 @@ class _SettingsTabState extends State { ), ), ), - if (item.isToggle) - Switch.adaptive( - value: item.toggleValue ?? false, - onChanged: item.onToggle, - ) - else - Icon( - Symbols.chevron_right, - color: cs.outline, - size: 20, - weight: 400, - ), + Icon( + Symbols.chevron_right, + color: cs.outline, + size: 20, + weight: 400, + ), ], ), ), @@ -801,21 +771,12 @@ class _SettingsItem { final VoidCallback? onTap; final Color? tintColor; - /// When [onToggle] is set the row renders a trailing switch instead of a - /// chevron, and [toggleValue] reflects its current state. - final bool? toggleValue; - final ValueChanged? onToggle; - const _SettingsItem({ required this.icon, required this.label, this.onTap, this.tintColor, - this.toggleValue, - this.onToggle, }); - - bool get isToggle => onToggle != null; } class _PhoneSpoiler extends StatefulWidget { diff --git a/lib/frontend/widgets/chat_menu_overlay.dart b/lib/frontend/widgets/chat_menu_overlay.dart index 030dfb9..d8c254b 100644 --- a/lib/frontend/widgets/chat_menu_overlay.dart +++ b/lib/frontend/widgets/chat_menu_overlay.dart @@ -60,6 +60,7 @@ class _ChatMenuLayerState extends State<_ChatMenuLayer> with SingleTickerProviderStateMixin { static const double _menuWidth = 290.0; static const double _hMargin = 8.0; + static const double _vMargin = 8.0; static const double _gap = 6.0; late final AnimationController _animController; @@ -104,17 +105,24 @@ class _ChatMenuLayerState extends State<_ChatMenuLayer> } Rect _resolveRect(Size screen) { - double left = widget.anchorRect.right - _menuWidth; - left = left.clamp(_hMargin, screen.width - _menuWidth - _hMargin); - double top = widget.anchorRect.bottom + _gap; - return Rect.fromLTWH(left, top, _menuWidth, 0); + final maxWidth = screen.width - 2 * _hMargin; + final width = maxWidth <= 0 ? screen.width : (_menuWidth.clamp(0.0, maxWidth)); + final maxLeft = screen.width - width - _hMargin; + double left = widget.anchorRect.right - width; + if (left > maxLeft) left = maxLeft; + if (left < _hMargin) left = _hMargin; + final top = widget.anchorRect.bottom + _gap; + return Rect.fromLTWH(left, top, width, 0); } @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final screen = MediaQuery.sizeOf(context); + final bottomInset = MediaQuery.paddingOf(context).bottom; final rect = _resolveRect(screen); + final maxHeight = (screen.height - rect.top - bottomInset - _vMargin) + .clamp(120.0, double.infinity); return AnimatedBuilder( animation: _animation, builder: (ctx, child) { @@ -151,21 +159,26 @@ class _ChatMenuLayerState extends State<_ChatMenuLayer> clipBehavior: Clip.antiAlias, elevation: 12, shadowColor: Colors.black.withValues(alpha: 0.45), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox(height: 6), - for (final item in widget.items) ...[ - _ChatMenuRow(item: item, onTap: () => _onItemTap(item)), - if (item.dividerAfter) - Divider( - height: 1, - thickness: 1, - color: cs.onSurface.withValues(alpha: 0.07), - ), - ], - const SizedBox(height: 6), - ], + child: ConstrainedBox( + constraints: BoxConstraints(maxHeight: maxHeight), + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 6), + for (final item in widget.items) ...[ + _ChatMenuRow(item: item, onTap: () => _onItemTap(item)), + if (item.dividerAfter) + Divider( + height: 1, + thickness: 1, + color: cs.onSurface.withValues(alpha: 0.07), + ), + ], + const SizedBox(height: 6), + ], + ), + ), ), ), ); diff --git a/lib/main.dart b/lib/main.dart index 6a79473..641cda4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -17,6 +17,7 @@ import 'core/storage/app_instance.dart'; import 'core/storage/draft_store.dart'; import 'core/config/app_accent.dart'; import 'core/config/app_amoled.dart'; +import 'core/config/app_show_extra_info.dart'; import 'core/config/app_bubble_behavior.dart'; import 'core/config/komet_settings.dart'; import 'core/config/app_bubble_shape.dart'; @@ -126,6 +127,7 @@ void main() async { final linkPreviewFuture = AppLinkPreview.load(); final cacheLimitFuture = AppMediaCacheLimit.load(); final digitalIdNativeFuture = AppDigitalIdNative.load(); + final showExtraInfoFuture = AppShowExtraInfo.load(); final packageInfo = await packageInfoFuture; isOnemeFlavor = packageInfo.packageName == 'ru.oneme.app'; @@ -170,6 +172,7 @@ void main() async { AppLinkPreview.current.value = await linkPreviewFuture; AppMediaCacheLimit.current.value = await cacheLimitFuture; AppDigitalIdNative.current.value = await digitalIdNativeFuture; + AppShowExtraInfo.current.value = await showExtraInfoFuture; runApp( KometApp( initialLocale: initialLocale,