From c527620a15a9e56f86ecb9cbdd4e159b4e76a843 Mon Sep 17 00:00:00 2001 From: Jganenokk Date: Mon, 13 Jul 2026 19:12:51 +0700 Subject: [PATCH] =?UTF-8?q?feat:=20glossy=20=D1=81=D1=82=D0=B8=D0=BB=D1=8C?= =?UTF-8?q?,=20=D0=B2=D0=BE=D0=B7=D0=BC=D0=BE=D0=B6=D0=BD=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D1=8C=20=D0=B2=D1=8B=D0=B1=D0=BE=D1=80=D0=B0=20frost=20b?= =?UTF-8?q?lur.=20fix:=20=D0=BF=D0=B5=D1=80=D0=B5=D1=85=D0=BE=D0=B4=20?= =?UTF-8?q?=D0=BA=20=D0=B7=D0=B0=D0=BA=D1=80=D0=B5=D0=BF=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D0=BD=D0=BE=D0=BC=D1=83=20=D1=81=D0=BE=D0=BE=D0=B1=D1=89=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/core/config/app_chat_chrome.dart | 4 +- lib/core/config/app_composer_background.dart | 25 ++ lib/core/config/app_composer_style.dart | 25 ++ lib/core/config/app_frost.dart | 20 + .../config/app_message_actions_style.dart | 4 +- lib/core/config/app_pill_gradient.dart | 2 +- lib/core/config/app_visual_style.dart | 4 +- lib/core/storage/app_database.dart | 50 +++ .../screens/chats/chat/chat_controller.dart | 201 ++++++++- .../screens/chats/chat/view/chat_header.dart | 18 + .../chats/chat/view/composer_input.dart | 129 ++++-- lib/frontend/screens/chats/chat_screen.dart | 319 ++++++++----- lib/frontend/screens/chats/search_screen.dart | 28 +- .../contacts/contact_profile_screen.dart | 420 ------------------ .../screens/contacts/contacts_tab.dart | 27 +- .../contacts/open_contact_profile.dart | 31 ++ .../screens/profile/appearance_screen.dart | 88 ++++ .../profile/customization_section.dart | 2 +- lib/frontend/widgets/glossy_pill.dart | 30 +- lib/frontend/widgets/max_link_handler.dart | 17 +- .../widgets/message_actions_overlay.dart | 8 +- lib/l10n/app_en.arb | 6 +- lib/l10n/app_localizations.dart | 26 +- lib/l10n/app_localizations_en.dart | 15 +- lib/l10n/app_localizations_ru.dart | 14 +- lib/l10n/app_ru.arb | 6 +- lib/main.dart | 6 + 27 files changed, 893 insertions(+), 632 deletions(-) create mode 100644 lib/core/config/app_composer_background.dart create mode 100644 lib/core/config/app_composer_style.dart create mode 100644 lib/core/config/app_frost.dart delete mode 100644 lib/frontend/screens/contacts/contact_profile_screen.dart create mode 100644 lib/frontend/screens/contacts/open_contact_profile.dart diff --git a/lib/core/config/app_chat_chrome.dart b/lib/core/config/app_chat_chrome.dart index 85f8b53..b8f9ca5 100644 --- a/lib/core/config/app_chat_chrome.dart +++ b/lib/core/config/app_chat_chrome.dart @@ -9,7 +9,7 @@ class AppChatChrome { static final _setting = PersistedEnum( prefKey: prefKey, - defaultValue: ChatChromeStyle.none, + defaultValue: ChatChromeStyle.transparent, encode: _encode, decode: _parse, ); @@ -17,7 +17,7 @@ class AppChatChrome { static ValueNotifier get current => _setting.current; static ChatChromeStyle _parse(String? value) => - enumFromName(ChatChromeStyle.values, value, ChatChromeStyle.none); + enumFromName(ChatChromeStyle.values, value, ChatChromeStyle.transparent); static String _encode(ChatChromeStyle value) => value.name; diff --git a/lib/core/config/app_composer_background.dart b/lib/core/config/app_composer_background.dart new file mode 100644 index 0000000..ecd14fe --- /dev/null +++ b/lib/core/config/app_composer_background.dart @@ -0,0 +1,25 @@ +import 'package:flutter/foundation.dart'; + +import 'persisted_setting.dart'; + +enum ComposerBackground { standard, frostBlur } + +class AppComposerBackground { + static const prefKey = 'app_composer_background'; + + static final _setting = PersistedEnum( + prefKey: prefKey, + defaultValue: ComposerBackground.standard, + encode: (value) => value.name, + decode: _parse, + ); + + static ValueNotifier get current => _setting.current; + + static Future load() => _setting.load(); + + static Future save(ComposerBackground value) => _setting.save(value); + + static ComposerBackground _parse(String? val) => + enumFromName(ComposerBackground.values, val, ComposerBackground.standard); +} diff --git a/lib/core/config/app_composer_style.dart b/lib/core/config/app_composer_style.dart new file mode 100644 index 0000000..69d3872 --- /dev/null +++ b/lib/core/config/app_composer_style.dart @@ -0,0 +1,25 @@ +import 'package:flutter/foundation.dart'; + +import 'persisted_setting.dart'; + +enum ComposerStyle { glossy, materialYou } + +class AppComposerStyle { + static const prefKey = 'app_composer_style'; + + static final _setting = PersistedEnum( + prefKey: prefKey, + defaultValue: ComposerStyle.glossy, + encode: (value) => value.name, + decode: _parse, + ); + + static ValueNotifier get current => _setting.current; + + static Future load() => _setting.load(); + + static Future save(ComposerStyle value) => _setting.save(value); + + static ComposerStyle _parse(String? val) => + enumFromName(ComposerStyle.values, val, ComposerStyle.glossy); +} diff --git a/lib/core/config/app_frost.dart b/lib/core/config/app_frost.dart new file mode 100644 index 0000000..3d8b2eb --- /dev/null +++ b/lib/core/config/app_frost.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; + +class AppFrost { + static const double sigma = 34; + static const double panelSigma = 24; + + static Color panelTint(ColorScheme cs) => cs.surface.withValues(alpha: 0.38); + + static Color blurPanelTint(ColorScheme cs) => + cs.surfaceContainerHigh.withValues(alpha: 0.55); + + static Color pillTint(ColorScheme cs) => + cs.surfaceContainerHigh.withValues(alpha: 0.45); + + static Color inputTint(ColorScheme cs) => + cs.surfaceContainerHighest.withValues(alpha: 0.45); + + static BorderSide hairline(ColorScheme cs) => + BorderSide(color: cs.outlineVariant.withValues(alpha: 0.4), width: 0.5); +} diff --git a/lib/core/config/app_message_actions_style.dart b/lib/core/config/app_message_actions_style.dart index 9375175..a21e6d8 100644 --- a/lib/core/config/app_message_actions_style.dart +++ b/lib/core/config/app_message_actions_style.dart @@ -9,7 +9,7 @@ class AppMessageActionsStyle { static final _setting = PersistedEnum( prefKey: prefKey, - defaultValue: MessageActionsStyle.radial, + defaultValue: MessageActionsStyle.list, encode: (value) => value.name, decode: _parse, ); @@ -21,7 +21,7 @@ class AppMessageActionsStyle { static Future save(MessageActionsStyle style) => _setting.save(style); static MessageActionsStyle _parse(String? val) => - enumFromName(MessageActionsStyle.values, val, MessageActionsStyle.radial); + enumFromName(MessageActionsStyle.values, val, MessageActionsStyle.list); static String label(MessageActionsStyle style) { switch (style) { diff --git a/lib/core/config/app_pill_gradient.dart b/lib/core/config/app_pill_gradient.dart index ff5e98b..d0b2967 100644 --- a/lib/core/config/app_pill_gradient.dart +++ b/lib/core/config/app_pill_gradient.dart @@ -7,7 +7,7 @@ class AppPillGradient { static final _setting = PersistedSetting( prefKey: prefKey, - defaultValue: true, + defaultValue: false, read: (prefs, key) => prefs.getBool(key), write: (prefs, key, value) async { await prefs.setBool(key, value); diff --git a/lib/core/config/app_visual_style.dart b/lib/core/config/app_visual_style.dart index ece7066..0404f67 100644 --- a/lib/core/config/app_visual_style.dart +++ b/lib/core/config/app_visual_style.dart @@ -9,7 +9,7 @@ class AppVisualStyle { static final _setting = PersistedEnum( prefKey: prefKey, - defaultValue: VisualStyle.materialYou, + defaultValue: VisualStyle.glossy, encode: _encode, decode: _parse, ); @@ -23,5 +23,5 @@ class AppVisualStyle { static String _encode(VisualStyle value) => value.name; static VisualStyle _parse(String? val) => - enumFromName(VisualStyle.values, val, VisualStyle.materialYou); + enumFromName(VisualStyle.values, val, VisualStyle.glossy); } diff --git a/lib/core/storage/app_database.dart b/lib/core/storage/app_database.dart index 1d50a80..9c03f5c 100644 --- a/lib/core/storage/app_database.dart +++ b/lib/core/storage/app_database.dart @@ -928,6 +928,56 @@ class AppDatabase { ); } + static Future>> loadMessagesBetween( + int accountId, + int chatId, { + required int afterTime, + required int beforeTime, + int limit = 60, + bool onlyVisible = false, + }) async { + final db = await _instance; + return db.query( + 'messages', + where: onlyVisible + ? 'account_id = ? AND chat_id = ? AND deleted = 0 ' + 'AND time > ? AND time < ?' + : 'account_id = ? AND chat_id = ? AND time > ? AND time < ?', + whereArgs: [accountId, chatId, afterTime, beforeTime], + orderBy: 'time ASC', + limit: limit, + ); + } + + static Future>> loadMessagesAround( + int accountId, + int chatId, { + required int centerTime, + int before = 40, + int after = 20, + bool onlyVisible = false, + }) async { + final db = await _instance; + final base = onlyVisible + ? 'account_id = ? AND chat_id = ? AND deleted = 0' + : 'account_id = ? AND chat_id = ?'; + final older = await db.query( + 'messages', + where: '$base AND time <= ?', + whereArgs: [accountId, chatId, centerTime], + orderBy: 'time DESC', + limit: before, + ); + final newer = await db.query( + 'messages', + where: '$base AND time > ?', + whereArgs: [accountId, chatId, centerTime], + orderBy: 'time ASC', + limit: after, + ); + return [...newer.reversed, ...older]; + } + static Future markMessageDeleted( int accountId, int chatId, diff --git a/lib/frontend/screens/chats/chat/chat_controller.dart b/lib/frontend/screens/chats/chat/chat_controller.dart index e91bc49..6b495bc 100644 --- a/lib/frontend/screens/chats/chat/chat_controller.dart +++ b/lib/frontend/screens/chats/chat/chat_controller.dart @@ -10,9 +10,25 @@ import '../../../../core/storage/app_database.dart'; import '../../../../core/utils/logger.dart'; import '../../../../main.dart'; +class HistoryGap { + HistoryGap({ + required this.edgeId, + required this.edgeTime, + required this.tailTime, + }); + + String edgeId; + int edgeTime; + final int tailTime; +} + class ChatController extends ChangeNotifier { static const int historyPageSize = 30; static const int historyInitialLimit = 50; + static const int jumpWindowBefore = 40; + static const int jumpWindowAfter = 20; + static const int historyWalkPageSize = 200; + static const int gapPageSize = 60; int chatId = 0; int myId = 0; @@ -23,6 +39,11 @@ class ChatController extends ChangeNotifier { bool hasMoreHistory = true; bool isLoadingMore = false; bool historyKickedOff = false; + bool loadingGap = false; + + final List gaps = []; + + bool get hasGap => gaps.isNotEmpty; bool Function() isMounted = () => true; @@ -93,20 +114,179 @@ class ChatController extends ChangeNotifier { Future> loadOlderFromDb( int beforeTime, - bool onlyVisible, - ) async { + bool onlyVisible, { + int? limit, + }) async { final rows = await AppDatabase.loadMessagesBefore( myId, chatId, beforeTime: beforeTime, - limit: historyPageSize, + limit: limit ?? historyPageSize, onlyVisible: onlyVisible, ); return CachedMessage.fromDbRowsAsync(rows); } + Future> loadGapSliceFromDb( + int afterTime, + int beforeTime, + bool onlyVisible, + ) async { + final rows = await AppDatabase.loadMessagesBetween( + myId, + chatId, + afterTime: afterTime, + beforeTime: beforeTime, + limit: gapPageSize, + onlyVisible: onlyVisible, + ); + return CachedMessage.fromDbRowsAsync(rows); + } + + Future> loadWindowFromDb( + int centerTime, + bool onlyVisible, + ) async { + final rows = await AppDatabase.loadMessagesAround( + myId, + chatId, + centerTime: centerTime, + before: jumpWindowBefore, + after: jumpWindowAfter, + onlyVisible: onlyVisible, + ); + return CachedMessage.fromDbRowsAsync(rows); + } + + Future loadMessageWindow({ + required String targetId, + required int targetTime, + }) async { + if (myId == 0 || targetTime <= 0) return false; + final onlyVisible = !KometSettings.viewDeleted.value; + + var window = await loadWindowFromDb(targetTime, onlyVisible); + if (!isMounted()) return false; + + if (!window.any((m) => m.id == targetId)) { + final fetched = await messagesModule.fetchHistory( + myId, + chatId, + fromTime: targetTime + 1, + forward: jumpWindowAfter, + backward: jumpWindowBefore + 1, + ); + if (!isMounted()) return false; + if (fetched.isNotEmpty && KometSettings.viewDeleted.value) { + await chats.reconcileDeletedFromFetch(myId, chatId, fetched); + } + window = await loadWindowFromDb(targetTime, onlyVisible); + if (!isMounted()) return false; + } + + if (window.isEmpty) return false; + + final oldestLoaded = messages.isEmpty ? 0 : messages.first.time; + final reachesLoaded = + messages.isEmpty || window.any((m) => m.time >= oldestLoaded); + + mergeMessages(window); + + if (reachesLoaded) { + persistSessionCache(); + } else { + _markGapAfterWindow(window); + } + return messages.any((m) => m.id == targetId); + } + + void _markGapAfterWindow(List window) { + var edge = window.first; + for (final m in window) { + if (m.time > edge.time) edge = m; + } + final idx = messages.indexWhere((m) => m.id == edge.id); + if (idx == -1 || idx + 1 >= messages.length) return; + final tailTime = messages[idx + 1].time; + gaps.removeWhere((g) => g.tailTime == tailTime); + gaps.add( + HistoryGap(edgeId: edge.id, edgeTime: edge.time, tailTime: tailTime), + ); + } + + void _closeGap(HistoryGap gap) { + gaps.remove(gap); + if (gaps.isEmpty) persistSessionCache(); + } + + Future fillGapForward(HistoryGap gap) async { + if (loadingGap || myId == 0 || !gaps.contains(gap)) return 0; + if (gap.edgeTime <= 0 || gap.tailTime <= gap.edgeTime) { + _closeGap(gap); + return 0; + } + + loadingGap = true; + try { + final onlyVisible = !KometSettings.viewDeleted.value; + var slice = await loadGapSliceFromDb( + gap.edgeTime, + gap.tailTime, + onlyVisible, + ); + if (!isMounted()) return 0; + + if (slice.length < gapPageSize) { + final fetched = await messagesModule.fetchHistory( + myId, + chatId, + fromTime: gap.edgeTime, + forward: gapPageSize, + backward: 0, + ); + if (!isMounted()) return 0; + if (fetched.isNotEmpty && KometSettings.viewDeleted.value) { + await chats.reconcileDeletedFromFetch(myId, chatId, fetched); + } + final refreshed = await loadGapSliceFromDb( + gap.edgeTime, + gap.tailTime, + onlyVisible, + ); + if (!isMounted()) return 0; + if (refreshed.length <= slice.length) { + if (refreshed.isNotEmpty) mergeMessages(refreshed); + _closeGap(gap); + return refreshed.length; + } + slice = refreshed; + } + + if (slice.isEmpty) { + _closeGap(gap); + return 0; + } + + mergeMessages(slice); + + var edge = slice.first; + for (final m in slice) { + if (m.time > edge.time) edge = m; + } + gap.edgeId = edge.id; + gap.edgeTime = edge.time; + if (edge.time >= gap.tailTime) _closeGap(gap); + return slice.length; + } catch (e) { + logger.e('Error filling history gap: $e'); + return 0; + } finally { + loadingGap = false; + } + } + void persistSessionCache() { - if (myId == 0 || messages.isEmpty) return; + if (myId == 0 || messages.isEmpty || hasGap) return; MessageSessionCache.save( myId, chatId, @@ -119,29 +299,32 @@ class ChatController extends ChangeNotifier { required void Function() onLoadingStarted, required void Function(int added) onLoaded, required void Function(Object error) onError, + int? pageSize, + bool persist = true, }) async { if (isLoadingMore || !hasMoreHistory || messages.isEmpty) return; isLoadingMore = true; onLoadingStarted(); + final size = pageSize ?? historyPageSize; final oldest = messages.first; final onlyVisible = !KometSettings.viewDeleted.value; try { - var older = await loadOlderFromDb(oldest.time, onlyVisible); + var older = await loadOlderFromDb(oldest.time, onlyVisible, limit: size); - if (older.length < historyPageSize) { + if (older.length < size) { final fetched = await messagesModule.fetchHistory( myId, chatId, fromTime: oldest.time, - count: historyPageSize, + count: size, ); if (fetched.isNotEmpty) { if (KometSettings.viewDeleted.value) { await chats.reconcileDeletedFromFetch(myId, chatId, fetched); } - older = await loadOlderFromDb(oldest.time, onlyVisible); + older = await loadOlderFromDb(oldest.time, onlyVisible, limit: size); } } @@ -149,7 +332,7 @@ class ChatController extends ChangeNotifier { final added = prependOlder(older); isLoadingMore = false; if (added == 0) hasMoreHistory = false; - persistSessionCache(); + if (persist) persistSessionCache(); onLoaded(added); } catch (e) { logger.e('Error loading more history: $e'); diff --git a/lib/frontend/screens/chats/chat/view/chat_header.dart b/lib/frontend/screens/chats/chat/view/chat_header.dart index 4fb57f3..74635a9 100644 --- a/lib/frontend/screens/chats/chat/view/chat_header.dart +++ b/lib/frontend/screens/chats/chat/view/chat_header.dart @@ -2,11 +2,14 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:material_symbols_icons/symbols.dart'; +import 'package:komet/core/config/app_frost.dart'; import 'package:komet/frontend/widgets/glossy_pill.dart'; import 'package:komet/frontend/widgets/online_dot.dart'; class ChatHeaderRow extends StatelessWidget { final bool glossy; + final bool frosted; + final BackdropKey? backdropKey; final ColorScheme cs; final bool embedded; final int chatId; @@ -28,6 +31,8 @@ class ChatHeaderRow extends StatelessWidget { const ChatHeaderRow({ super.key, required this.glossy, + required this.frosted, + this.backdropKey, required this.cs, required this.embedded, required this.chatId, @@ -51,6 +56,10 @@ class ChatHeaderRow extends StatelessWidget { Widget build(BuildContext context) => glossy ? _glossyRow(context) : _materialRow(context); + Color? get _pillColor => frosted ? AppFrost.pillTint(cs) : null; + + double? get _pillBlur => frosted ? AppFrost.sigma : null; + Widget _glossyRow(BuildContext context) { return Padding( padding: const EdgeInsets.fromLTRB(10, 4, 10, 8), @@ -62,6 +71,9 @@ class ChatHeaderRow extends StatelessWidget { width: 56, height: 56, child: GlossyPill( + color: _pillColor, + blurSigma: _pillBlur, + backdropKey: backdropKey, onTap: () { if (embedded) { onClose?.call(); @@ -83,6 +95,9 @@ class ChatHeaderRow extends StatelessWidget { const SizedBox(width: 8), Expanded( child: GlossyPill( + color: _pillColor, + blurSigma: _pillBlur, + backdropKey: backdropKey, onTap: onOpenInfo, padding: const EdgeInsets.fromLTRB(6, 6, 16, 6), child: Row( @@ -168,6 +183,9 @@ class ChatHeaderRow extends StatelessWidget { ), const SizedBox(width: 8), GlossyPill( + color: _pillColor, + blurSigma: _pillBlur, + backdropKey: backdropKey, padding: const EdgeInsets.symmetric(horizontal: 2), child: SizedBox( height: 56, diff --git a/lib/frontend/screens/chats/chat/view/composer_input.dart b/lib/frontend/screens/chats/chat/view/composer_input.dart index 4f2ee27..4086575 100644 --- a/lib/frontend/screens/chats/chat/view/composer_input.dart +++ b/lib/frontend/screens/chats/chat/view/composer_input.dart @@ -8,6 +8,9 @@ import 'package:material_symbols_icons/symbols.dart'; import 'package:komet/backend/modules/messages.dart'; import 'package:komet/core/config/app_chat_chrome.dart'; import 'package:komet/core/config/app_colors.dart'; +import 'package:komet/core/config/app_composer_background.dart'; +import 'package:komet/core/config/app_composer_style.dart'; +import 'package:komet/core/config/app_frost.dart'; import 'package:komet/frontend/screens/chats/chat/upload_status.dart'; import 'package:komet/frontend/screens/chats/chat/video_note_controller.dart'; import 'package:komet/frontend/screens/chats/chat/voice_record_controller.dart'; @@ -19,6 +22,9 @@ class ComposerInputBar extends StatelessWidget { super.key, required this.chatType, required this.chrome, + required this.style, + required this.background, + this.backdropKey, required this.attachAnim, required this.replyTo, required this.myId, @@ -43,6 +49,9 @@ class ComposerInputBar extends StatelessWidget { final String chatType; final ChatChromeStyle chrome; + final ComposerStyle style; + final ComposerBackground background; + final BackdropKey? backdropKey; final Animation attachAnim; final ValueListenable replyTo; final int myId; @@ -104,7 +113,7 @@ class ComposerInputBar extends StatelessWidget { ); } - return SafeArea( + final bar = SafeArea( child: Column( mainAxisSize: MainAxisSize.min, children: [ @@ -124,18 +133,9 @@ class ComposerInputBar extends StatelessWidget { minHeight: 54, maxHeight: 180, ), - child: GlossyPill( - color: Color.alphaBlend( - cs.surfaceContainerHighest.withValues(alpha: 0.92), - cs.surface, - ), - borderRadius: BorderRadius.circular(28), - depth: 8, - borderSide: BorderSide( - color: cs.outlineVariant.withValues(alpha: 0.5), - width: 0.5, - ), - child: Stack( + child: _fieldSurface( + cs, + Stack( alignment: Alignment.center, children: [ AnimatedBuilder( @@ -317,14 +317,16 @@ class ComposerInputBar extends StatelessWidget { builder: (context, videoMode, _) { final sendMode = hasText || locked; - final pill = GlossyPill( - color: sendMode + final pill = _actionSurface( + color: _flat + ? Colors.transparent + : sendMode ? cs.primary : recording ? cs.error + : _frost + ? AppFrost.inputTint(cs) : cs.surfaceContainerHighest, - borderRadius: - BorderRadius.circular(27), onTap: hasText ? onSendText : locked @@ -335,7 +337,6 @@ class ComposerInputBar extends StatelessWidget { onLongPress: hasText ? onScheduleMessage : null, - depth: 8, child: SizedBox( width: 54, height: 54, @@ -346,7 +347,13 @@ class ComposerInputBar extends StatelessWidget { : videoMode ? Symbols.videocam : Symbols.mic, - color: sendMode + color: _flat + ? (sendMode + ? cs.primary + : recording + ? cs.error + : cs.onSurfaceVariant) + : sendMode ? cs.onPrimary : recording ? cs.onError @@ -405,6 +412,72 @@ class ComposerInputBar extends StatelessWidget { ], ), ); + + return _barSurface(cs, bar); + } + + bool get _flat => style == ComposerStyle.materialYou; + + bool get _frost => background == ComposerBackground.frostBlur; + + Widget _barSurface(ColorScheme cs, Widget child) { + if (!_flat || background == ComposerBackground.frostBlur) return child; + return DecoratedBox( + decoration: BoxDecoration( + color: cs.surface, + border: Border(top: AppFrost.hairline(cs)), + ), + child: child, + ); + } + + Widget _fieldSurface(ColorScheme cs, Widget child) { + if (_flat) return child; + return GlossyPill( + color: _frost + ? AppFrost.inputTint(cs) + : Color.alphaBlend( + cs.surfaceContainerHighest.withValues(alpha: 0.92), + cs.surface, + ), + blurSigma: _frost ? AppFrost.sigma : null, + backdropKey: backdropKey, + borderRadius: BorderRadius.circular(28), + depth: 8, + borderSide: BorderSide( + color: cs.outlineVariant.withValues(alpha: 0.5), + width: 0.5, + ), + child: child, + ); + } + + Widget _actionSurface({ + required Color color, + required Widget child, + VoidCallback? onTap, + VoidCallback? onLongPress, + }) { + if (_flat) { + return Material( + color: color, + shape: const CircleBorder(), + clipBehavior: Clip.antiAlias, + child: onTap == null && onLongPress == null + ? child + : InkWell(onTap: onTap, onLongPress: onLongPress, child: child), + ); + } + return GlossyPill( + color: color, + blurSigma: _frost ? AppFrost.sigma : null, + backdropKey: backdropKey, + borderRadius: BorderRadius.circular(27), + onTap: onTap, + onLongPress: onLongPress, + depth: 8, + child: child, + ); } Widget _replyPreview(ColorScheme cs) { @@ -465,19 +538,19 @@ class ComposerInputBar extends StatelessWidget { ], ), ); - if (chrome != ChatChromeStyle.transparent) return row; + if (_flat && _frost) return row; + if (!_frost && chrome != ChatChromeStyle.transparent) return row; return ClipRect( child: BackdropFilter( - filter: ui.ImageFilter.blur(sigmaX: 34, sigmaY: 34), + filter: ui.ImageFilter.blur( + sigmaX: AppFrost.sigma, + sigmaY: AppFrost.sigma, + ), + backdropGroupKey: backdropKey, child: DecoratedBox( decoration: BoxDecoration( - color: cs.surface.withValues(alpha: 0.38), - border: Border( - top: BorderSide( - color: cs.outlineVariant.withValues(alpha: 0.4), - width: 0.5, - ), - ), + color: AppFrost.panelTint(cs), + border: Border(top: AppFrost.hairline(cs)), ), child: row, ), diff --git a/lib/frontend/screens/chats/chat_screen.dart b/lib/frontend/screens/chats/chat_screen.dart index 7890b13..c7de5be 100644 --- a/lib/frontend/screens/chats/chat_screen.dart +++ b/lib/frontend/screens/chats/chat_screen.dart @@ -15,7 +15,7 @@ import 'package:komet/backend/modules/upload_notification_service.dart'; import 'package:komet/core/media/gallery_source.dart'; import 'package:komet/core/utils/format.dart'; import 'package:komet/frontend/screens/chats/chat_info_screen.dart'; -import 'package:komet/frontend/screens/contacts/contact_profile_screen.dart'; +import 'package:komet/frontend/screens/contacts/open_contact_profile.dart'; import 'package:komet/frontend/screens/chats/chat_list_screen.dart'; import 'package:komet/frontend/screens/chats/poll_create_screen.dart'; import 'package:komet/frontend/widgets/animated_text_swap.dart'; @@ -68,6 +68,9 @@ import 'chat/view/shimmer_loading.dart'; import '../../../core/config/app_commands.dart'; import '../../../core/config/app_visual_style.dart'; import '../../../core/config/app_chat_chrome.dart'; +import 'package:komet/core/config/app_composer_background.dart'; +import 'package:komet/core/config/app_frost.dart'; +import 'package:komet/core/config/app_composer_style.dart'; import '../../../core/config/komet_settings.dart'; import '../../../models/attachment.dart'; import '../../../models/sticker.dart'; @@ -110,15 +113,24 @@ class _UnreadSeparatorItem { class _FrostedPanel extends StatelessWidget { final Color tint; final Border? border; + final double sigma; + final BackdropKey? backdropKey; final Widget child; - const _FrostedPanel({required this.tint, this.border, required this.child}); + const _FrostedPanel({ + required this.tint, + this.border, + this.sigma = AppFrost.panelSigma, + this.backdropKey, + required this.child, + }); @override Widget build(BuildContext context) { return ClipRect( child: BackdropFilter( - filter: ui.ImageFilter.blur(sigmaX: 24, sigmaY: 24), + filter: ui.ImageFilter.blur(sigmaX: sigma, sigmaY: sigma), + backdropGroupKey: backdropKey, child: DecoratedBox( decoration: BoxDecoration(color: tint, border: border), child: child, @@ -437,6 +449,9 @@ class _ChatScreenState extends State static const double _glossyHeaderHeight = 76.0; static const double _glossySearchHeight = 58.0; static const double _pinnedBannerLift = 6.0; + + final BackdropKey _barBackdrop = BackdropKey(); + final BackdropKey _pillBackdrop = BackdropKey(); bool get _isLoadingMore => _chatController.isLoadingMore; set _isLoadingMore(bool v) => _chatController.isLoadingMore = v; bool get _hasMoreHistory => _chatController.hasMoreHistory; @@ -450,6 +465,12 @@ class _ChatScreenState extends State bool _peerIsBot = false; ChatWallpaper? _wallpaper; + bool get _composerFrosted => + AppComposerBackground.current.value == ComposerBackground.frostBlur; + + bool get _composerUnderlap => + AppChatChrome.current.value != ChatChromeStyle.color || _composerFrosted; + ChatChromeStyle get _effectiveChrome { final chrome = AppChatChrome.current.value; if (_wallpaper != null && chrome == ChatChromeStyle.none) { @@ -510,6 +531,8 @@ class _ChatScreenState extends State _scrollController.addListener(_exitTextSelectionOnScroll); AppVisualStyle.current.addListener(_onVisualStyleChanged); AppChatChrome.current.addListener(_onVisualStyleChanged); + AppComposerStyle.current.addListener(_onVisualStyleChanged); + AppComposerBackground.current.addListener(_onVisualStyleChanged); _shimmerController = AnimationController( vsync: this, duration: const Duration(milliseconds: 1500), @@ -898,7 +921,7 @@ class _ChatScreenState extends State final ua = _unreadAnchorTime; return ua != null && _messages.indexWhere((m) => m.time > ua) > 0; }, - maxPages: 80, + maxPages: 15, ); if (!mounted) return; if (_unreadAnchorTime == null) _resolveCountBasedAnchor(); @@ -1161,27 +1184,14 @@ class _ChatScreenState extends State void _jumpToPinnedMessage() { final id = chat?.pinnedMsgId; - final time = chat?.pinnedMsgTime; if (id == null) return; - unawaited(_openPinnedMessage(id.toString(), time ?? 0)); - } - - Future _openPinnedMessage(String messageId, int time) async { - if (!_messages.any((m) => m.id == messageId)) { - await _walkHistoryBack( - reached: () => _messages.any((m) => m.id == messageId), - maxPages: 60, - targetTime: time, - ); - if (!mounted) return; - await WidgetsBinding.instance.endOfFrame; - if (!mounted) return; - } + final messageId = id.toString(); if (_messages.any((m) => m.id == messageId)) { _scrollToLoadedMessage(messageId); - } else { - showCustomNotification(context, 'Сообщение не загружено'); + return; } + setState(_beginTargetNavigation); + unawaited(_runGoToMessage(messageId, chat?.pinnedMsgTime ?? 0)); } bool _badgeRefreshing = false; @@ -1269,7 +1279,9 @@ class _ChatScreenState extends State void _maybeLoadMoreHistory() { if (!_scrollController.hasClients) return; if (_historyAutoloadSuppressed) return; - if (_isLoading || _isLoadingMore || !_hasMoreHistory) return; + if (_isLoading) return; + _maybeFillGap(); + if (_isLoadingMore || !_hasMoreHistory) return; if (_messages.isEmpty) return; final pos = _scrollController.position; if (pos.maxScrollExtent <= 0) return; @@ -1278,6 +1290,75 @@ class _ChatScreenState extends State } } + void _maybeFillGap() { + final controller = _chatController; + if (!controller.hasGap || controller.loadingGap) return; + for (final gap in controller.gaps) { + final box = _keyForMessage(gap.edgeId).currentContext?.findRenderObject(); + if (box is RenderBox && box.attached) { + unawaited(_fillGapForward(gap)); + return; + } + } + } + + Future _fillGapForward(HistoryGap gap) async { + final edgeId = gap.edgeId; + final beforeDy = _messageOffsetInList(edgeId); + final added = await _chatController.fillGapForward(gap); + if (!mounted || added == 0) return; + + _syncReactionNotifiersFromMessages(); + _bumpMessages(); + await WidgetsBinding.instance.endOfFrame; + if (!mounted || !_scrollController.hasClients) return; + + final afterDy = _messageOffsetInList(edgeId); + if (beforeDy != null && afterDy != null) { + final delta = beforeDy - afterDy; + if (delta.abs() > 0.5) { + final pos = _scrollController.position; + _scrollController.jumpTo( + (pos.pixels + delta).clamp(pos.minScrollExtent, pos.maxScrollExtent), + ); + } + } + _loadForwardedSenderNames(); + _loadGroupSenderNames(); + } + + double? _messageOffsetInList(String messageId) { + final listBox = _listKey.currentContext?.findRenderObject(); + final box = _keyForMessage(messageId).currentContext?.findRenderObject(); + if (listBox is! RenderBox || box is! RenderBox || !box.attached) return null; + return box.localToGlobal(Offset.zero, ancestor: listBox).dy; + } + + Future _loadMessageWindow(String messageId, int targetTime) async { + if (targetTime <= 0) { + await _walkHistoryBack( + reached: () => _messages.any((m) => m.id == messageId), + maxPages: 10, + ); + return; + } + + _historyAutoloadSuppressCount++; + try { + await _chatController.loadMessageWindow( + targetId: messageId, + targetTime: targetTime, + ); + } finally { + _historyAutoloadSuppressCount--; + } + if (!mounted) return; + _syncReactionNotifiersFromMessages(); + _bumpMessages(); + _loadForwardedSenderNames(); + _loadGroupSenderNames(); + } + Future _walkHistoryBack({ required bool Function() reached, required int maxPages, @@ -1294,7 +1375,11 @@ class _ChatScreenState extends State (_messages.isEmpty || _messages.first.time > targetTime)) { page++; final before = _messages.isEmpty ? 0 : _messages.first.time; - await _loadMoreHistory(resolveSenderNames: false); + await _loadMoreHistory( + resolveSenderNames: false, + pageSize: ChatController.historyWalkPageSize, + persist: false, + ); if (!mounted) return; final after = _messages.isEmpty ? 0 : _messages.first.time; if (after == before) break; @@ -1303,12 +1388,19 @@ class _ChatScreenState extends State _historyAutoloadSuppressCount--; } if (!mounted) return; + _chatController.persistSessionCache(); _loadForwardedSenderNames(); _loadGroupSenderNames(); } - Future _loadMoreHistory({bool resolveSenderNames = true}) async { + Future _loadMoreHistory({ + bool resolveSenderNames = true, + int? pageSize, + bool persist = true, + }) async { await _chatController.loadMoreHistory( + pageSize: pageSize, + persist: persist, onLoadingStarted: _bumpMessages, onLoaded: (added) { if (added > 0) _syncReactionNotifiersFromMessages(); @@ -1411,6 +1503,8 @@ class _ChatScreenState extends State _readMarkTimer?.cancel(); AppVisualStyle.current.removeListener(_onVisualStyleChanged); AppChatChrome.current.removeListener(_onVisualStyleChanged); + AppComposerStyle.current.removeListener(_onVisualStyleChanged); + AppComposerBackground.current.removeListener(_onVisualStyleChanged); _composerHeight.dispose(); _pinnedBannerHeight.dispose(); _floatingDateTimer?.cancel(); @@ -1970,6 +2064,9 @@ class _ChatScreenState extends State ComposerInputBar( chatType: widget.chatType, chrome: _effectiveChrome, + style: AppComposerStyle.current.value, + background: AppComposerBackground.current.value, + backdropKey: _pillBackdrop, attachAnim: _attachAnim, replyTo: _replyTo, myId: _myId, @@ -2028,15 +2125,23 @@ class _ChatScreenState extends State ], ); Widget wrapChrome(Widget child) { + if (_composerFrosted) { + if (AppComposerStyle.current.value != ComposerStyle.materialYou) { + return child; + } + return _FrostedPanel( + sigma: AppFrost.sigma, + tint: AppFrost.panelTint(cs), + border: Border(top: AppFrost.hairline(cs)), + backdropKey: _barBackdrop, + child: child, + ); + } if (_effectiveChrome != ChatChromeStyle.blur) return child; return _FrostedPanel( - tint: cs.surfaceContainerHigh.withValues(alpha: 0.55), - border: Border( - top: BorderSide( - color: cs.outlineVariant.withValues(alpha: 0.4), - width: 0.5, - ), - ), + tint: AppFrost.blurPanelTint(cs), + border: Border(top: AppFrost.hairline(cs)), + backdropKey: _barBackdrop, child: child, ); } @@ -2398,13 +2503,9 @@ class _ChatScreenState extends State : Colors.transparent, flexibleSpace: chrome == ChatChromeStyle.blur ? _FrostedPanel( - tint: cs.surfaceContainerHigh.withValues(alpha: 0.55), - border: Border( - bottom: BorderSide( - color: cs.outlineVariant.withValues(alpha: 0.4), - width: 0.5, - ), - ), + tint: AppFrost.blurPanelTint(cs), + border: Border(bottom: AppFrost.hairline(cs)), + backdropKey: _barBackdrop, child: const SizedBox.expand(), ) : (chrome == ChatChromeStyle.none && !glossy) @@ -2426,22 +2527,12 @@ class _ChatScreenState extends State ), ) : (chrome == ChatChromeStyle.transparent && !glossy) - ? IgnorePointer( - child: DecoratedBox( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - cs.surface.withValues(alpha: 0.72), - cs.surface.withValues(alpha: 0.45), - cs.surface.withValues(alpha: 0.0), - ], - stops: const [0.0, 0.62, 1.0], - ), - ), - child: const SizedBox.expand(), - ), + ? _FrostedPanel( + sigma: AppFrost.sigma, + tint: AppFrost.panelTint(cs), + border: Border(bottom: AppFrost.hairline(cs)), + backdropKey: _barBackdrop, + child: const SizedBox.expand(), ) : null, foregroundColor: cs.onSurface, @@ -2477,6 +2568,10 @@ class _ChatScreenState extends State offset: Offset(0, -height * 0.4 * t), child: ChatHeaderRow( glossy: glossy, + frosted: + glossy && + chrome == ChatChromeStyle.transparent, + backdropKey: _pillBackdrop, cs: cs, embedded: widget.embedded, chatId: widget.chatId, @@ -3482,14 +3577,12 @@ class _ChatScreenState extends State void _openSenderProfile(int senderId) { if (senderId == 0 || senderId == _myId) return; - Navigator.push( - context, - MaterialPageRoute( - builder: (_) => ContactProfileScreen( - contactId: senderId, - initialName: ContactCache.get(senderId), - initialAvatarUrl: ContactCache.getAvatar(senderId), - ), + unawaited( + openContactDialogProfile( + context, + contactId: senderId, + name: ContactCache.get(senderId) ?? 'User #$senderId', + avatarUrl: ContactCache.getAvatar(senderId), ), ); } @@ -3570,11 +3663,7 @@ class _ChatScreenState extends State if (!mounted) return; if (!_messages.any((m) => m.id == id)) { - await _walkHistoryBack( - reached: () => _messages.any((m) => m.id == id), - maxPages: 80, - targetTime: targetTime, - ); + await _loadMessageWindow(id, targetTime); if (!mounted) return; await WidgetsBinding.instance.endOfFrame; if (!mounted) return; @@ -3684,29 +3773,14 @@ class _ChatScreenState extends State Future _openSearchResult(MessageSearchResult result) async { _closeSearch(); - await WidgetsBinding.instance.endOfFrame; - if (!mounted) return; - - if (!_messages.any((m) => m.id == result.id)) { - var guard = 0; - while (mounted && - guard < 60 && - _hasMoreHistory && - !_messages.any((m) => m.id == result.id) && - (_messages.isEmpty || _messages.first.time > result.time)) { - guard++; - final before = _messages.isEmpty ? 0 : _messages.first.time; - await _loadMoreHistory(); - if (!mounted) return; - final after = _messages.isEmpty ? 0 : _messages.first.time; - if (after == before) break; - } - if (!mounted) return; + if (_messages.any((m) => m.id == result.id)) { await WidgetsBinding.instance.endOfFrame; if (!mounted) return; + _scrollToLoadedMessage(result.id); + return; } - - _scrollToLoadedMessage(result.id); + setState(_beginTargetNavigation); + await _runGoToMessage(result.id, result.time); } void _scrollToLoadedMessage( @@ -4129,6 +4203,8 @@ class _ChatScreenState extends State text: pinned.pinnedMsgText, isPreview: pinned.pinnedMsgIsPreview, floating: floating, + frosted: _effectiveChrome == ChatChromeStyle.transparent, + backdropKey: _pillBackdrop, onTap: _jumpToPinnedMessage, onUnpin: pinned.canPinMessages(_myId) ? () => unawaited(_unpinCurrentMessage()) @@ -4139,6 +4215,11 @@ class _ChatScreenState extends State Widget _buildColorBody() { final cs = Theme.of(context).colorScheme; final banner = _buildPinnedBanner(floating: false); + final frosted = _composerFrosted; + final composer = _MeasureSize( + onHeight: (value) => _composerHeight.value = value, + child: _buildComposerArea(context), + ); return Column( children: [ ?banner, @@ -4151,12 +4232,17 @@ class _ChatScreenState extends State child: ChatWallpaperView(wallpaper: _wallpaper!), ), Positioned.fill(child: _buildMessagesArea()), - Positioned( - left: 0, - right: 0, - bottom: 0, - child: CommandPanelView(commandPanel: _commandPanel), + ValueListenableBuilder( + valueListenable: _composerHeight, + builder: (context, height, _) => Positioned( + left: 0, + right: 0, + bottom: frosted ? height : 0, + child: CommandPanelView(commandPanel: _commandPanel), + ), ), + if (frosted) + Positioned(left: 0, right: 0, bottom: 0, child: composer), SearchOverlay( cs: cs, searchAnim: _searchAnim, @@ -4168,7 +4254,7 @@ class _ChatScreenState extends State ], ), ), - _buildComposerArea(context), + if (!frosted) composer, ], ); } @@ -4391,13 +4477,8 @@ class _ChatScreenState extends State if (index == 0) { return ValueListenableBuilder( valueListenable: _composerHeight, - builder: (context, height, _) => SizedBox( - height: - AppChatChrome.current.value == - ChatChromeStyle.color - ? 0 - : height, - ), + builder: (context, height, _) => + SizedBox(height: _composerUnderlap ? height : 0), ); } if (index > items.length) { @@ -5609,6 +5690,8 @@ class _PinnedMessageBanner extends StatelessWidget { final VoidCallback onTap; final VoidCallback? onUnpin; final bool floating; + final bool frosted; + final BackdropKey? backdropKey; const _PinnedMessageBanner({ required this.text, @@ -5616,13 +5699,17 @@ class _PinnedMessageBanner extends StatelessWidget { required this.onTap, this.onUnpin, this.floating = false, + this.frosted = false, + this.backdropKey, }); @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final content = Material( - color: floating + color: frosted + ? AppFrost.panelTint(cs) + : floating ? cs.surfaceContainerHigh.withValues(alpha: 0.92) : cs.surfaceContainerHigh, borderRadius: floating ? BorderRadius.circular(16) : null, @@ -5679,16 +5766,32 @@ class _PinnedMessageBanner extends StatelessWidget { ), ); - if (!floating) { - return DecoratedBox( - decoration: BoxDecoration( - border: Border( - bottom: BorderSide( - color: cs.outlineVariant.withValues(alpha: 0.4), - width: 0.5, + final bottomBorder = Border(bottom: AppFrost.hairline(cs)); + + if (frosted) { + return ClipRRect( + borderRadius: floating + ? BorderRadius.circular(16) + : BorderRadius.zero, + child: BackdropFilter( + filter: ui.ImageFilter.blur( + sigmaX: AppFrost.sigma, + sigmaY: AppFrost.sigma, + ), + backdropGroupKey: backdropKey, + child: DecoratedBox( + decoration: BoxDecoration( + border: floating ? null : bottomBorder, ), + child: content, ), ), + ); + } + + if (!floating) { + return DecoratedBox( + decoration: BoxDecoration(border: bottomBorder), child: content, ); } diff --git a/lib/frontend/screens/chats/search_screen.dart b/lib/frontend/screens/chats/search_screen.dart index 90c9683..ac9c317 100644 --- a/lib/frontend/screens/chats/search_screen.dart +++ b/lib/frontend/screens/chats/search_screen.dart @@ -11,7 +11,7 @@ import '../../../core/utils/debouncer.dart'; import '../../../core/utils/names.dart'; import '../../widgets/komet_avatar.dart'; import '../../widgets/swipe_route.dart'; -import '../contacts/contact_profile_screen.dart'; +import '../contacts/open_contact_profile.dart'; import 'chat_screen.dart'; class SearchScreen extends StatefulWidget { @@ -147,13 +147,12 @@ class _SearchScreenState extends State { } void _openContact(Map row) { - Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => ContactProfileScreen( - contactId: row['id'] as int, - initialName: _contactName(row), - initialAvatarUrl: row['base_url'] as String?, - ), + unawaited( + openContactDialogProfile( + context, + contactId: row['id'] as int, + name: _contactName(row), + avatarUrl: row['base_url'] as String?, ), ); } @@ -166,13 +165,12 @@ class _SearchScreenState extends State { } void _openPhoneResult(PhoneLookupResult result) { - Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => ContactProfileScreen( - contactId: result.id, - initialName: result.name, - initialAvatarUrl: result.avatarUrl, - ), + unawaited( + openContactDialogProfile( + context, + contactId: result.id, + name: result.name ?? 'User #${result.id}', + avatarUrl: result.avatarUrl, ), ); } diff --git a/lib/frontend/screens/contacts/contact_profile_screen.dart b/lib/frontend/screens/contacts/contact_profile_screen.dart deleted file mode 100644 index d853608..0000000 --- a/lib/frontend/screens/contacts/contact_profile_screen.dart +++ /dev/null @@ -1,420 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:material_symbols_icons/symbols.dart'; - -import '../../../core/cache/info_cache.dart'; -import '../../../core/storage/app_database.dart'; -import '../../../core/storage/token_storage.dart'; -import '../../../core/utils/format.dart'; -import '../../../l10n/app_localizations.dart'; -import '../../../models/contact_info.dart'; -import '../../widgets/avatar_history_screen.dart'; -import '../../widgets/custom_notification.dart'; -import '../../widgets/glossy_pill.dart'; -import '../../widgets/komet_avatar.dart'; -import '../../widgets/connection_status.dart'; -import '../../widgets/swipe_route.dart'; -import '../chats/chat_screen.dart'; - -class ContactProfileScreen extends StatefulWidget { - final int contactId; - final String? initialName; - final String? initialAvatarUrl; - - const ContactProfileScreen({ - super.key, - required this.contactId, - this.initialName, - this.initialAvatarUrl, - }); - - @override - State createState() => _ContactProfileScreenState(); -} - -class _ContactProfileScreenState extends State { - bool _loading = true; - ContactInfo? _contact; - int? _seenTime; - int _presenceStatus = 0; - - @override - void initState() { - super.initState(); - _load(); - } - - Future _load() async { - try { - final contactFuture = ContactInfoFetch.get(widget.contactId); - final presenceFuture = PresenceFetch.get(widget.contactId); - final contact = await contactFuture; - final presence = await presenceFuture; - if (!mounted) return; - if (contact != null) { - _contact = contact; - } - if (presence != null) { - _seenTime = presence['seen'] as int?; - _presenceStatus = (presence['status'] as int?) ?? 0; - } - } catch (e) { - if (mounted) { - showCustomNotification( - context, - AppLocalizations.of(context)!.contactProfileLoadError(e.toString()), - ); - } - } finally { - if (mounted) setState(() => _loading = false); - } - } - - String _displayName() { - return _contact?.displayName ?? - widget.initialName ?? - 'User #${widget.contactId}'; - } - - String? _avatarUrl() { - return _contact?.avatarUrl ?? widget.initialAvatarUrl; - } - - Set _options() { - return _contact?.options.toSet() ?? const {}; - } - - bool get _isBot => _options().contains('BOT'); - bool get _isVerified => _options().contains('OFFICIAL'); - - String _subtitle() { - final l10n = AppLocalizations.of(context)!; - if (_isBot) return l10n.contactProfileBot; - if (_presenceStatus == 1) return l10n.contactProfileOnline; - if (_presenceStatus == 2 || _presenceStatus == 3) return l10n.contactProfileRecentlyActive; - if (_seenTime != null && _seenTime! > 0) return formatLastSeen(_seenTime!); - return ''; - } - - Future _openChat() async { - final accountId = await TokenStorage.getActiveAccountId(); - if (accountId == null) return; - final existing = await AppDatabase.findDialogChatByParticipant( - accountId, - widget.contactId, - ); - final chatId = existing ?? (accountId ^ widget.contactId); - if (!mounted) return; - pushSwipeable( - context, - (_) => ChatScreen( - chatId: chatId, - name: _displayName(), - imageUrl: _avatarUrl() ?? '', - chatType: 'DIALOG', - ), - ); - } - - @override - Widget build(BuildContext context) { - final cs = Theme.of(context).colorScheme; - return Scaffold( - backgroundColor: cs.surface, - floatingActionButtonLocation: FloatingActionButtonLocation.startFloat, - floatingActionButton: const ConnectionSpinner(), - body: SafeArea( - child: _loading - ? const Center(child: CircularProgressIndicator()) - : _buildBody(cs), - ), - ); - } - - Widget _buildBody(ColorScheme cs) { - return CustomScrollView( - slivers: [ - SliverAppBar( - backgroundColor: Colors.transparent, - elevation: 0, - floating: true, - leading: IconButton( - icon: Icon(Symbols.arrow_back, color: cs.onSurface), - onPressed: () => Navigator.pop(context), - ), - ), - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Column( - children: [ - GestureDetector( - onTap: () => AvatarHistoryScreen.open( - context, - contactId: widget.contactId, - name: _displayName(), - currentAvatarUrl: _avatarUrl(), - ), - child: KometAvatar( - name: _displayName(), - imageUrl: _avatarUrl(), - size: 96, - fontSize: 36, - ), - ), - const SizedBox(height: 14), - _buildNameRow(cs), - const SizedBox(height: 4), - Text( - _subtitle(), - style: TextStyle(color: cs.onSurfaceVariant, fontSize: 14), - ), - const SizedBox(height: 20), - _buildActions(cs), - const SizedBox(height: 16), - _buildInfoCard(cs), - const SizedBox(height: 40), - ], - ), - ), - ), - ], - ); - } - - Widget _buildNameRow(ColorScheme cs) { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - Flexible( - child: Text( - _displayName(), - style: TextStyle( - color: cs.onSurface, - fontSize: 22, - fontWeight: FontWeight.w700, - ), - textAlign: TextAlign.center, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ), - if (_isVerified) ...[ - const SizedBox(width: 6), - Icon(Symbols.verified, color: cs.primary, size: 20, fill: 1), - ], - ], - ); - } - - Widget _buildActions(ColorScheme cs) { - final l10n = AppLocalizations.of(context)!; - final actions = <({IconData icon, String label, VoidCallback? onTap})>[ - ( - icon: Symbols.chat_bubble, - label: l10n.contactProfileActionChat, - onTap: _openChat, - ), - ( - icon: Symbols.notifications, - label: l10n.contactProfileActionSound, - onTap: null, - ), - if (!_isBot) - (icon: Symbols.call, label: l10n.contactProfileActionCall, onTap: null), - ]; - return Row( - children: [ - for (var i = 0; i < actions.length; i++) ...[ - Expanded( - child: GestureDetector( - onTap: actions[i].onTap, - child: GlossyPill( - color: cs.surfaceContainerHigh, - borderRadius: BorderRadius.circular(14), - padding: const EdgeInsets.symmetric(vertical: 12), - depth: 6, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(actions[i].icon, color: cs.primary, size: 22), - const SizedBox(height: 4), - Text( - actions[i].label, - style: TextStyle(color: cs.onSurface, fontSize: 12), - ), - ], - ), - ), - ), - ), - if (i < actions.length - 1) const SizedBox(width: 8), - ], - ], - ); - } - - Widget _buildInfoCard(ColorScheme cs) { - final c = _contact; - if (c == null) return const SizedBox.shrink(); - - final l10n = AppLocalizations.of(context)!; - final rows = []; - - final phoneStr = formatPhone(c.raw['phone']); - if (phoneStr != null) { - rows.add( - _infoRow(cs, Symbols.phone, l10n.contactProfileInfoPhone, phoneStr), - ); - } - - final country = c.raw['country'] as String?; - if (country != null && country.isNotEmpty) { - rows.add( - _infoRow(cs, Symbols.public, l10n.contactProfileInfoCountry, country), - ); - } - - final genderStr = formatGender(c.raw['gender']); - if (genderStr != null) { - rows.add( - _infoRow(cs, Symbols.wc, l10n.contactProfileInfoGender, genderStr), - ); - } - - final regTime = c.raw['registrationTime'] as int?; - if (regTime != null && regTime > 0) { - rows.add( - _infoRow( - cs, - Symbols.event, - l10n.contactProfileInfoRegistration, - formatDateTimeWords(DateTime.fromMillisecondsSinceEpoch(regTime)), - ), - ); - } - - final updateTime = c.raw['updateTime'] as int?; - if (updateTime != null && updateTime > 0) { - rows.add( - _infoRow( - cs, - Symbols.update, - l10n.contactProfileInfoUpdated, - formatDateTimeWords(DateTime.fromMillisecondsSinceEpoch(updateTime)), - ), - ); - } - - final accountStatus = c.raw['accountStatus']; - if (accountStatus is int && accountStatus != 0) { - rows.add( - _infoRow( - cs, - Symbols.account_circle, - l10n.contactProfileInfoAccountStatus, - accountStatus.toString(), - ), - ); - } - - final desc = (c.raw['description'] as String?)?.trim(); - if (desc != null && desc.isNotEmpty) { - rows.add( - _infoRow( - cs, - Symbols.info, - l10n.contactProfileInfoDescription, - desc, - multiline: true, - ), - ); - } - - final link = c.raw['link'] as String?; - if (link != null && link.isNotEmpty) { - rows.add(_infoRow(cs, Symbols.link, l10n.contactProfileInfoLink, link)); - } - - final webApp = c.raw['webApp'] as String?; - if (webApp != null && webApp.isNotEmpty) { - rows.add(_infoRow(cs, Symbols.web, 'Web app', webApp)); - } - - final opts = _options(); - if (opts.isNotEmpty) { - rows.add( - _infoRow( - cs, - Symbols.label, - l10n.contactProfileInfoFlags, - opts.join(', '), - multiline: true, - ), - ); - } - - rows.add(_infoRow(cs, Symbols.tag, 'ID', widget.contactId.toString())); - - if (rows.isEmpty) return const SizedBox.shrink(); - - return GlossyPill( - color: cs.surfaceContainerHigh, - borderRadius: BorderRadius.circular(20), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - depth: 6, - child: SizedBox( - width: double.infinity, - child: Column( - children: [ - for (var i = 0; i < rows.length; i++) ...[ - if (i > 0) - Divider( - height: 1, - color: cs.outlineVariant.withValues(alpha: 0.3), - ), - rows[i], - ], - ], - ), - ), - ); - } - - Widget _infoRow( - ColorScheme cs, - IconData icon, - String label, - String value, { - bool multiline = false, - }) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 10), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Icon(icon, color: cs.onSurfaceVariant, size: 20), - const SizedBox(width: 14), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - label, - style: TextStyle(color: cs.onSurfaceVariant, fontSize: 12), - ), - const SizedBox(height: 2), - Text( - value, - style: TextStyle(color: cs.onSurface, fontSize: 14), - maxLines: multiline ? null : 1, - overflow: multiline ? null : TextOverflow.ellipsis, - ), - ], - ), - ), - ], - ), - ); - } -} diff --git a/lib/frontend/screens/contacts/contacts_tab.dart b/lib/frontend/screens/contacts/contacts_tab.dart index 694dcb2..edb5a1b 100644 --- a/lib/frontend/screens/contacts/contacts_tab.dart +++ b/lib/frontend/screens/contacts/contacts_tab.dart @@ -13,32 +13,7 @@ import '../../widgets/connection_status.dart'; import '../../widgets/sheet_helpers.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, - ), - ), - ); -} +import 'open_contact_profile.dart'; class ContactsTab extends StatefulWidget { const ContactsTab({super.key}); diff --git a/lib/frontend/screens/contacts/open_contact_profile.dart b/lib/frontend/screens/contacts/open_contact_profile.dart new file mode 100644 index 0000000..e93a707 --- /dev/null +++ b/lib/frontend/screens/contacts/open_contact_profile.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; + +import '../../../core/storage/app_database.dart'; +import '../../../core/storage/token_storage.dart'; +import '../chats/chat_info_screen.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, + ), + ), + ); +} diff --git a/lib/frontend/screens/profile/appearance_screen.dart b/lib/frontend/screens/profile/appearance_screen.dart index 3c19088..8f78c1d 100644 --- a/lib/frontend/screens/profile/appearance_screen.dart +++ b/lib/frontend/screens/profile/appearance_screen.dart @@ -10,6 +10,8 @@ import '../../../core/config/app_bubble_shape.dart'; import '../../../core/config/app_pill_gradient.dart'; import '../../../core/config/app_visual_style.dart'; import '../../../core/config/app_chat_chrome.dart'; +import '../../../core/config/app_composer_background.dart'; +import '../../../core/config/app_composer_style.dart'; import '../../../core/utils/bubble_radius.dart'; import '../../../core/utils/debouncer.dart'; import '../../../core/utils/haptics.dart'; @@ -119,6 +121,8 @@ class _AppearanceScreenState extends State { const SizedBox(height: 12), const _ChatChromeCard(), const SizedBox(height: 12), + const _ComposerBarCard(), + const SizedBox(height: 12), const _GradientToggleCard(), ], ), @@ -253,6 +257,90 @@ class _ChatChromeCard extends StatelessWidget { } } +class _ComposerBarCard extends StatelessWidget { + const _ComposerBarCard(); + + @override + Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; + final l10n = AppLocalizations.of(context)!; + return GlossyPill( + color: cs.surfaceContainerHigh, + borderRadius: BorderRadius.circular(28), + padding: const EdgeInsets.fromLTRB(20, 18, 20, 20), + depth: 6, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + l10n.appearanceComposerTitle, + style: TextStyle( + color: cs.onSurface, + fontSize: 16, + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(height: 4), + Text( + l10n.appearanceComposerSubtitle, + style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13), + ), + const SizedBox(height: 16), + ValueListenableBuilder( + valueListenable: AppComposerStyle.current, + builder: (context, current, _) { + return SegmentedButton( + segments: [ + ButtonSegment( + value: ComposerStyle.glossy, + label: Text(l10n.appearanceVisualStyleGlossy), + ), + ButtonSegment( + value: ComposerStyle.materialYou, + label: Text(l10n.appearanceVisualStyleMaterialYou), + ), + ], + selected: {current}, + onSelectionChanged: (set) { + if (set.isNotEmpty) { + Haptics.selection(); + AppComposerStyle.save(set.first); + } + }, + ); + }, + ), + const SizedBox(height: 10), + ValueListenableBuilder( + valueListenable: AppComposerBackground.current, + builder: (context, current, _) { + return SegmentedButton( + segments: [ + ButtonSegment( + value: ComposerBackground.standard, + label: Text(l10n.appearanceComposerBackgroundStandard), + ), + ButtonSegment( + value: ComposerBackground.frostBlur, + label: Text(l10n.appearanceComposerBackgroundFrost), + ), + ], + selected: {current}, + onSelectionChanged: (set) { + if (set.isNotEmpty) { + Haptics.selection(); + AppComposerBackground.save(set.first); + } + }, + ); + }, + ), + ], + ), + ); + } +} + class _GradientToggleCard extends StatelessWidget { const _GradientToggleCard(); diff --git a/lib/frontend/screens/profile/customization_section.dart b/lib/frontend/screens/profile/customization_section.dart index 1073de2..b0aac73 100644 --- a/lib/frontend/screens/profile/customization_section.dart +++ b/lib/frontend/screens/profile/customization_section.dart @@ -40,7 +40,7 @@ class _CustomizationSectionState extends State { builder: (context) => const ThemeSettingsScreen(), ), _CustomizationCategory( - icon: Symbols.palette, + icon: Symbols.styler, title: 'Внешний вид', builder: (context) => const AppearanceScreen(), ), diff --git a/lib/frontend/widgets/glossy_pill.dart b/lib/frontend/widgets/glossy_pill.dart index 6a1b513..1d90a15 100644 --- a/lib/frontend/widgets/glossy_pill.dart +++ b/lib/frontend/widgets/glossy_pill.dart @@ -1,3 +1,5 @@ +import 'dart:ui' as ui; + import 'package:flutter/material.dart'; import '../../core/config/app_pill_gradient.dart'; @@ -90,6 +92,8 @@ class GlossyPill extends StatelessWidget { final double depth; final bool elevated; final BorderSide? borderSide; + final double? blurSigma; + final BackdropKey? backdropKey; const GlossyPill({ super.key, @@ -102,9 +106,14 @@ class GlossyPill extends StatelessWidget { this.depth = 10, this.elevated = false, this.borderSide, + this.blurSigma, + this.backdropKey, }) : borderRadius = borderRadius ?? const BorderRadius.all(Radius.circular(100)); + double? _sigmaFor(Color base) => + blurSigma != null && base.a < 1 ? blurSigma : null; + @override Widget build(BuildContext context) { return ValueListenableBuilder( @@ -123,7 +132,7 @@ class GlossyPill extends StatelessWidget { final cs = Theme.of(context).colorScheme; final base = color ?? cs.surfaceContainerHigh; final content = Padding(padding: padding, child: child); - return Material( + final material = Material( color: base, elevation: elevated ? 3 : 0, shadowColor: Colors.black.withValues(alpha: 0.4), @@ -137,12 +146,23 @@ class GlossyPill extends StatelessWidget { ? content : InkWell(onTap: onTap, onLongPress: onLongPress, child: content), ); + final sigma = _sigmaFor(base); + if (sigma == null) return material; + return ClipRRect( + borderRadius: borderRadius, + child: BackdropFilter( + filter: ui.ImageFilter.blur(sigmaX: sigma, sigmaY: sigma), + backdropGroupKey: backdropKey, + child: material, + ), + ); } Widget _glossy(BuildContext context, bool gradient) { final cs = Theme.of(context).colorScheme; final base = color ?? cs.surfaceContainerHigh; final content = Padding(padding: padding, child: child); + final sigma = _sigmaFor(base); return RepaintBoundary( child: DecoratedBox( @@ -158,6 +178,14 @@ class GlossyPill extends StatelessWidget { child: Stack( fit: StackFit.passthrough, children: [ + if (sigma != null) + Positioned.fill( + child: BackdropFilter( + filter: ui.ImageFilter.blur(sigmaX: sigma, sigmaY: sigma), + backdropGroupKey: backdropKey, + child: const SizedBox.expand(), + ), + ), if (gradient) ...[ Positioned.fill( child: IgnorePointer( diff --git a/lib/frontend/widgets/max_link_handler.dart b/lib/frontend/widgets/max_link_handler.dart index 4fe7346..477cdc9 100644 --- a/lib/frontend/widgets/max_link_handler.dart +++ b/lib/frontend/widgets/max_link_handler.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import '../../backend/modules/chats.dart'; @@ -6,7 +8,7 @@ import '../../core/links/max_link.dart'; import '../../core/storage/app_database.dart'; import '../../main.dart'; import '../screens/chats/chat_screen.dart'; -import '../screens/contacts/contact_profile_screen.dart'; +import '../screens/contacts/open_contact_profile.dart'; import 'call_link_handler.dart'; import 'confirm_dialog.dart'; import 'custom_notification.dart'; @@ -75,13 +77,12 @@ void _openContact(BuildContext context, Map contact) { showCustomNotification(context, 'Не удалось открыть профиль'); return; } - Navigator.of(context).push( - MaterialPageRoute( - builder: (_) => ContactProfileScreen( - contactId: id, - initialName: _contactName(contact), - initialAvatarUrl: contact['baseUrl'] as String?, - ), + unawaited( + openContactDialogProfile( + context, + contactId: id, + name: _contactName(contact), + avatarUrl: contact['baseUrl'] as String?, ), ); } diff --git a/lib/frontend/widgets/message_actions_overlay.dart b/lib/frontend/widgets/message_actions_overlay.dart index 5dda793..8a7b7bc 100644 --- a/lib/frontend/widgets/message_actions_overlay.dart +++ b/lib/frontend/widgets/message_actions_overlay.dart @@ -494,19 +494,19 @@ class _MessageActionsLayerState extends State<_MessageActionsLayer> final hasText = widget.messageText != null && widget.messageText!.isNotEmpty; return <_Action>[ + if (widget.onReply != null) + _Action(Symbols.reply, l10n.msgActionsReply, _reply), + if (widget.onForward != null) + _Action(Symbols.forward, l10n.msgActionsForward, _forward), if (hasText) _Action(Symbols.content_copy, l10n.msgActionsCopy, _copy), if (widget.isMe && widget.onEdit != null) _Action(Symbols.edit, l10n.msgActionsEdit, _edit), - if (widget.onReply != null) - _Action(Symbols.reply, l10n.msgActionsReply, _reply), if (widget.onPin != null) _Action( widget.isPinned ? Symbols.keep_off : Symbols.push_pin, widget.isPinned ? l10n.msgActionsUnpin : l10n.msgActionsPin, _pin, ), - if (widget.onForward != null) - _Action(Symbols.forward, l10n.msgActionsForward, _forward), if (widget.onMarkUnread != null) _Action( Symbols.mark_chat_unread, diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index e973007..ed95676 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -295,7 +295,11 @@ "appearanceChatChromeColor": "Color", "appearanceChatChromeBlur": "Blur", "appearanceChatChromeNone": "None", - "appearanceChatChromeTransparent": "Clear", + "appearanceChatChromeTransparent": "Frost blur", + "appearanceComposerTitle": "Input bar", + "appearanceComposerSubtitle": "Style and background of the message input bar", + "appearanceComposerBackgroundStandard": "Default", + "appearanceComposerBackgroundFrost": "Frost blur", "appearanceGradientTitle": "Gradient", "appearanceGradientSubtitle": "Depth and highlights in Glossy capsules", "appearanceAccentColorTitle": "Accent color", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index a7d2884..e9d73f9 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -1559,9 +1559,33 @@ abstract class AppLocalizations { /// No description provided for @appearanceChatChromeTransparent. /// /// In en, this message translates to: - /// **'Clear'** + /// **'Frost blur'** String get appearanceChatChromeTransparent; + /// No description provided for @appearanceComposerTitle. + /// + /// In en, this message translates to: + /// **'Input bar'** + String get appearanceComposerTitle; + + /// No description provided for @appearanceComposerSubtitle. + /// + /// In en, this message translates to: + /// **'Style and background of the message input bar'** + String get appearanceComposerSubtitle; + + /// No description provided for @appearanceComposerBackgroundStandard. + /// + /// In en, this message translates to: + /// **'Default'** + String get appearanceComposerBackgroundStandard; + + /// No description provided for @appearanceComposerBackgroundFrost. + /// + /// In en, this message translates to: + /// **'Frost blur'** + String get appearanceComposerBackgroundFrost; + /// No description provided for @appearanceGradientTitle. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 744e0d6..131218f 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -775,7 +775,20 @@ class AppLocalizationsEn extends AppLocalizations { String get appearanceChatChromeNone => 'None'; @override - String get appearanceChatChromeTransparent => 'Clear'; + String get appearanceChatChromeTransparent => 'Frost blur'; + + @override + String get appearanceComposerTitle => 'Input bar'; + + @override + String get appearanceComposerSubtitle => + 'Style and background of the message input bar'; + + @override + String get appearanceComposerBackgroundStandard => 'Default'; + + @override + String get appearanceComposerBackgroundFrost => 'Frost blur'; @override String get appearanceGradientTitle => 'Gradient'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 8dbb85c..257c242 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -778,7 +778,19 @@ class AppLocalizationsRu extends AppLocalizations { String get appearanceChatChromeNone => 'Нет'; @override - String get appearanceChatChromeTransparent => 'Прозр.'; + String get appearanceChatChromeTransparent => 'Frost blur'; + + @override + String get appearanceComposerTitle => 'Вид панели ввода'; + + @override + String get appearanceComposerSubtitle => 'Стиль и фон панели ввода сообщений'; + + @override + String get appearanceComposerBackgroundStandard => 'Default'; + + @override + String get appearanceComposerBackgroundFrost => 'Frost blur'; @override String get appearanceGradientTitle => 'Градиент'; diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 725a0fc..ba52539 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -260,7 +260,11 @@ "appearanceChatChromeColor": "Цвет", "appearanceChatChromeBlur": "Блюр", "appearanceChatChromeNone": "Нет", - "appearanceChatChromeTransparent": "Прозр.", + "appearanceChatChromeTransparent": "Frost blur", + "appearanceComposerTitle": "Вид панели ввода", + "appearanceComposerSubtitle": "Стиль и фон панели ввода сообщений", + "appearanceComposerBackgroundStandard": "Default", + "appearanceComposerBackgroundFrost": "Frost blur", "appearanceGradientTitle": "Градиент", "appearanceGradientSubtitle": "Объём и блики в Glossy-капсулах", "appearanceAccentColorTitle": "Акцентный цвет", diff --git a/lib/main.dart b/lib/main.dart index 48f39e0..a18762c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -38,6 +38,8 @@ import 'core/config/app_media_cache.dart'; import 'core/config/app_pill_gradient.dart'; import 'core/config/app_visual_style.dart'; import 'core/config/app_chat_chrome.dart'; +import 'core/config/app_composer_background.dart'; +import 'core/config/app_composer_style.dart'; import 'core/config/app_wallpaper_tint.dart'; import 'core/storage/chat_wallpaper_store.dart'; import 'core/utils/wallpaper_seed.dart'; @@ -190,6 +192,8 @@ void main(List args) async { final pillGradientFuture = AppPillGradient.load(); final visualStyleFuture = AppVisualStyle.load(); final chatChromeFuture = AppChatChrome.load(); + final composerStyleFuture = AppComposerStyle.load(); + final composerBackgroundFuture = AppComposerBackground.load(); final wallpaperTintFuture = AppWallpaperTint.load(); final themeScheduleFuture = AppThemeSchedule.load(); final messageActionsFuture = AppMessageActionsStyle.load(); @@ -241,6 +245,8 @@ void main(List args) async { pillGradientFuture, visualStyleFuture, chatChromeFuture, + composerStyleFuture, + composerBackgroundFuture, wallpaperTintFuture, themeScheduleFuture, messageActionsFuture,