diff --git a/lib/backend/modules/account.dart b/lib/backend/modules/account.dart index ddc2b53..6a0ee60 100644 --- a/lib/backend/modules/account.dart +++ b/lib/backend/modules/account.dart @@ -11,6 +11,7 @@ import '../../core/storage/app_database.dart'; import '../../core/storage/spoofing_service.dart'; import '../../core/storage/token_storage.dart'; import '../../core/utils/logger.dart'; +import '../../models/login_info.dart'; import 'banners.dart'; import 'chats.dart'; import 'complaints.dart'; @@ -693,41 +694,12 @@ class AccountModule { } Future _saveLoginInfo(Map data, int accountId) async { - final contact = data['profile']?['contact'] as Map?; - final videoChatHistory = data['videoChatHistory']; - final chats = data['chats'] as List?; final config = data['config'] as Map?; final serverConfig = config?['server'] as Map?; - final userConfig = config?['user'] as Map?; if (serverConfig != null) { await _persistEntryBannerApps(accountId, serverConfig); } - final yMap = serverConfig?['y-map'] as Map?; - final whiteListLinks = serverConfig?['white-list-links'] as List?; - final fileUploadUnsupported = - serverConfig?['file-upload-unsupported-types'] as List?; - final time = data['time'] as int?; - - final info = { - 'registrationTime': contact?['registrationTime'], - 'country': contact?['country'], - 'videoChatHistory': videoChatHistory, - 'updateTime': contact?['updateTime'], - 'id': contact?['id'], - 'chatMarker': chats != null && chats.isNotEmpty - ? _extractChatMarker(chats.cast()) - : null, - 'time': time, - 'server': serverConfig != null - ? _extractServerInfo( - serverConfig, - yMap, - whiteListLinks, - fileUploadUnsupported, - ) - : null, - 'user': userConfig != null ? _extractUserConfig(userConfig) : null, - }; + final info = LoginInfo.fromPayload(data); await AppDatabase.saveLoginInfo(accountId, jsonEncode(info)); } @@ -760,86 +732,6 @@ class AccountModule { } } - Map _extractChatMarker(List chats) { - int? latestTime; - for (final chat in chats) { - final lastEventTime = chat['lastEventTime'] as int?; - if (lastEventTime != null && - (latestTime == null || lastEventTime > latestTime)) { - latestTime = lastEventTime; - } - } - return {'chatMarker': latestTime}; - } - - Map _extractServerInfo( - Map serverConfig, - Map? yMap, - List? whiteListLinks, - List? fileUploadUnsupported, - ) { - return { - 'account-removal-enabled': serverConfig['account-removal-enabled'], - 'image-size': serverConfig['image-size'], - 'gce': serverConfig['gce'], - 'gcce': serverConfig['gcce'], - 'max-msg-length': serverConfig['max-msg-length'], - 'quotes-enabled': serverConfig['quotes-enabled'], - 'calls-endpoint': serverConfig['calls-endpoint'], - 'send-location-enabled': serverConfig['send-location-enabled'], - 'lgce': serverConfig['lgce'], - 'wud': serverConfig['wud'], - 'video-msg-enabled': serverConfig['video-msg-enabled'], - 'grse': serverConfig['grse'], - 'edit-timeout': serverConfig['edit-timeout'], - 'image-quality': serverConfig['image-quality'], - 'unsafe-files-alert': serverConfig['unsafe-files-alert'], - 'account-nickname-enabled': serverConfig['account-nickname-enabled'], - 'mentions_entity_names_limit': - serverConfig['mentions_entity_names_limit'], - 'reactions-enabled': serverConfig['reactions-enabled'], - 'y-map': yMap != null - ? { - 'tile': yMap['tile'], - 'geocoder': yMap['geocoder'], - 'static': yMap['static'], - } - : null, - 'white-list-links': whiteListLinks, - 'file-upload-unsupported-types': fileUploadUnsupported, - }; - } - - Map _extractUserConfig(Map userConfig) { - return { - 'CHATS_PUSH_NOTIFICATION': userConfig['CHATS_PUSH_NOTIFICATION'], - 'PUSH_DETAILS': userConfig['PUSH_DETAILS'], - 'PUSH_SOUND': userConfig['PUSH_SOUND'], - 'PHONE_NUMBER_PRIVACY': userConfig['PHONE_NUMBER_PRIVACY'], - 'INACTIVE_TTL': userConfig['INACTIVE_TTL'], - 'SHOW_READ_MARK': userConfig['SHOW_READ_MARK'], - 'AUDIO_TRANSCRIPTION_ENABLED': userConfig['AUDIO_TRANSCRIPTION_ENABLED'], - 'SEARCH_BY_PHONE': userConfig['SEARCH_BY_PHONE'], - 'INCOMING_CALL': userConfig['INCOMING_CALL'], - 'DOUBLE_TAP_REACTION_DISABLED': - userConfig['DOUBLE_TAP_REACTION_DISABLED'], - 'SAFE_MODE_NO_PIN': userConfig['SAFE_MODE_NO_PIN'], - 'CHATS_PUSH_SOUND': userConfig['CHATS_PUSH_SOUND'], - 'DOUBLE_TAP_REACTION_VALUE': userConfig['DOUBLE_TAP_REACTION_VALUE'], - 'FAMILY_PROTECTION': userConfig['FAMILY_PROTECTION'], - 'HIDDEN': userConfig['HIDDEN'], - 'CHATS_INVITE': userConfig['CHATS_INVITE'], - 'PUSH_NEW_CONTACTS': userConfig['PUSH_NEW_CONTACTS'], - 'UNSAFE_FILES': userConfig['UNSAFE_FILES'], - 'DONT_DISTURB_UNTIL': userConfig['DONT_DISTURB_UNTIL'], - 'ALT_KEYBOARD': userConfig['ALT_KEYBOARD'], - 'CONTENT_LEVEL_ACCESS': userConfig['CONTENT_LEVEL_ACCESS'], - 'STICKERS_SUGGEST': userConfig['STICKERS_SUGGEST'], - 'SAFE_MODE': userConfig['SAFE_MODE'], - 'M_CALL_PUSH_NOTIFICATION': userConfig['M_CALL_PUSH_NOTIFICATION'], - }; - } - Future _requestCodeInternal( String phone, AuthRequestType type, diff --git a/lib/backend/modules/chats.dart b/lib/backend/modules/chats.dart index 5754597..bda1158 100644 --- a/lib/backend/modules/chats.dart +++ b/lib/backend/modules/chats.dart @@ -146,6 +146,12 @@ class CachedChat { return iAmAdmin(myId) || options.contains('ALL_CAN_PIN_MESSAGE'); } + bool get forwardDisabled => options.contains('DISABLE_FORWARD'); + + bool get copyDisabled => options.contains('MESSAGE_COPY_NOT_ALLOWED'); + + bool get confirmBeforeSend => options.contains('CONFIRM_BEFORE_SEND'); + bool get isMuted { if (dontDisturbUntil == ChatsModule.muteOff) return false; if (dontDisturbUntil < 0) return true; @@ -502,7 +508,10 @@ class ChatsModule { if (cached.unreadCount == next && nextMark == currentMark) return; final participants = Map.from(cached.participants) ..[accountId] = nextMark; - final updated = cached.copyWith(unreadCount: next, participants: participants); + final updated = cached.copyWith( + unreadCount: next, + participants: participants, + ); await AppDatabase.saveChats([updated.toDbRow()]); _bump(); } @@ -1571,7 +1580,9 @@ class ChatsModule { }; final packet = await api.sendRequest(Opcode.msgSend, payload); if (!packet.isOk) { - logger.w('_createChat($chatType): server error payload=${packet.payload}'); + logger.w( + '_createChat($chatType): server error payload=${packet.payload}', + ); return null; } final data = packet.payload; @@ -1882,7 +1893,9 @@ class ChatsModule { 'operation': 'add', }); if (!packet.isOk) { - logger.w('addMembers $chatId: ${messageFromErrorPayload(packet.payload)}'); + logger.w( + 'addMembers $chatId: ${messageFromErrorPayload(packet.payload)}', + ); return false; } final data = packet.payload; diff --git a/lib/backend/modules/outbox.dart b/lib/backend/modules/outbox.dart index 4f451e9..64ff389 100644 --- a/lib/backend/modules/outbox.dart +++ b/lib/backend/modules/outbox.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import '../../core/protocol/packet.dart'; import '../../core/storage/app_database.dart'; import '../../core/storage/token_storage.dart'; import '../../core/utils/logger.dart'; @@ -88,8 +89,23 @@ class OutboxService { elements: elements.isEmpty ? null : elements, ); } catch (e) { - logger.w('Outbox: отправка ${pending.id} не удалась: $e'); - continue; + if (!isPermanentSendFailure(e)) { + logger.w('Outbox: отправка ${pending.id} не удалась: $e'); + continue; + } + logger.w('Outbox: ${pending.id} отклонено сервером: $e'); + final failed = pending.copyWith(status: 'error'); + await AppDatabase.saveMessages([failed.toDbRow()]); + chats.emitMessageSent(pending.chatId, pending.id, failed); + await chats.applyOutgoing( + accountId, + pending.chatId, + messageId: failed.id, + time: failed.time, + text: text, + status: 'error', + elements: elements.isEmpty ? null : elements, + ); } } } catch (e) { diff --git a/lib/core/config/app_colors.dart b/lib/core/config/app_colors.dart index 6ee6bfe..a92d723 100644 --- a/lib/core/config/app_colors.dart +++ b/lib/core/config/app_colors.dart @@ -6,6 +6,25 @@ extension AppColorTokens on ColorScheme { const int kAvatarThumbSize = 144; +const Color kSuccessGreen = Color(0xFF2EC36B); +const Color kDangerRed = Color(0xFFE5484D); const Color kReadReceiptBlue = Color(0xFF4FC3F7); -const Color kOnlineGreen = Color(0xFF34C759); -const Color kEditorAccent = Color(0xFF2F8FFF); + +class MediaAccent { + static Color? _seed; + static ColorScheme? _scheme; + + static ColorScheme schemeOf(BuildContext context) { + final seed = Theme.of(context).colorScheme.primary; + if (_seed != seed || _scheme == null) { + _seed = seed; + _scheme = ColorScheme.fromSeed( + seedColor: seed, + brightness: Brightness.dark, + ); + } + return _scheme!; + } + + static Color of(BuildContext context) => schemeOf(context).primary; +} diff --git a/lib/core/config/app_composer_style.dart b/lib/core/config/app_composer_style.dart index 0fbc158..69a4e82 100644 --- a/lib/core/config/app_composer_style.dart +++ b/lib/core/config/app_composer_style.dart @@ -1,15 +1,24 @@ import 'package:flutter/foundation.dart'; +import 'app_visual_style.dart'; import 'persisted_setting.dart'; -enum ComposerStyle { glossy, materialYou } +enum ComposerStyle { auto, glossy, materialYou } + +class ComposerChrome { + static bool isGlossy(ComposerStyle style) => switch (style) { + ComposerStyle.auto => AppVisualStyle.current.value.glossyChrome, + ComposerStyle.glossy => true, + ComposerStyle.materialYou => false, + }; +} class AppComposerStyle { static const prefKey = 'app_composer_style'; static final _setting = PersistedEnum( prefKey: prefKey, - defaultValue: ComposerStyle.materialYou, + defaultValue: ComposerStyle.auto, encode: (value) => value.name, decode: _parse, ); @@ -21,5 +30,5 @@ class AppComposerStyle { static Future save(ComposerStyle value) => _setting.save(value); static ComposerStyle _parse(String? val) => - enumFromName(ComposerStyle.values, val, ComposerStyle.materialYou); + enumFromName(ComposerStyle.values, val, ComposerStyle.auto); } diff --git a/lib/core/config/app_fonts.dart b/lib/core/config/app_fonts.dart index 8fe9b9d..a118948 100644 --- a/lib/core/config/app_fonts.dart +++ b/lib/core/config/app_fonts.dart @@ -1,5 +1,25 @@ import 'package:flutter/material.dart'; +const String kDisplayFontFamily = 'Outfit'; + +@immutable +class AppDisplayFont extends ThemeExtension { + final String? family; + + const AppDisplayFont(this.family); + + @override + AppDisplayFont copyWith({String? family}) => + AppDisplayFont(family ?? this.family); + + @override + AppDisplayFont lerp(ThemeExtension? other, double t) => + t < 0.5 ? this : (other as AppDisplayFont? ?? this); +} + +String? displayFontOf(BuildContext context) => + Theme.of(context).extension()?.family ?? kDisplayFontFamily; + class AppFont { final String id; final String label; @@ -38,6 +58,11 @@ class AppFonts { return builtIn.firstWhere((f) => f.id == id, orElse: () => fallback); } + static String? displayFamily(String id) { + final font = resolve(id); + return font.isSystem ? kDisplayFontFamily : font.fontFamily; + } + static TextTheme textTheme(String id, TextTheme base) { final family = resolve(id).fontFamily; if (family == null) return base; diff --git a/lib/core/config/app_frost.dart b/lib/core/config/app_frost.dart index 0c21828..91b7de2 100644 --- a/lib/core/config/app_frost.dart +++ b/lib/core/config/app_frost.dart @@ -3,14 +3,20 @@ import 'package:flutter/material.dart'; class AppFrost { static const double sigma = 34; static const double panelSigma = 24; + static const double overlaySigma = 18; + static const double mediaBackdropSigma = 30; static const double glassAlpha = 0.28; static const double blurPanelAlpha = 0.55; + static const double scrimAlpha = 0.4; static Color glassTint(ColorScheme cs, [double alpha = glassAlpha]) => cs.surfaceContainerHigh.withValues(alpha: alpha); static Color blurPanelTint(ColorScheme cs) => glassTint(cs, blurPanelAlpha); + static Color scrim([double alpha = scrimAlpha]) => + Colors.black.withValues(alpha: alpha); + static BorderSide hairline(ColorScheme cs) => BorderSide(color: cs.outlineVariant.withValues(alpha: 0.4), width: 0.5); } diff --git a/lib/core/config/app_nav_pill_style.dart b/lib/core/config/app_nav_pill_style.dart index 9d05f58..0a76c44 100644 --- a/lib/core/config/app_nav_pill_style.dart +++ b/lib/core/config/app_nav_pill_style.dart @@ -1,17 +1,27 @@ import 'package:flutter/foundation.dart'; import '../../frontend/widgets/liquid_glass.dart'; +import 'app_visual_style.dart'; import 'persisted_setting.dart'; -enum NavPillStyle { glossy, frostBlur, liquidGlass } +enum NavPillStyle { auto, glossy, frostBlur, liquidGlass } class NavPillMaterial { - static bool isLiquid(NavPillStyle style) => - style == NavPillStyle.liquidGlass && LiquidGlass.isSupported; + static NavPillStyle resolve(NavPillStyle style) { + if (style != NavPillStyle.auto) return style; + return AppVisualStyle.current.value == VisualStyle.liquidGlass + ? NavPillStyle.liquidGlass + : NavPillStyle.glossy; + } - static bool isFrost(NavPillStyle style) => - style == NavPillStyle.frostBlur || - (style == NavPillStyle.liquidGlass && !LiquidGlass.isSupported); + static bool isLiquid(NavPillStyle style) => + resolve(style) == NavPillStyle.liquidGlass && LiquidGlass.isSupported; + + static bool isFrost(NavPillStyle style) { + final resolved = resolve(style); + return resolved == NavPillStyle.frostBlur || + (resolved == NavPillStyle.liquidGlass && !LiquidGlass.isSupported); + } } class AppNavPillStyle { @@ -19,7 +29,7 @@ class AppNavPillStyle { static final _setting = PersistedEnum( prefKey: prefKey, - defaultValue: NavPillStyle.frostBlur, + defaultValue: NavPillStyle.auto, encode: (value) => value.name, decode: _parse, ); @@ -31,5 +41,5 @@ class AppNavPillStyle { static Future save(NavPillStyle value) => _setting.save(value); static NavPillStyle _parse(String? val) => - enumFromName(NavPillStyle.values, val, NavPillStyle.frostBlur); + enumFromName(NavPillStyle.values, val, NavPillStyle.auto); } diff --git a/lib/core/config/app_shape.dart b/lib/core/config/app_shape.dart new file mode 100644 index 0000000..dfce8fc --- /dev/null +++ b/lib/core/config/app_shape.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; + +class AppShape { + static const double card = 20; + static const double button = 14; + static const double sheet = 24; + static const double dialog = 24; + static const double pill = 100; + + static const BorderRadius cardRadius = BorderRadius.all( + Radius.circular(card), + ); + static const BorderRadius buttonRadius = BorderRadius.all( + Radius.circular(button), + ); + static const BorderRadius pillRadius = BorderRadius.all( + Radius.circular(pill), + ); + + static const RoundedRectangleBorder buttonBorder = RoundedRectangleBorder( + borderRadius: buttonRadius, + ); + static const RoundedRectangleBorder dialogBorder = RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(dialog)), + ); + static const RoundedRectangleBorder sheetBorder = RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(sheet)), + ); +} diff --git a/lib/core/protocol/packet.dart b/lib/core/protocol/packet.dart index 44034ab..7a79820 100644 --- a/lib/core/protocol/packet.dart +++ b/lib/core/protocol/packet.dart @@ -50,6 +50,12 @@ class SessionExpiredException extends PacketError { const SessionExpiredException(super.message); } +bool isPermanentSendFailure(Object error) { + if (error is! PacketError) return false; + if (error is SessionExpiredException) return false; + return !(error.errorKey?.contains('not.ready') ?? false); +} + String messageFromErrorPayload(dynamic payload) { if (payload is Map) { final msg = payload['message']; diff --git a/lib/frontend/debug/header_section.dart b/lib/frontend/debug/header_section.dart index e0bc0cd..4204033 100644 --- a/lib/frontend/debug/header_section.dart +++ b/lib/frontend/debug/header_section.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:material_symbols_icons/symbols.dart'; +import '../../core/config/app_fonts.dart'; class DebugHeaderSection extends StatelessWidget { const DebugHeaderSection({super.key}); @@ -28,7 +29,7 @@ class DebugHeaderSection extends StatelessWidget { color: cs.onSurface, fontSize: 20, fontWeight: FontWeight.w700, - fontFamily: 'Outfit', + fontFamily: displayFontOf(context), ), ), ), diff --git a/lib/frontend/debug/sync_probe_section.dart b/lib/frontend/debug/sync_probe_section.dart index 197f5d6..d1ad614 100644 --- a/lib/frontend/debug/sync_probe_section.dart +++ b/lib/frontend/debug/sync_probe_section.dart @@ -7,6 +7,7 @@ import '../../core/protocol/packet.dart'; import '../../main.dart'; import '../widgets/glossy_pill.dart'; import '../widgets/small_spinner.dart'; +import '../../core/config/app_shape.dart'; class DebugSyncProbeSection extends StatefulWidget { const DebugSyncProbeSection({super.key}); @@ -143,9 +144,7 @@ class _DebugSyncProbeSectionState extends State { onPressed: _loading ? null : _send, style: FilledButton.styleFrom( minimumSize: const Size.fromHeight(44), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), + shape: AppShape.buttonBorder, ), child: _loading ? const SmallSpinner(size: 20) diff --git a/lib/frontend/screens/auth/code_confirmation_screen.dart b/lib/frontend/screens/auth/code_confirmation_screen.dart index 0ef603f..03912ba 100644 --- a/lib/frontend/screens/auth/code_confirmation_screen.dart +++ b/lib/frontend/screens/auth/code_confirmation_screen.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:komet/l10n/app_localizations.dart'; import 'package:flutter/services.dart'; +import 'package:material_symbols_icons/symbols.dart'; import 'password_2fa_screen.dart'; import 'registration_screen.dart'; import 'session_stale_recovery.dart'; @@ -314,7 +315,7 @@ class _CodeConfirmationScreenState extends State backgroundColor: Colors.transparent, elevation: 0, leading: IconButton( - icon: Icon(Icons.arrow_back, color: cs.onSurfaceVariant), + icon: Icon(Symbols.arrow_back, color: cs.onSurfaceVariant), onPressed: () => Navigator.pop(context), ), ), @@ -528,7 +529,7 @@ class _CodeConfirmationScreenState extends State child: recovering ? SmallSpinner(size: 24, color: cs.onPrimaryContainer) : Icon( - Icons.arrow_forward, + Symbols.arrow_forward, color: _codeController.text.length == 6 ? cs.onPrimaryContainer : cs.onSurfaceVariant, diff --git a/lib/frontend/screens/auth/login_screen.dart b/lib/frontend/screens/auth/login_screen.dart index a1eb649..008d058 100644 --- a/lib/frontend/screens/auth/login_screen.dart +++ b/lib/frontend/screens/auth/login_screen.dart @@ -23,6 +23,8 @@ import '../../widgets/small_spinner.dart'; import '../../../backend/api.dart'; import '../../../core/protocol/packet.dart'; import '../../../main.dart'; +import '../../../core/config/app_frost.dart'; +import '../../../core/config/app_shape.dart'; class LoginScreen extends StatefulWidget { final int? returnToAccountId; @@ -409,7 +411,7 @@ class _LoginScreenState extends State { context: screenContext, barrierDismissible: true, barrierLabel: '', - barrierColor: Colors.black54, + barrierColor: AppFrost.scrim(), transitionDuration: const Duration(milliseconds: 250), pageBuilder: (context, anim1, anim2) => const SizedBox.shrink(), transitionBuilder: (context, anim1, anim2, child) { @@ -424,9 +426,7 @@ class _LoginScreenState extends State { child: AlertDialog( backgroundColor: cs.surfaceContainerHigh, surfaceTintColor: Colors.transparent, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), + shape: AppShape.dialogBorder, contentPadding: const EdgeInsets.fromLTRB(24, 24, 24, 8), actionsPadding: const EdgeInsets.fromLTRB(12, 0, 12, 12), content: Column( @@ -840,7 +840,7 @@ class _LoginScreenState extends State { ), const Spacer(), Icon( - Icons.keyboard_arrow_down, + Symbols.keyboard_arrow_down, color: cs.onSurfaceVariant, ), ], @@ -1005,7 +1005,7 @@ class _LoginScreenState extends State { color: cs.onPrimaryContainer, ) : Icon( - Icons.arrow_forward, + Symbols.arrow_forward, color: _isPhoneValid ? cs.onPrimaryContainer : cs.onSurfaceVariant, @@ -1068,4 +1068,3 @@ class _LoginScreenState extends State { ); } } - diff --git a/lib/frontend/screens/auth/password_2fa_screen.dart b/lib/frontend/screens/auth/password_2fa_screen.dart index bdd7e6b..1ef6731 100644 --- a/lib/frontend/screens/auth/password_2fa_screen.dart +++ b/lib/frontend/screens/auth/password_2fa_screen.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:material_symbols_icons/symbols.dart'; import '../../../core/protocol/packet.dart'; import '../../../main.dart'; import '../../widgets/animated_slash_icon.dart'; @@ -117,7 +118,7 @@ class _Password2FAScreenState extends State backgroundColor: Colors.transparent, elevation: 0, leading: IconButton( - icon: Icon(Icons.arrow_back, color: cs.onSurfaceVariant), + icon: Icon(Symbols.arrow_back, color: cs.onSurfaceVariant), onPressed: () => Navigator.pop(context), ), ), @@ -173,8 +174,8 @@ class _Password2FAScreenState extends State ), suffixIcon: IconButton( icon: AnimatedSlashIcon( - icon: Icons.visibility, - slashedIcon: Icons.visibility_off, + icon: Symbols.visibility, + slashedIcon: Symbols.visibility_off, slashed: _isPasswordVisible, color: cs.onSurfaceVariant, ), @@ -203,7 +204,7 @@ class _Password2FAScreenState extends State child: _isLoading ? SmallSpinner(size: 24, color: cs.onPrimaryContainer) : Icon( - Icons.arrow_forward, + Symbols.arrow_forward, color: _passwordController.text.isNotEmpty ? cs.onPrimaryContainer : cs.onSurfaceVariant, diff --git a/lib/frontend/screens/auth/registration_screen.dart b/lib/frontend/screens/auth/registration_screen.dart index 86b72d1..a338b1e 100644 --- a/lib/frontend/screens/auth/registration_screen.dart +++ b/lib/frontend/screens/auth/registration_screen.dart @@ -1,6 +1,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:komet/l10n/app_localizations.dart'; +import 'package:material_symbols_icons/symbols.dart'; import '../../../backend/modules/account.dart'; import '../../../main.dart'; @@ -99,7 +100,7 @@ class _RegistrationScreenState extends State { backgroundColor: Colors.transparent, elevation: 0, leading: IconButton( - icon: Icon(Icons.arrow_back, color: cs.onSurfaceVariant), + icon: Icon(Symbols.arrow_back, color: cs.onSurfaceVariant), onPressed: _isSubmitting ? null : () => Navigator.pop(context), ), ), @@ -113,7 +114,7 @@ class _RegistrationScreenState extends State { child: _isSubmitting ? SmallSpinner(size: 22, color: cs.onSurfaceVariant) : Icon( - Icons.arrow_forward, + Symbols.arrow_forward, color: _canSubmit ? cs.onPrimaryContainer : cs.onSurfaceVariant, ), ), diff --git a/lib/frontend/screens/auth/token_login_screen.dart b/lib/frontend/screens/auth/token_login_screen.dart index 58bb761..916f7e6 100644 --- a/lib/frontend/screens/auth/token_login_screen.dart +++ b/lib/frontend/screens/auth/token_login_screen.dart @@ -9,6 +9,7 @@ import '../../widgets/adaptive_shell.dart'; import '../../widgets/custom_notification.dart'; import '../../widgets/section_header.dart'; import '../../widgets/small_spinner.dart'; +import '../../../core/config/app_shape.dart'; class TokenLoginScreen extends StatefulWidget { final int? returnToAccountId; @@ -141,7 +142,7 @@ class _TokenLoginScreenState extends State { onPressed: _isLoading ? null : _login, style: FilledButton.styleFrom( minimumSize: const Size.fromHeight(52), - shape: const StadiumBorder(), + shape: AppShape.buttonBorder, ), child: _isLoading ? const SmallSpinner(size: 22) @@ -346,7 +347,7 @@ class _TokenLoginScreenState extends State { return ChoiceChip( label: Text(opt.label), avatar: isSelected - ? Icon(Icons.check, size: 18, color: cs.onSecondaryContainer) + ? Icon(Symbols.check, size: 18, color: cs.onSecondaryContainer) : Icon(opt.icon, size: 18, color: cs.onSurfaceVariant), selected: isSelected, showCheckmark: false, diff --git a/lib/frontend/screens/calls/call_link_sheet.dart b/lib/frontend/screens/calls/call_link_sheet.dart index 03abeee..e64368d 100644 --- a/lib/frontend/screens/calls/call_link_sheet.dart +++ b/lib/frontend/screens/calls/call_link_sheet.dart @@ -9,6 +9,7 @@ import 'package:komet/frontend/widgets/custom_notification.dart'; import 'package:komet/frontend/widgets/small_spinner.dart'; import 'package:komet/l10n/app_localizations.dart'; import 'package:komet/main.dart' show messagesModule; +import '../../../core/config/app_shape.dart'; Future showCreatedCallSheet( BuildContext context, { @@ -196,9 +197,7 @@ class _CreatedCallCardState extends State<_CreatedCallCard> { child: FilledButton( onPressed: () => Navigator.of(context).pop(true), style: FilledButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(14), - ), + shape: AppShape.buttonBorder, ), child: Text( l10n.callLinkStart, diff --git a/lib/frontend/screens/calls/call_participants_sheet.dart b/lib/frontend/screens/calls/call_participants_sheet.dart index bfdccb3..f22131c 100644 --- a/lib/frontend/screens/calls/call_participants_sheet.dart +++ b/lib/frontend/screens/calls/call_participants_sheet.dart @@ -10,6 +10,7 @@ import '../../widgets/custom_notification.dart'; import '../../widgets/komet_avatar.dart'; import '../../widgets/prompt_dialog.dart'; import '../../widgets/sheet_helpers.dart'; +import '../../../core/config/app_fonts.dart'; class CallParticipantView { final String name; @@ -118,7 +119,7 @@ class _ParticipantsSheetState extends State<_ParticipantsSheet> { color: cs.onSurface, fontSize: 18, fontWeight: FontWeight.w600, - fontFamily: 'Outfit', + fontFamily: displayFontOf(context), ), ), ), @@ -358,7 +359,7 @@ class _ParticipantsSheetState extends State<_ParticipantsSheet> { color: cs.onSurface, fontSize: 20, fontWeight: FontWeight.w700, - fontFamily: 'Outfit', + fontFamily: displayFontOf(context), ), ), ); diff --git a/lib/frontend/screens/calls/call_screen.dart b/lib/frontend/screens/calls/call_screen.dart index d21653a..92f53f2 100644 --- a/lib/frontend/screens/calls/call_screen.dart +++ b/lib/frontend/screens/calls/call_screen.dart @@ -20,6 +20,7 @@ import '../../../core/cache/info_cache.dart'; import '../../../core/calls/call_controller.dart'; import '../../../core/calls/call_info.dart'; import '../../../core/calls/call_session.dart'; +import '../../../core/config/app_colors.dart'; import '../../../core/utils/format.dart'; import '../../../core/utils/logger.dart'; import '../../../l10n/app_localizations.dart'; @@ -30,9 +31,7 @@ import '../../widgets/sheet_helpers.dart'; import '../../widgets/small_spinner.dart'; import 'call_participants_sheet.dart'; import 'komet_hub.dart'; - -const Color _kEndRed = Color(0xFFE5484D); -const Color _kAcceptGreen = Color(0xFF2EC36B); +import '../../../core/config/app_fonts.dart'; class CallScreen extends StatefulWidget { final String name; @@ -603,7 +602,7 @@ class _CallScreenState extends State with TickerProviderStateMixin { color: cs.onSurface, fontSize: 24, fontWeight: FontWeight.w700, - fontFamily: 'Outfit', + fontFamily: displayFontOf(context), ), ), const SizedBox(height: 2), @@ -673,7 +672,7 @@ class _CallScreenState extends State with TickerProviderStateMixin { borderRadius: BorderRadius.circular(20), depth: 6, borderSide: speaking - ? const BorderSide(color: _kAcceptGreen, width: 2.5) + ? const BorderSide(color: kSuccessGreen, width: 2.5) : null, padding: EdgeInsets.all(showVideo ? 0 : 12), child: showVideo @@ -1136,7 +1135,7 @@ class _CallScreenState extends State with TickerProviderStateMixin { color: cs.onPrimaryContainer, fontSize: size * 0.38, fontWeight: FontWeight.w600, - fontFamily: 'Outfit', + fontFamily: displayFontOf(context), ), ), ); @@ -1154,7 +1153,7 @@ class _CallScreenState extends State with TickerProviderStateMixin { color: cs.onSurface, fontSize: 30, fontWeight: FontWeight.w600, - fontFamily: 'Outfit', + fontFamily: displayFontOf(context), height: 1.1, ), ), @@ -1228,14 +1227,14 @@ class _CallScreenState extends State with TickerProviderStateMixin { _CallButton( icon: Symbols.call_end, label: l10n.callDecline, - background: _kEndRed, + background: kDangerRed, foreground: Colors.white, onTap: _decline, ), _CallButton( icon: Symbols.call, label: l10n.callAccept, - background: _kAcceptGreen, + background: kSuccessGreen, foreground: Colors.white, onTap: _accept, ), @@ -1290,7 +1289,7 @@ class _CallScreenState extends State with TickerProviderStateMixin { _CallButton( icon: Symbols.call_end, label: l10n.callEndButton, - background: _kEndRed, + background: kDangerRed, foreground: Colors.white, onTap: _hangup, ), @@ -1550,7 +1549,7 @@ class _CallInfoSheet extends StatelessWidget { color: cs.onSurface, fontSize: 20, fontWeight: FontWeight.w700, - fontFamily: 'Outfit', + fontFamily: displayFontOf(context), ), ), const SizedBox(height: 2), diff --git a/lib/frontend/screens/calls/calls_tab.dart b/lib/frontend/screens/calls/calls_tab.dart index c0e1f7a..68f92b5 100644 --- a/lib/frontend/screens/calls/calls_tab.dart +++ b/lib/frontend/screens/calls/calls_tab.dart @@ -20,6 +20,7 @@ import '../../widgets/spectrum_tint.dart'; import '../../../l10n/app_localizations.dart'; import 'call_link_sheet.dart'; import 'call_screen.dart'; +import '../../../core/config/app_fonts.dart'; class CallsTab extends StatefulWidget { const CallsTab({super.key}); @@ -490,7 +491,7 @@ class _CallsTabState extends State color: cs.onSurface, fontSize: 24, fontWeight: FontWeight.w700, - fontFamily: 'Outfit', + fontFamily: displayFontOf(context), ), ), const ConnectionStatusLine(), diff --git a/lib/frontend/screens/calls/komet_hub.dart b/lib/frontend/screens/calls/komet_hub.dart index 94c2dfa..b36ed44 100644 --- a/lib/frontend/screens/calls/komet_hub.dart +++ b/lib/frontend/screens/calls/komet_hub.dart @@ -7,6 +7,7 @@ import '../../../core/calls/call_session.dart'; import '../../../core/games/checkers.dart'; import '../../../l10n/app_localizations.dart'; import '../../widgets/sheet_helpers.dart'; +import '../../../core/config/app_fonts.dart'; Future showKometHub( BuildContext context, { @@ -115,7 +116,7 @@ class _KometHubState extends State<_KometHub> { color: cs.onSurface, fontSize: 18, fontWeight: FontWeight.w700, - fontFamily: 'Outfit', + fontFamily: displayFontOf(context), ), ), ], diff --git a/lib/frontend/screens/chats/chat/video_note_controller.dart b/lib/frontend/screens/chats/chat/video_note_controller.dart index a137405..eec9646 100644 --- a/lib/frontend/screens/chats/chat/video_note_controller.dart +++ b/lib/frontend/screens/chats/chat/video_note_controller.dart @@ -19,6 +19,7 @@ import '../../../../core/utils/logger.dart'; import '../../../widgets/custom_notification.dart'; import '../../../widgets/lottie_slash_icon.dart'; import 'voice_record_controller.dart'; +import '../../../../core/config/app_frost.dart'; const String _flashIcon = 'assets/lottie/ic_flash_on_to_off.json'; @@ -304,7 +305,7 @@ class VideoNoteRecordingLayer extends StatefulWidget { class _VideoNoteRecordingLayerState extends State with SingleTickerProviderStateMixin { static const double _circle = 260; - static const double _maxBlur = 18; + static const double _maxBlur = AppFrost.overlaySigma; late final AnimationController _reveal = AnimationController( vsync: this, diff --git a/lib/frontend/screens/chats/chat/view/chat_header.dart b/lib/frontend/screens/chats/chat/view/chat_header.dart index f51d14b..a91a15e 100644 --- a/lib/frontend/screens/chats/chat/view/chat_header.dart +++ b/lib/frontend/screens/chats/chat/view/chat_header.dart @@ -16,6 +16,7 @@ import 'package:komet/frontend/widgets/online_dot.dart'; import 'package:komet/frontend/widgets/profile_hero.dart'; import 'package:komet/main.dart' show storiesModule; import 'package:komet/models/story.dart'; +import '../../../../../core/config/app_fonts.dart'; class ChatHeaderRow extends StatelessWidget { final bool glossy; @@ -85,7 +86,7 @@ class ChatHeaderRow extends StatelessWidget { color: cs.onSurface, fontSize: 17, fontWeight: FontWeight.w600, - fontFamily: 'Outfit', + fontFamily: displayFontOf(context), ); return Padding( padding: const EdgeInsets.fromLTRB(10, 4, 10, 8), @@ -152,7 +153,7 @@ class ChatHeaderRow extends StatelessWidget { color: cs.onPrimaryContainer, fontSize: d * 0.36, fontWeight: FontWeight.w600, - fontFamily: 'Outfit', + fontFamily: displayFontOf(context), ), ), ), @@ -270,7 +271,7 @@ class ChatHeaderRow extends StatelessWidget { color: cs.onSurface, fontSize: 16, fontWeight: FontWeight.w600, - fontFamily: 'Outfit', + fontFamily: displayFontOf(context), ); return Row( children: [ diff --git a/lib/frontend/screens/chats/chat/view/composer_input.dart b/lib/frontend/screens/chats/chat/view/composer_input.dart index cec4858..a028366 100644 --- a/lib/frontend/screens/chats/chat/view/composer_input.dart +++ b/lib/frontend/screens/chats/chat/view/composer_input.dart @@ -398,118 +398,126 @@ class ComposerInputBar extends StatelessWidget { }, child: ValueListenableBuilder( valueListenable: hasText, - builder: (context, hasText, _) => ValueListenableBuilder( - valueListenable: voiceRec.locked, - builder: (context, voiceLocked, _) => - ValueListenableBuilder( - valueListenable: voiceRec.isRecording, - builder: (context, voiceRecording, _) => - AnimatedBuilder( - animation: Listenable.merge([ - note.videoNoteMode, - note.isRecording, - note.locked, - ]), - builder: (context, _) { - final videoMode = - note.videoNoteMode.value; - final noteRecording = - note.isRecording.value; - final recording = - voiceRecording || noteRecording; - final locked = noteRecording - ? note.locked.value - : voiceLocked; - final sendMode = - hasText || - hasForward || - locked || - forceSend; - final pill = _actionSurface( - color: _flat - ? Colors.transparent - : recording - ? cs.error - : _frost - ? AppFrost.glassTint(cs) - : cs.surfaceContainerHighest, - onTap: - (hasText || - hasForward || - forceSend) - ? onSendText - : locked - ? () => noteRecording - ? note.stop(cancel: false) - : voiceRec.stop( - cancel: false, - ) - : null, - onLongPress: - (hasText && - !forceSend && - !hasForward) - ? onScheduleMessage - : null, - child: SizedBox( - width: 54, - height: 54, - child: Center( - child: ComposerMorphIcon( - action: sendMode - ? ComposerAction.send - : videoMode - ? ComposerAction.videocam - : ComposerAction.mic, - color: recording - ? (_flat - ? cs.error - : cs.onError) - : sendMode - ? cs.primary - : _flat - ? cs.onSurfaceVariant - : cs.onSurface, + builder: (context, hasText, _) => + ValueListenableBuilder( + valueListenable: voiceRec.locked, + builder: (context, voiceLocked, _) => + ValueListenableBuilder( + valueListenable: voiceRec.isRecording, + builder: (context, voiceRecording, _) => + AnimatedBuilder( + animation: Listenable.merge([ + note.videoNoteMode, + note.isRecording, + note.locked, + ]), + builder: (context, _) { + final videoMode = + note.videoNoteMode.value; + final noteRecording = + note.isRecording.value; + final recording = + voiceRecording || + noteRecording; + final locked = noteRecording + ? note.locked.value + : voiceLocked; + final sendMode = + hasText || + hasForward || + locked || + forceSend; + final pill = _actionSurface( + color: _flat + ? Colors.transparent + : recording + ? cs.error + : _frost + ? AppFrost.glassTint(cs) + : cs.surfaceContainerHighest, + onTap: + (hasText || + hasForward || + forceSend) + ? onSendText + : locked + ? () => noteRecording + ? note.stop( + cancel: false, + ) + : voiceRec.stop( + cancel: false, + ) + : null, + onLongPress: + (hasText && + !forceSend && + !hasForward) + ? onScheduleMessage + : null, + child: SizedBox( + width: 54, + height: 54, + child: Center( + child: ComposerMorphIcon( + action: sendMode + ? ComposerAction.send + : videoMode + ? ComposerAction + .videocam + : ComposerAction.mic, + color: recording + ? (_flat + ? cs.error + : cs.onError) + : sendMode + ? cs.primary + : _flat + ? cs.onSurfaceVariant + : cs.onSurface, + ), + ), ), - ), - ), - ); - final visual = _recordingButtonVisual( - pill: pill, - cs: cs, - active: recording && !locked, - ); - final voiceEnabled = - !sendMode && !forceSend; - return GestureDetector( - onTap: voiceEnabled - ? note.toggleMode - : null, - onLongPressStart: voiceEnabled - ? (_) => videoMode - ? note.start() - : voiceRec.start() - : null, - onLongPressMoveUpdate: voiceEnabled - ? (d) => videoMode - ? note.handleDrag( - d.offsetFromOrigin, - ) - : voiceRec.handleDrag( - d.offsetFromOrigin, - ) - : null, - onLongPressEnd: voiceEnabled - ? (_) => videoMode - ? note.handleEnd() - : voiceRec.handleEnd() - : null, - child: visual, - ); - }, - ), - ), - ), + ); + final visual = + _recordingButtonVisual( + pill: pill, + cs: cs, + active: + recording && !locked, + ); + final voiceEnabled = + !sendMode && !forceSend; + return GestureDetector( + onTap: voiceEnabled + ? note.toggleMode + : null, + onLongPressStart: voiceEnabled + ? (_) => videoMode + ? note.start() + : voiceRec.start() + : null, + onLongPressMoveUpdate: + voiceEnabled + ? (d) => videoMode + ? note.handleDrag( + d.offsetFromOrigin, + ) + : voiceRec.handleDrag( + d.offsetFromOrigin, + ) + : null, + onLongPressEnd: voiceEnabled + ? (_) => videoMode + ? note.handleEnd() + : voiceRec.handleEnd() + : null, + child: visual, + ); + }, + ), + ), + ), ), ), ], @@ -525,7 +533,7 @@ class ComposerInputBar extends StatelessWidget { return _barSurface(cs, bar); } - bool get _flat => style == ComposerStyle.materialYou; + bool get _flat => !ComposerChrome.isGlossy(style); bool get _frost => ComposerMaterial.isFrost(background); diff --git a/lib/frontend/screens/chats/chat/view/search_view.dart b/lib/frontend/screens/chats/chat/view/search_view.dart index 06c5c0d..82bf958 100644 --- a/lib/frontend/screens/chats/chat/view/search_view.dart +++ b/lib/frontend/screens/chats/chat/view/search_view.dart @@ -12,6 +12,7 @@ import 'package:komet/frontend/widgets/komet_avatar.dart'; import 'package:komet/frontend/widgets/small_spinner.dart'; import 'package:komet/frontend/screens/chats/chat/chat_search_controller.dart'; import 'package:komet/frontend/screens/chats/chat/message_search_result.dart'; +import '../../../../../core/config/app_fonts.dart'; class SearchTopBar extends StatelessWidget { const SearchTopBar({ @@ -37,13 +38,17 @@ class SearchTopBar extends StatelessWidget { textInputAction: TextInputAction.search, onSubmitted: search.submit, cursorColor: cs.primary, - style: TextStyle(color: cs.onSurface, fontSize: 16, fontFamily: 'Outfit'), + style: TextStyle( + color: cs.onSurface, + fontSize: 16, + fontFamily: displayFontOf(context), + ), decoration: InputDecoration( hintText: 'Поиск...', hintStyle: TextStyle( color: cs.onSurfaceVariant, fontSize: 16, - fontFamily: 'Outfit', + fontFamily: displayFontOf(context), ), border: InputBorder.none, isDense: true, @@ -164,7 +169,8 @@ class SearchOverlay extends StatelessWidget { bottom: MediaQuery.paddingOf(context).bottom + 16, ), itemCount: results.length, - itemBuilder: (context, index) => _tile(results[index]), + itemBuilder: (context, index) => + _tile(context, results[index]), ); } if (loading) { @@ -196,7 +202,7 @@ class SearchOverlay extends StatelessWidget { ); } - Widget _tile(MessageSearchResult r) { + Widget _tile(BuildContext context, MessageSearchResult r) { final name = senderName(r.senderId); final date = formatDateWords(DateTime.fromMillisecondsSinceEpoch(r.time)); return InkWell( @@ -228,7 +234,7 @@ class SearchOverlay extends StatelessWidget { color: cs.primary, fontSize: 15, fontWeight: FontWeight.w600, - fontFamily: 'Outfit', + fontFamily: displayFontOf(context), ), ), ), diff --git a/lib/frontend/screens/chats/chat/view/selection_bar.dart b/lib/frontend/screens/chats/chat/view/selection_bar.dart index 8344868..913064f 100644 --- a/lib/frontend/screens/chats/chat/view/selection_bar.dart +++ b/lib/frontend/screens/chats/chat/view/selection_bar.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:komet/backend/modules/messages.dart'; import 'package:komet/frontend/widgets/glossy_pill.dart'; +import '../../../../../core/config/app_fonts.dart'; class SelectionTopBar extends StatelessWidget { final ColorScheme cs; @@ -51,7 +52,7 @@ class SelectionTopBar extends StatelessWidget { color: cs.onSurface, fontSize: 18, fontWeight: FontWeight.w600, - fontFamily: 'Outfit', + fontFamily: displayFontOf(context), ), ), ), @@ -114,7 +115,7 @@ class SelectionTopBar extends StatelessWidget { color: cs.onSurface, fontSize: 18, fontWeight: FontWeight.w600, - fontFamily: 'Outfit', + fontFamily: displayFontOf(context), ), ), ), @@ -149,6 +150,7 @@ class SelectionBottomBar extends StatelessWidget { final Set selected; final VoidCallback onReply; final VoidCallback onForward; + final bool allowForward; const SelectionBottomBar({ super.key, @@ -156,6 +158,7 @@ class SelectionBottomBar extends StatelessWidget { required this.selected, required this.onReply, required this.onForward, + this.allowForward = true, }); @override @@ -169,6 +172,7 @@ class SelectionBottomBar extends StatelessWidget { if (single) ...[ Expanded( child: _pill( + context, cs, icon: Symbols.reply, label: 'Ответить', @@ -176,18 +180,22 @@ class SelectionBottomBar extends StatelessWidget { onTap: onReply, ), ), - const SizedBox(width: 12), + if (allowForward) const SizedBox(width: 12), ] else const Spacer(), - Expanded( - child: _pill( - cs, - icon: Symbols.forward, - label: 'Переслать', - iconLeading: true, - onTap: onForward, - ), - ), + if (allowForward) + Expanded( + child: _pill( + context, + cs, + icon: Symbols.forward, + label: 'Переслать', + iconLeading: true, + onTap: onForward, + ), + ) + else if (!single) + const Spacer(), ], ), ), @@ -195,6 +203,7 @@ class SelectionBottomBar extends StatelessWidget { } Widget _pill( + BuildContext context, ColorScheme cs, { required IconData icon, required String label, @@ -207,7 +216,7 @@ class SelectionBottomBar extends StatelessWidget { color: cs.onSurface, fontSize: 16, fontWeight: FontWeight.w600, - fontFamily: 'Outfit', + fontFamily: displayFontOf(context), ), ); final iconWidget = Icon(icon, color: cs.onSurface, size: 22, weight: 500); diff --git a/lib/frontend/screens/chats/chat_encryption_screen.dart b/lib/frontend/screens/chats/chat_encryption_screen.dart index 0eed735..ccc775c 100644 --- a/lib/frontend/screens/chats/chat_encryption_screen.dart +++ b/lib/frontend/screens/chats/chat_encryption_screen.dart @@ -8,6 +8,7 @@ import '../../widgets/glossy_pill.dart'; import '../../widgets/primary_loading_button.dart'; import '../../widgets/settings_card.dart'; import '../../widgets/small_spinner.dart'; +import '../../../core/config/app_fonts.dart'; class ChatEncryptionScreen extends StatefulWidget { final int accountId; @@ -101,7 +102,7 @@ class _ChatEncryptionScreenState extends State { color: cs.onSurface, fontSize: 20, fontWeight: FontWeight.w700, - fontFamily: 'Outfit', + fontFamily: displayFontOf(context), ), ), ), diff --git a/lib/frontend/screens/chats/chat_info_screen.dart b/lib/frontend/screens/chats/chat_info_screen.dart index fe3b0ae..fae7f0c 100644 --- a/lib/frontend/screens/chats/chat_info_screen.dart +++ b/lib/frontend/screens/chats/chat_info_screen.dart @@ -1,4 +1,6 @@ import 'dart:async'; + +import 'package:flutter/services.dart'; import 'dart:math' as math; import 'dart:ui' show lerpDouble; @@ -48,6 +50,7 @@ import '../stories/story_viewer_screen.dart'; import 'chat_screen.dart'; import 'group_invite_sheets.dart'; import 'profile_action_sheets.dart'; +import '../../../core/config/app_fonts.dart'; class _MemberInfo { final int id; @@ -588,9 +591,9 @@ class _ChatInfoScreenState extends State : collapsedH; final delta = expandedH - collapsedH; _syncHeaderDelta(delta); - final controller = _bodyScrollController ??= - (ScrollController(initialScrollOffset: delta) - ..addListener(_onBodyScroll)); + final controller = _bodyScrollController ??= (ScrollController( + initialScrollOffset: delta, + )..addListener(_onBodyScroll)); return NotificationListener( onNotification: (n) => _onHeaderScrollNotification(n, delta), @@ -794,7 +797,11 @@ class _ChatInfoScreenState extends State : SegmentedRingPainter( total: _storyPreview!.totalCount, read: _storyPreview!.readCount, - unreadColors: [cs.primary, cs.tertiary, cs.primary], + unreadColors: [ + cs.primary, + cs.tertiary, + cs.primary, + ], readColor: cs.outlineVariant, strokeWidth: 3.4, ), @@ -809,7 +816,7 @@ class _ChatInfoScreenState extends State child: Row( children: [ IconButton( - icon: Icon(Icons.arrow_back, color: iconColor), + icon: Icon(Symbols.arrow_back, color: iconColor), onPressed: () => Navigator.pop(context), ), Expanded( @@ -958,13 +965,13 @@ class _ChatInfoScreenState extends State if (interactive && _avatarHover) ...[ _avatarArrow( alignment: Alignment.centerLeft, - icon: Icons.chevron_left, + icon: Symbols.chevron_left, enabled: _avatarIndex > 0, onTap: () => _stepAvatar(-1), ), _avatarArrow( alignment: Alignment.centerRight, - icon: Icons.chevron_right, + icon: Symbols.chevron_right, enabled: _avatarIndex < pages.length - 1, onTap: () => _stepAvatar(1), ), @@ -1085,11 +1092,11 @@ class _ChatInfoScreenState extends State '${pluralRu(unread.length, 'история', 'истории', 'историй')}', maxLines: 1, overflow: TextOverflow.ellipsis, - style: const TextStyle( + style: TextStyle( color: Colors.white, fontSize: 17, fontWeight: FontWeight.w600, - fontFamily: 'Outfit', + fontFamily: displayFontOf(context), ), ), ), @@ -1147,12 +1154,12 @@ class _ChatInfoScreenState extends State final color = iconColor ?? cs.onSurface; if (entries.isEmpty) { return IconButton( - icon: Icon(Icons.more_vert, color: color), + icon: Icon(Symbols.more_vert, color: color), onPressed: null, ); } return PopupMenuButton( - icon: Icon(Icons.more_vert, color: color), + icon: Icon(Symbols.more_vert, color: color), onSelected: (action) => action(), itemBuilder: (_) => [ for (final entry in entries) @@ -1181,7 +1188,9 @@ class _ChatInfoScreenState extends State _moreMenuEntries() { if (_isLoading) return const []; final entries = - <({IconData icon, String label, bool destructive, VoidCallback onTap})>[]; + < + ({IconData icon, String label, bool destructive, VoidCallback onTap}) + >[]; if (widget.chatType == 'DIALOG') { if (_isContact) { @@ -1270,7 +1279,7 @@ class _ChatInfoScreenState extends State color: textColor, fontSize: lerpDouble(22, 25, t)!, fontWeight: FontWeight.w700, - fontFamily: 'Outfit', + fontFamily: displayFontOf(context), ); final custom = _customName; final real = _realName; @@ -1377,8 +1386,8 @@ class _ChatInfoScreenState extends State Widget _buildActions(ColorScheme cs) { final muteBtn = ( - icon: Icons.notifications, - slashedIcon: Icons.notifications_off, + icon: Symbols.notifications, + slashedIcon: Symbols.notifications_off, slashed: _isMuted, label: _isMuted ? l10n.chatInfoActionMuted @@ -1386,14 +1395,14 @@ class _ChatInfoScreenState extends State onTap: _muteBusy ? null : _toggleMute, ); final chatBtn = ( - icon: Icons.chat_bubble, + icon: Symbols.chat_bubble, slashedIcon: null, slashed: false, label: l10n.contactProfileActionChat, onTap: _openChat, ); final leaveBtn = ( - icon: Icons.exit_to_app, + icon: Symbols.exit_to_app, slashedIcon: null, slashed: false, label: l10n.chatInfoActionLeave, @@ -1416,7 +1425,7 @@ class _ChatInfoScreenState extends State muteBtn, if (!_isBot) ( - icon: Icons.call, + icon: Symbols.call, slashedIcon: null, slashed: false, label: l10n.contactProfileActionCall, @@ -1526,7 +1535,9 @@ class _ChatInfoScreenState extends State showCustomNotification( context, error ?? - (muted ? l10n.chatInfoNotificationsOn : l10n.chatInfoNotificationsOff), + (muted + ? l10n.chatInfoNotificationsOn + : l10n.chatInfoNotificationsOff), ); } @@ -1549,8 +1560,11 @@ class _ChatInfoScreenState extends State if (active != null) { await navigator.push( MaterialPageRoute( - builder: (_) => - CallScreen(name: _customName, avatarUrl: avatarUrl, session: active), + builder: (_) => CallScreen( + name: _customName, + avatarUrl: avatarUrl, + session: active, + ), ), ); return; @@ -1819,8 +1833,11 @@ class _ChatInfoScreenState extends State } } } else { - final link = _chatInfo?.link; - if (link != null && link.isNotEmpty) { + final info = _chatInfo; + final link = info?.link; + if (link != null && + link.isNotEmpty && + (info?.canSeeInviteLink(_myId) ?? false)) { items.add(_linkCard(cs, link)); } final desc = _chatInfo?.description; @@ -1903,7 +1920,7 @@ class _ChatInfoScreenState extends State ), ), IconButton( - icon: Icon(Icons.qr_code_2, color: cs.primary, size: 22), + icon: Icon(Symbols.qr_code_2, color: cs.primary, size: 22), onPressed: () {}, ), ], @@ -1911,6 +1928,9 @@ class _ChatInfoScreenState extends State ); } + static const Duration _descRevealDuration = Duration(milliseconds: 260); + static const Curve _descRevealCurve = Curves.easeOutCubic; + Widget _collapsibleDescCard(ColorScheme cs, String desc) { const int collapsedLines = 3; final isLong = desc.length > 120; @@ -1930,23 +1950,45 @@ class _ChatInfoScreenState extends State style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13), ), const SizedBox(height: 4), - FormattedMessageText( - text: desc, - ranges: const [], - entityMode: TextEntityMode.copy, - style: TextStyle(color: cs.onSurface, fontSize: 15, height: 1.4), - maxLines: (_descExpanded || !isLong) ? null : collapsedLines, - overflow: (_descExpanded || !isLong) - ? null - : TextOverflow.ellipsis, + AnimatedSize( + duration: _descRevealDuration, + curve: _descRevealCurve, + alignment: Alignment.topLeft, + child: FormattedMessageText( + text: desc, + ranges: const [], + entityMode: TextEntityMode.copy, + style: TextStyle( + color: cs.onSurface, + fontSize: 15, + height: 1.4, + ), + maxLines: (_descExpanded || !isLong) ? null : collapsedLines, + overflow: (_descExpanded || !isLong) + ? null + : TextOverflow.ellipsis, + ), ), if (isLong) ...[ const SizedBox(height: 6), GestureDetector( - onTap: () => setState(() => _descExpanded = !_descExpanded), - child: Text( - _descExpanded ? l10n.chatInfoCollapse : l10n.chatInfoShowMore, - style: TextStyle(color: cs.primary, fontSize: 13), + behavior: HitTestBehavior.opaque, + onTap: () { + Haptics.tap(); + setState(() => _descExpanded = !_descExpanded); + }, + child: AnimatedTextSwap( + showAlternate: _descExpanded, + duration: _descRevealDuration, + curve: _descRevealCurve, + alternate: Text( + l10n.chatInfoCollapse, + style: TextStyle(color: cs.primary, fontSize: 13), + ), + child: Text( + l10n.chatInfoShowMore, + style: TextStyle(color: cs.primary, fontSize: 13), + ), ), ), ], @@ -2046,7 +2088,7 @@ class _ChatInfoScreenState extends State return _buildPlaceholder( cs, l10n.chatInfoEmptyGeneralChats, - Icons.group, + Symbols.group, ); } return CommonChatsTab( @@ -2060,7 +2102,7 @@ class _ChatInfoScreenState extends State cs, SharedContentKind.media, l10n.chatInfoEmptyMedia, - Icons.photo_library, + Symbols.photo_library, ); } if (_selectedTab == l10n.chatInfoTabFiles) { @@ -2068,7 +2110,7 @@ class _ChatInfoScreenState extends State cs, SharedContentKind.files, l10n.chatInfoEmptyFiles, - Icons.description, + Symbols.description, ); } if (_selectedTab == l10n.chatInfoTabVoice) { @@ -2076,7 +2118,7 @@ class _ChatInfoScreenState extends State cs, SharedContentKind.voice, l10n.chatInfoEmptyVoice, - Icons.mic, + Symbols.mic, ); } if (_selectedTab == l10n.chatInfoTabLinks) { @@ -2084,7 +2126,7 @@ class _ChatInfoScreenState extends State cs, SharedContentKind.links, l10n.chatInfoEmptyLinks, - Icons.link, + Symbols.link, ); } return const SizedBox.shrink(); @@ -2187,7 +2229,7 @@ class _ChatInfoScreenState extends State children: [ _memberAction( cs, - Icons.person_add, + Symbols.person_add, l10n.chatInfoAddMember, _openAddMembers, ), @@ -2195,7 +2237,7 @@ class _ChatInfoScreenState extends State _listDivider(cs), _memberAction( cs, - Icons.link, + Symbols.link, l10n.chatInfoInviteByLink, () => _openInviteLink(_inviteLink!), ), @@ -2232,7 +2274,7 @@ class _ChatInfoScreenState extends State padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), child: Row( children: [ - Icon(Icons.expand_more, color: cs.primary, size: 26), + Icon(Symbols.expand_more, color: cs.primary, size: 26), const SizedBox(width: 14), Text( l10n.chatInfoShowMore, @@ -2480,6 +2522,7 @@ class _ChatInfoScreenState extends State add(l10n.chatInfoRowId, chat['id']); if (type == 'DIALOG') { + add(l10n.chatInfoRowUserId, _contactData?.raw['id'] ?? _otherId); add(l10n.chatInfoRowCreated, chat['created'], tsFormat: true); add(l10n.chatInfoRowModified, chat['modified'], tsFormat: true); add(l10n.callInfoStatus, chat['status']); @@ -2508,6 +2551,7 @@ class _ChatInfoScreenState extends State final opts = chat['options'] as Map?; add(l10n.chatInfoRowOfficialGroup, opts?['OFFICIAL'] as bool?); add(l10n.chatInfoRowSignAdmin, opts?['SIGN_ADMIN'] as bool?); + _addChatOptions(add, opts); add(l10n.callInfoStatus, chat['status']); } @@ -2524,6 +2568,7 @@ class _ChatInfoScreenState extends State l10n.chatInfoRowOnlyAdmin, opts?['ONLY_ADMIN_CAN_ADD_MEMBER'] as bool?, ); + _addChatOptions(add, opts); add(l10n.callInfoStatus, chat['status']); } @@ -2572,6 +2617,36 @@ class _ChatInfoScreenState extends State ); } + void _addChatOptions( + void Function(String label, dynamic val, {bool tsFormat}) add, + Map? opts, + ) { + if (opts == null) return; + add(l10n.chatInfoRowDisableForward, opts['DISABLE_FORWARD'] as bool?); + add( + l10n.chatInfoRowCopyDisabled, + opts['MESSAGE_COPY_NOT_ALLOWED'] as bool?, + ); + add(l10n.chatInfoRowOnlyAdminCall, opts['ONLY_ADMIN_CAN_CALL'] as bool?); + add(l10n.chatInfoRowAllCanPin, opts['ALL_CAN_PIN_MESSAGE'] as bool?); + add( + l10n.chatInfoRowMembersSeeLink, + opts['MEMBERS_CAN_SEE_PRIVATE_LINK'] as bool?, + ); + add( + l10n.chatInfoRowConfirmBeforeSend, + opts['CONFIRM_BEFORE_SEND'] as bool?, + ); + add( + l10n.chatInfoRowOnlyOwnerIconTitle, + opts['ONLY_OWNER_CAN_CHANGE_ICON_TITLE'] as bool?, + ); + add( + l10n.chatInfoRowPromotedDisabled, + opts['PROMOTED_CONTENT_DISABLED'] as bool?, + ); + } + List<({String label, String value})> _buildExtraContactRows() { final c = _contactData; if (c == null) return const []; @@ -2646,6 +2721,13 @@ class _ChatInfoScreenState extends State ); } + Future _copyInfoValue(String value) async { + await Clipboard.setData(ClipboardData(text: value)); + if (!mounted) return; + Haptics.tap(); + showCustomNotification(context, l10n.msgActionsCopied); + } + Widget _infoRow( ColorScheme cs, String label, @@ -2658,22 +2740,26 @@ class _ChatInfoScreenState extends State crossAxisAlignment: CrossAxisAlignment.center, children: [ Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - label, - style: TextStyle(color: cs.onSurfaceVariant, fontSize: 10), - ), - Text( - value, - style: TextStyle( - color: cs.onSurface, - fontSize: 12, - fontWeight: FontWeight.w500, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onLongPress: () => unawaited(_copyInfoValue(value)), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle(color: cs.onSurfaceVariant, fontSize: 10), ), - ), - ], + Text( + value, + style: TextStyle( + color: cs.onSurface, + fontSize: 12, + fontWeight: FontWeight.w500, + ), + ), + ], + ), ), ), ?trailing, diff --git a/lib/frontend/screens/chats/chat_list_screen.dart b/lib/frontend/screens/chats/chat_list_screen.dart index d96754b..c4d0edd 100644 --- a/lib/frontend/screens/chats/chat_list_screen.dart +++ b/lib/frontend/screens/chats/chat_list_screen.dart @@ -84,10 +84,12 @@ import '../../../backend/modules/webapp.dart'; import '../../../models/story.dart'; import '../webapp/open_mini_app.dart'; import '../stories/story_ring.dart'; +import '../../widgets/attachment/bubbles/bubble_context.dart'; import '../../widgets/sending_clock_icon.dart'; import '../stories/story_viewer_screen.dart'; import '../downloads_screen.dart'; import '../../widgets/media_playback_pill.dart'; +import '../../../core/config/app_fonts.dart'; class _StoriesScrollPhysics extends BouncingScrollPhysics { final bool Function() blockPositive; @@ -1545,7 +1547,8 @@ class _ChatListScreenState extends State ) + 8) * (1.0 - _pullRatio), - height: FoldedStoryStack.outerSize, + height: + FoldedStoryStack.outerSize, child: OverflowBox( alignment: Alignment.centerLeft, maxWidth: @@ -1575,7 +1578,7 @@ class _ChatListScreenState extends State color: cs.onSurface, fontSize: 20, fontWeight: FontWeight.w600, - fontFamily: 'Outfit', + fontFamily: displayFontOf(context), ), ), ), @@ -1753,9 +1756,7 @@ class _ChatListScreenState extends State ), ), if (!widget.forwardMode) - const MediaPlaybackPill( - margin: EdgeInsets.fromLTRB(20, 6, 20, 2), - ), + const MediaPlaybackPill(margin: EdgeInsets.fromLTRB(20, 6, 20, 2)), if (!widget.forwardMode) _buildInformerBanner(), ], ), @@ -2407,7 +2408,7 @@ class _ChatListScreenState extends State color: cs.onSurface, fontSize: 20, fontWeight: FontWeight.w600, - fontFamily: 'Outfit', + fontFamily: displayFontOf(context), ), ), ], @@ -2687,6 +2688,8 @@ class _ChatListScreenState extends State return oneLine.isEmpty ? null : oneLine; } + static const double _ownStatusIconSize = 14; + String? _ownStatusFor(CachedChat chat, bool isPlaceholder) { if (isPlaceholder || chat.id == 0) return null; final me = _profile?.id; @@ -2695,31 +2698,19 @@ class _ChatListScreenState extends State } Widget _ownStatusIcon(ColorScheme cs, String status, bool read) { - if (isSendingStatus(status)) { - return SendingClockIcon(color: cs.outline, size: 14); - } - IconData icon; - Color color; - switch (status) { - case 'sending': - case 'pending': - icon = Symbols.schedule; - color = cs.outline; - case 'error': - icon = Symbols.error; - color = Colors.redAccent; - default: - if (read) { - icon = Symbols.done_all; - color = kReadReceiptBlue; - } else { - icon = Symbols.check; - color = cs.outline; - } - } + final sending = isSendingStatus(status); + final effective = (read && !sending && status != 'error') ? 'read' : status; + final visual = messageStatusVisual(effective, dimColor: cs.outline); return Padding( padding: const EdgeInsets.only(left: 6), - child: Icon(icon, size: 16, color: color, fill: 1), + child: sending + ? SendingClockIcon(color: visual.color, size: _ownStatusIconSize) + : Icon( + visual.icon, + size: _ownStatusIconSize, + color: visual.color, + weight: 400, + ), ); } diff --git a/lib/frontend/screens/chats/chat_screen.dart b/lib/frontend/screens/chats/chat_screen.dart index 57ba193..798aadc 100644 --- a/lib/frontend/screens/chats/chat_screen.dart +++ b/lib/frontend/screens/chats/chat_screen.dart @@ -118,6 +118,8 @@ import 'chat/retain_offset_physics.dart'; import 'profile_action_sheets.dart'; import '../../../core/media/media_playback.dart'; import '../../widgets/media_playback_pill.dart'; +import '../../../core/config/app_fonts.dart'; +import '../../../core/config/app_shape.dart'; class _DateSeparatorItem { final DateTime date; @@ -598,7 +600,9 @@ class _ChatScreenState extends State ChatChromeStyle get _effectiveChrome { final chrome = AppChatChrome.current.value; - if (chrome == ChatChromeStyle.liquidGlass) return ChatChromeStyle.transparent; + if (chrome == ChatChromeStyle.liquidGlass) { + return ChatChromeStyle.transparent; + } if (_wallpaper != null && chrome == ChatChromeStyle.none) { return ChatChromeStyle.blur; } @@ -1612,7 +1616,9 @@ class _ChatScreenState extends State if (_historyAutoloadSuppressed) return; if (_isLoading) return; if (_commentsMode) { - if (_commentsLoadingMore || !_commentsHasMore || _messages.isEmpty) return; + if (_commentsLoadingMore || !_commentsHasMore || _messages.isEmpty) { + return; + } final pos = _scrollController.position; if (pos.pixels - pos.minScrollExtent <= _historyPrefetchExtent) { unawaited(_loadMoreComments()); @@ -1692,7 +1698,10 @@ class _ChatScreenState extends State return null; } - Future _holdScrollAfterAppend(String? anchorId, double? beforeDy) async { + Future _holdScrollAfterAppend( + String? anchorId, + double? beforeDy, + ) async { if (anchorId == null || beforeDy == null) return; await WidgetsBinding.instance.endOfFrame; if (!mounted || !_scrollController.hasClients) return; @@ -2218,7 +2227,11 @@ class _ChatScreenState extends State String? _effectiveStatus(CachedMessage msg) { if (msg.senderId != _myId) return null; - if (msg.status == 'sending' || msg.status == 'error') return msg.status; + if (msg.status == 'sending' || + msg.status == 'pending' || + msg.status == 'error') { + return msg.status; + } return 'sent'; } @@ -2692,6 +2705,7 @@ class _ChatScreenState extends State selected: selected, onReply: _replySelected, onForward: _forwardSelected, + allowForward: !(chat?.forwardDisabled ?? false), ), ), ), @@ -2699,7 +2713,7 @@ class _ChatScreenState extends State ); Widget wrapChrome(Widget child) { if (_composerFrosted) { - if (AppComposerStyle.current.value != ComposerStyle.materialYou) { + if (ComposerChrome.isGlossy(AppComposerStyle.current.value)) { return child; } return _FrostedPanel( @@ -2778,7 +2792,7 @@ class _ChatScreenState extends State color: cs.onSurface, fontSize: 18, fontWeight: FontWeight.w600, - fontFamily: 'Outfit', + fontFamily: displayFontOf(context), ), ), const SizedBox(height: 16), @@ -2927,6 +2941,7 @@ class _ChatScreenState extends State builder: (ctx, setLocalState) { return AlertDialog( backgroundColor: cs.surfaceContainerHigh, + shape: AppShape.dialogBorder, title: const Text('Удалить сообщение'), content: Column( mainAxisSize: MainAxisSize.min, @@ -3183,7 +3198,8 @@ class _ChatScreenState extends State headerStatus: _headerStatusNotifier, scheduledCount: _scheduledCount, otherUnread: _otherUnread, - showCall: !_commentsMode && + showCall: + !_commentsMode && widget.chatType == 'DIALOG' && !_peerIsBot, onClose: widget.onClose, @@ -3868,6 +3884,16 @@ class _ChatScreenState extends State } } + if (chat?.confirmBeforeSend ?? false) { + final l10n = AppLocalizations.of(context)!; + final confirmed = await showConfirmDialog( + context, + message: l10n.chatSendConfirmMessage, + confirmLabel: l10n.chatSendConfirmAction, + ); + if (!confirmed || !mounted) return; + } + final wireText = await _encryptOutgoing(text); if (wireText == null || !mounted) return; final encrypted = wireText != text; @@ -4028,6 +4054,9 @@ class _ChatScreenState extends State } return; } + final failed = isPermanentSendFailure(e); + final status = failed ? 'error' : 'pending'; + if (failed) logger.w('Отправка отклонена сервером: $e'); final index = _messages.indexWhere((m) => m.id == tempId); if (index != -1 && mounted) { final queued = CachedMessage( @@ -4037,7 +4066,7 @@ class _ChatScreenState extends State senderId: _myId, text: text, time: now, - status: 'pending', + status: status, payload: composedPayload, ); _messages[index] = queued; @@ -4051,7 +4080,7 @@ class _ChatScreenState extends State messageId: tempId, time: now, text: text, - status: 'pending', + status: status, elements: elements, ), ); @@ -5586,9 +5615,7 @@ class _ChatScreenState extends State final cs = Theme.of(context).colorScheme; return Padding( padding: const EdgeInsets.symmetric(vertical: 12), - child: Center( - child: SmallSpinner(size: 22, color: cs.onSurfaceVariant), - ), + child: Center(child: SmallSpinner(size: 22, color: cs.onSurfaceVariant)), ); } @@ -5670,7 +5697,8 @@ class _ChatScreenState extends State final bool isChannelPost = !_commentsMode && - (chat?.type ?? widget.chatType) == 'CHANNEL' && + (chat?.type ?? widget.chatType) == + 'CHANNEL' && !message.isControl; final bool isCommentedPost = _commentsMode && @@ -5746,9 +5774,12 @@ class _ChatScreenState extends State onReply: message.isControl ? null : () => _startReply(message), - onForward: message.isControl + onForward: + message.isControl || + (chat?.forwardDisabled ?? false) ? null : () => _forwardMessages([message]), + allowCopy: !(chat?.copyDisabled ?? false), onMarkUnread: message.isControl ? null : () => _markMessageUnread(message), @@ -6135,7 +6166,10 @@ class _ChatScreenState extends State FileAttachment(fileId: fileId), ).id; try { - final realId = await messagesModule.sendFileMessage(widget.chatId, fileId); + final realId = await messagesModule.sendFileMessage( + widget.chatId, + fileId, + ); final ok = realId != null; if (!mounted) return ok; if (ok) { @@ -6271,7 +6305,9 @@ class _ChatScreenState extends State var durationMs = video.item.duration?.inMilliseconds; if (durationMs == null && DesktopVideoProbe.supported) { - durationMs = (await DesktopVideoProbe.duration(file.path))?.inMilliseconds; + durationMs = (await DesktopVideoProbe.duration( + file.path, + ))?.inMilliseconds; } final dims = await video.item.dimensions(); Uint8List? thumbBytes; @@ -6949,9 +6985,7 @@ class _PinnedMessageBanner extends StatelessWidget { if (frosted) { return GlassSurface( liquid: liquid, - borderRadius: floating - ? BorderRadius.circular(16) - : BorderRadius.zero, + borderRadius: floating ? BorderRadius.circular(16) : BorderRadius.zero, frostTint: Colors.transparent, border: floating ? null : bottomBorder, backdropKey: backdropKey, @@ -7059,6 +7093,7 @@ class _SelectableMessageRow extends StatefulWidget { final VoidCallback? onEdit; final VoidCallback? onReply; final VoidCallback? onForward; + final bool allowCopy; final VoidCallback? onMarkUnread; final VoidCallback? onPin; final bool Function() isPinned; @@ -7083,6 +7118,7 @@ class _SelectableMessageRow extends StatefulWidget { this.onEdit, this.onReply, this.onForward, + this.allowCopy = true, this.onMarkUnread, this.onPin, required this.isPinned, @@ -7153,6 +7189,7 @@ class _SelectableMessageRowState extends State<_SelectableMessageRow> { onEdit: widget.onEdit, onReply: widget.onReply, onForward: widget.onForward, + allowCopy: widget.allowCopy, onMarkUnread: widget.onMarkUnread, onPin: widget.onPin, isPinned: _isPinnedNow(), @@ -7221,6 +7258,7 @@ class _SelectableMessageRowState extends State<_SelectableMessageRow> { onEdit: widget.onEdit, onReply: widget.onReply, onForward: widget.onForward, + allowCopy: widget.allowCopy, onMarkUnread: widget.onMarkUnread, onPin: widget.onPin, isPinned: _isPinnedNow(), diff --git a/lib/frontend/screens/chats/chat_wallpaper_preview_screen.dart b/lib/frontend/screens/chats/chat_wallpaper_preview_screen.dart index ede2389..aaa342c 100644 --- a/lib/frontend/screens/chats/chat_wallpaper_preview_screen.dart +++ b/lib/frontend/screens/chats/chat_wallpaper_preview_screen.dart @@ -6,6 +6,8 @@ import 'package:material_symbols_icons/symbols.dart'; import 'package:komet/core/storage/chat_wallpaper_store.dart'; import 'package:komet/frontend/widgets/chat_wallpaper_view.dart'; +import '../../../core/config/app_frost.dart'; +import '../../../core/config/app_fonts.dart'; class ChatWallpaperPreviewScreen extends StatefulWidget { final Uint8List imageBytes; @@ -100,13 +102,13 @@ class _ChatWallpaperPreviewScreenState icon: const Icon(Symbols.arrow_back, color: Colors.white), onPressed: () => Navigator.pop(context), ), - const Text( + Text( 'Обои', style: TextStyle( color: Colors.white, fontSize: 22, fontWeight: FontWeight.w700, - fontFamily: 'Outfit', + fontFamily: displayFontOf(context), ), ), ], @@ -158,7 +160,10 @@ class _Frosted extends StatelessWidget { return ClipRRect( borderRadius: BorderRadius.circular(radius), child: BackdropFilter( - filter: ui.ImageFilter.blur(sigmaX: 24, sigmaY: 24), + filter: ui.ImageFilter.blur( + sigmaX: AppFrost.panelSigma, + sigmaY: AppFrost.panelSigma, + ), child: DecoratedBox( decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.14), @@ -258,7 +263,7 @@ class _DimLabel extends StatelessWidget { color: color, fontSize: 16, fontWeight: FontWeight.w600, - fontFamily: 'Outfit', + fontFamily: displayFontOf(context), ); return Padding( padding: const EdgeInsets.symmetric(horizontal: 16), @@ -279,7 +284,8 @@ class _RevealClipper extends CustomClipper { const _RevealClipper(this.fraction); @override - Rect getClip(Size size) => Rect.fromLTWH(0, 0, size.width * fraction, size.height); + Rect getClip(Size size) => + Rect.fromLTWH(0, 0, size.width * fraction, size.height); @override bool shouldReclip(_RevealClipper oldClipper) => @@ -324,11 +330,11 @@ class _ToggleChip extends StatelessWidget { const SizedBox(width: 10), Text( label, - style: const TextStyle( + style: TextStyle( color: Colors.white, fontSize: 16, fontWeight: FontWeight.w600, - fontFamily: 'Outfit', + fontFamily: displayFontOf(context), ), ), ], @@ -350,7 +356,7 @@ class _ApplyButton extends StatelessWidget { onTap: onTap, child: _Frosted( radius: 26, - child: const SizedBox( + child: SizedBox( height: 52, child: Center( child: Text( @@ -359,7 +365,7 @@ class _ApplyButton extends StatelessWidget { color: Colors.white, fontSize: 17, fontWeight: FontWeight.w700, - fontFamily: 'Outfit', + fontFamily: displayFontOf(context), ), ), ), @@ -384,6 +390,7 @@ class _SamplePreview extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _bubble( + context, text: 'Как насчёт новых обоев для этого чата?', color: cs.surfaceContainerHighest.withValues(alpha: 0.92), textColor: cs.onSurface, @@ -391,6 +398,7 @@ class _SamplePreview extends StatelessWidget { ), const SizedBox(height: 8), _bubble( + context, text: 'Отличная идея.', color: cs.primary, textColor: cs.onPrimary, @@ -402,7 +410,8 @@ class _SamplePreview extends StatelessWidget { ); } - Widget _bubble({ + Widget _bubble( + BuildContext context, { required String text, required Color color, required Color textColor, @@ -423,7 +432,7 @@ class _SamplePreview extends StatelessWidget { style: TextStyle( color: textColor, fontSize: 15, - fontFamily: 'Outfit', + fontFamily: displayFontOf(context), ), ), ), diff --git a/lib/frontend/screens/chats/profile_action_sheets.dart b/lib/frontend/screens/chats/profile_action_sheets.dart index c9b1cef..f6cfd2a 100644 --- a/lib/frontend/screens/chats/profile_action_sheets.dart +++ b/lib/frontend/screens/chats/profile_action_sheets.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import '../contacts/contact_sheet_common.dart'; +import '../../../core/config/app_fonts.dart'; class ConfirmChoice { final bool confirmed; @@ -135,7 +136,7 @@ class _ConfirmCardState extends State<_ConfirmCard> { color: cs.onSurface, fontSize: 18, fontWeight: FontWeight.w700, - fontFamily: 'Outfit', + fontFamily: displayFontOf(context), ), ), const SizedBox(height: 8), @@ -159,8 +160,7 @@ class _ConfirmCardState extends State<_ConfirmCard> { Checkbox( value: _checked, visualDensity: VisualDensity.compact, - materialTapTargetSize: - MaterialTapTargetSize.shrinkWrap, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, onChanged: (v) => setState(() => _checked = v ?? false), ), const SizedBox(width: 8), @@ -195,9 +195,9 @@ class _ConfirmCardState extends State<_ConfirmCard> { foregroundColor: cs.onErrorContainer, ) : null, - onPressed: () => Navigator.of(context).pop( - ConfirmChoice(confirmed: true, checked: _checked), - ), + onPressed: () => Navigator.of( + context, + ).pop(ConfirmChoice(confirmed: true, checked: _checked)), child: Text(widget.confirmLabel), ), ], @@ -282,7 +282,7 @@ class _ComplaintCardState extends State<_ComplaintCard> { color: cs.onSurface, fontSize: 18, fontWeight: FontWeight.w700, - fontFamily: 'Outfit', + fontFamily: displayFontOf(context), ), ), const SizedBox(height: 6), diff --git a/lib/frontend/screens/chats/scheduled_messages_screen.dart b/lib/frontend/screens/chats/scheduled_messages_screen.dart index cbc9767..783b174 100644 --- a/lib/frontend/screens/chats/scheduled_messages_screen.dart +++ b/lib/frontend/screens/chats/scheduled_messages_screen.dart @@ -19,6 +19,7 @@ import '../../widgets/schedule_time_picker.dart'; import '../../widgets/sheet_helpers.dart'; import '../../widgets/small_spinner.dart'; import '../../widgets/reload_on_reconnect.dart'; +import '../../../core/config/app_fonts.dart'; class ScheduledMessagesScreen extends StatefulWidget { final int chatId; @@ -117,7 +118,7 @@ class _ScheduledMessagesScreenState extends State color: cs.onSurface, fontSize: 18, fontWeight: FontWeight.w600, - fontFamily: 'Outfit', + fontFamily: displayFontOf(context), ), ), const SizedBox(height: 16), @@ -249,7 +250,7 @@ class _ScheduledMessagesScreenState extends State style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, - fontFamily: 'Outfit', + fontFamily: displayFontOf(context), ), ), Text( diff --git a/lib/frontend/screens/contacts/add_contact_sheet.dart b/lib/frontend/screens/contacts/add_contact_sheet.dart index a9d5712..4645c53 100644 --- a/lib/frontend/screens/contacts/add_contact_sheet.dart +++ b/lib/frontend/screens/contacts/add_contact_sheet.dart @@ -10,6 +10,7 @@ import 'package:komet/frontend/screens/contacts/open_contact_profile.dart'; import 'package:komet/frontend/widgets/custom_notification.dart'; import 'package:komet/l10n/app_localizations.dart'; import 'package:komet/main.dart'; +import 'package:material_symbols_icons/symbols.dart'; Future showAddContactSheet(BuildContext context) { return showBlurredCard( @@ -209,7 +210,7 @@ class _AddContactCardState extends State<_AddContactCard> { ), ), Icon( - Icons.keyboard_arrow_down, + Symbols.keyboard_arrow_down, size: 20, color: cs.onSurfaceVariant, ), @@ -297,7 +298,10 @@ class _AddContactCardState extends State<_AddContactCard> { ) : Text( l10n.addContactSave, - style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600), + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), ), ), ); @@ -339,5 +343,4 @@ class _AddContactCardState extends State<_AddContactCard> { ), ); } - } diff --git a/lib/frontend/screens/contacts/contact_sheet_common.dart b/lib/frontend/screens/contacts/contact_sheet_common.dart index 114e7bc..4ae2861 100644 --- a/lib/frontend/screens/contacts/contact_sheet_common.dart +++ b/lib/frontend/screens/contacts/contact_sheet_common.dart @@ -2,6 +2,8 @@ import 'dart:ui'; import 'package:flutter/material.dart'; +import '../../../core/config/app_frost.dart'; + Future showBlurredCard( BuildContext context, Widget Function(BuildContext hostContext) builder, @@ -10,13 +12,16 @@ Future showBlurredCard( context: context, barrierDismissible: true, barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel, - barrierColor: Colors.black.withValues(alpha: 0.28), + barrierColor: AppFrost.scrim(), transitionDuration: const Duration(milliseconds: 260), pageBuilder: (_, _, _) => builder(context), transitionBuilder: (_, anim, _, child) { final t = Curves.easeOutCubic.transform(anim.value); return BackdropFilter( - filter: ImageFilter.blur(sigmaX: 14 * t, sigmaY: 14 * t), + filter: ImageFilter.blur( + sigmaX: AppFrost.overlaySigma * t, + sigmaY: AppFrost.overlaySigma * t, + ), child: Opacity( opacity: anim.value, child: Transform.scale(scale: 0.94 + 0.06 * t, child: child), diff --git a/lib/frontend/screens/contacts/contacts_tab.dart b/lib/frontend/screens/contacts/contacts_tab.dart index 1515165..3194469 100644 --- a/lib/frontend/screens/contacts/contacts_tab.dart +++ b/lib/frontend/screens/contacts/contacts_tab.dart @@ -19,6 +19,9 @@ import '../../widgets/springy_tap.dart'; import '../chats/chat_info_screen.dart'; import 'nfc_exchange_sheet.dart'; import 'open_contact_profile.dart'; +import '../../../core/config/app_frost.dart'; +import '../../../core/config/app_fonts.dart'; +import '../../../core/config/app_shape.dart'; enum _SearchMode { phone, id } @@ -57,7 +60,7 @@ class _ContactsTabState extends State with SpectrumSurface { context: context, barrierDismissible: true, barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel, - barrierColor: Colors.black54, + barrierColor: AppFrost.scrim(), transitionDuration: const Duration(milliseconds: 320), pageBuilder: (_, _, _) => const Align( alignment: Alignment.topCenter, @@ -240,7 +243,7 @@ class _ContactsTabState extends State with SpectrumSurface { color: cs.onSurface, fontSize: 24, fontWeight: FontWeight.w700, - fontFamily: 'Outfit', + fontFamily: displayFontOf(context), ), ), const ConnectionStatusLine(), @@ -495,9 +498,7 @@ class _SearchContactSheetState extends State<_SearchContactSheet> { selected: {_mode}, onSelectionChanged: (s) => _setMode(s.first), showSelectedIcon: false, - style: ButtonStyle( - visualDensity: VisualDensity.compact, - ), + style: ButtonStyle(visualDensity: VisualDensity.compact), ), const SizedBox(height: 12), TextField( @@ -570,9 +571,7 @@ class _SearchContactSheetState extends State<_SearchContactSheet> { FilledButton( onPressed: _loading ? null : _submit, style: FilledButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(14), - ), + shape: AppShape.buttonBorder, padding: const EdgeInsets.symmetric(vertical: 14), ), child: _loading diff --git a/lib/frontend/screens/contacts/edit_contact_sheet.dart b/lib/frontend/screens/contacts/edit_contact_sheet.dart index 55a653c..09cde8b 100644 --- a/lib/frontend/screens/contacts/edit_contact_sheet.dart +++ b/lib/frontend/screens/contacts/edit_contact_sheet.dart @@ -6,6 +6,8 @@ import 'package:komet/frontend/widgets/custom_notification.dart'; import 'package:komet/frontend/widgets/komet_avatar.dart'; import 'package:komet/l10n/app_localizations.dart'; import 'package:komet/main.dart'; +import 'package:material_symbols_icons/symbols.dart'; +import '../../../core/config/app_shape.dart'; enum EditContactAction { updated, removed } @@ -132,6 +134,7 @@ class _EditContactCardState extends State<_EditContactCard> { final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( + shape: AppShape.dialogBorder, title: Text(l10n.editContactDeleteConfirmTitle), content: Text(l10n.editContactDeleteConfirmBody), actions: [ @@ -158,9 +161,9 @@ class _EditContactCardState extends State<_EditContactCard> { return; } - Navigator.of(context).pop( - const EditContactResult(EditContactAction.removed), - ); + Navigator.of( + context, + ).pop(const EditContactResult(EditContactAction.removed)); } @override @@ -260,14 +263,12 @@ class _EditContactCardState extends State<_EditContactCard> { ), if (hasText) InkWell( - onTap: _busy - ? null - : () => setState(() => controller.clear()), + onTap: _busy ? null : () => setState(() => controller.clear()), borderRadius: BorderRadius.circular(20), child: Padding( padding: const EdgeInsets.all(4), child: Icon( - Icons.close, + Symbols.close, size: 18, color: cs.onSurfaceVariant, ), @@ -294,7 +295,10 @@ class _EditContactCardState extends State<_EditContactCard> { ) : Text( l10n.editContactSave, - style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600), + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), ), ), ); @@ -313,14 +317,19 @@ class _EditContactCardState extends State<_EditContactCard> { ? SizedBox( width: 20, height: 20, - child: CircularProgressIndicator(strokeWidth: 2, color: cs.error), + child: CircularProgressIndicator( + strokeWidth: 2, + color: cs.error, + ), ) : Text( l10n.editContactDelete, - style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600), + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), ), ), ); } - } diff --git a/lib/frontend/screens/contacts/nfc_exchange_sheet.dart b/lib/frontend/screens/contacts/nfc_exchange_sheet.dart index 0384dd0..7aa094e 100644 --- a/lib/frontend/screens/contacts/nfc_exchange_sheet.dart +++ b/lib/frontend/screens/contacts/nfc_exchange_sheet.dart @@ -17,6 +17,7 @@ import '../../../models/contact_info.dart'; import '../../widgets/custom_notification.dart'; import '../../widgets/komet_avatar.dart'; import '../../widgets/small_spinner.dart'; +import '../../../core/config/app_shape.dart'; enum _Stage { checking, @@ -159,7 +160,10 @@ class _NfcExchangeSheetState extends State ); if (!mounted) return; setState(() => _stage = _Stage.added); - showCustomNotification(context, AppLocalizations.of(context)!.nfcContactAdded); + showCustomNotification( + context, + AppLocalizations.of(context)!.nfcContactAdded, + ); await Future.delayed(const Duration(milliseconds: 700)); if (mounted) Navigator.pop(context); } catch (e) { @@ -412,7 +416,8 @@ class _NfcExchangeSheetState extends State ), const SizedBox(height: 4), Text( - formatPhone(_peerPhone) ?? l10n.nfcPeerIdFallback('${_peerId ?? ''}'), + formatPhone(_peerPhone) ?? + l10n.nfcPeerIdFallback('${_peerId ?? ''}'), style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13), ), const SizedBox(height: 24), @@ -421,9 +426,7 @@ class _NfcExchangeSheetState extends State child: FilledButton( onPressed: (_stage == _Stage.adding || loading) ? null : _add, style: FilledButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(14), - ), + shape: AppShape.buttonBorder, padding: const EdgeInsets.symmetric(vertical: 14), ), child: _stage == _Stage.adding diff --git a/lib/frontend/screens/downloads_screen.dart b/lib/frontend/screens/downloads_screen.dart index 4c32fd8..1a77791 100644 --- a/lib/frontend/screens/downloads_screen.dart +++ b/lib/frontend/screens/downloads_screen.dart @@ -14,6 +14,7 @@ import '../widgets/chat_menu_overlay.dart'; import '../widgets/confirm_dialog.dart'; import '../widgets/custom_notification.dart'; import '../widgets/small_spinner.dart'; +import '../widgets/sheet_helpers.dart'; class DownloadsScreen extends StatefulWidget { const DownloadsScreen({super.key}); @@ -116,9 +117,7 @@ class _DownloadsScreenState extends State { final clear = await showModalBottomSheet( context: context, backgroundColor: Theme.of(context).colorScheme.surfaceContainerHigh, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(22)), - ), + shape: kSheetShape, builder: (sheetContext) => SafeArea( child: Padding( padding: const EdgeInsets.symmetric(vertical: 10), diff --git a/lib/frontend/screens/profile/app_icon_screen.dart b/lib/frontend/screens/profile/app_icon_screen.dart index 31a667b..7db13fe 100644 --- a/lib/frontend/screens/profile/app_icon_screen.dart +++ b/lib/frontend/screens/profile/app_icon_screen.dart @@ -5,8 +5,8 @@ import '../../widgets/connection_status.dart'; import '../../../core/config/app_icon.dart'; import '../../../core/utils/haptics.dart'; import '../../widgets/custom_notification.dart'; -import '../../widgets/glossy_pill.dart'; import '../../widgets/settings_radio_tile.dart'; +import '../../widgets/settings_card.dart'; class AppIconScreen extends StatefulWidget { const AppIconScreen({super.key}); @@ -57,11 +57,8 @@ class _AppIconScreenState extends State { physics: const BouncingScrollPhysics(), padding: const EdgeInsets.fromLTRB(16, 12, 16, 120), children: [ - GlossyPill( - color: cs.surfaceContainerHigh, - borderRadius: BorderRadius.circular(28), + SettingsPanel( padding: const EdgeInsets.fromLTRB(20, 18, 20, 12), - depth: 6, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/lib/frontend/screens/profile/appearance_screen.dart b/lib/frontend/screens/profile/appearance_screen.dart index 5d4497d..31e2222 100644 --- a/lib/frontend/screens/profile/appearance_screen.dart +++ b/lib/frontend/screens/profile/appearance_screen.dart @@ -19,8 +19,9 @@ import '../../../core/utils/debouncer.dart'; import '../../../core/utils/haptics.dart'; import '../../../l10n/app_localizations.dart'; import '../../../main.dart'; -import '../../widgets/glossy_pill.dart'; import '../../widgets/liquid_glass.dart'; +import '../../widgets/settings_card.dart'; +import '../../../core/config/app_shape.dart'; class AppearanceScreen extends StatefulWidget { const AppearanceScreen({super.key}); @@ -141,7 +142,9 @@ class _AppearanceScreenState extends State { void _applyVisualStyle(VisualStyle style) { AppVisualStyle.save(style); if (style == VisualStyle.liquidGlass) { - AppNavPillStyle.save(NavPillStyle.liquidGlass); + if (AppNavPillStyle.current.value != NavPillStyle.auto) { + AppNavPillStyle.save(NavPillStyle.liquidGlass); + } AppComposerBackground.save(ComposerBackground.liquidGlass); AppChatChrome.save(ChatChromeStyle.liquidGlass); return; @@ -164,11 +167,7 @@ class _VisualStyleCard extends StatelessWidget { 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, + return SettingsPanel( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -233,11 +232,7 @@ class _ChatChromeCard extends StatelessWidget { 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, + return SettingsPanel( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -314,11 +309,7 @@ class _ComposerBarCard extends StatelessWidget { 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, + return SettingsPanel( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -339,24 +330,32 @@ class _ComposerBarCard extends StatelessWidget { 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); - } - }, + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: SegmentedButton( + showSelectedIcon: false, + segments: [ + ButtonSegment( + value: ComposerStyle.auto, + label: Text(l10n.appearanceStyleAuto), + ), + 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); + } + }, + ), ); }, ), @@ -409,11 +408,7 @@ class _NavPillStyleCard extends StatelessWidget { 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, + return SettingsPanel( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -439,30 +434,37 @@ class _NavPillStyleCard extends StatelessWidget { !LiquidGlass.isSupported ? NavPillStyle.frostBlur : current; - return SegmentedButton( - showSelectedIcon: false, - segments: [ - ButtonSegment( - value: NavPillStyle.glossy, - label: Text(l10n.appearanceNavPillGlossy), - ), - ButtonSegment( - value: NavPillStyle.frostBlur, - label: Text(l10n.appearanceNavPillFrost), - ), - if (LiquidGlass.isSupported) + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: SegmentedButton( + showSelectedIcon: false, + segments: [ ButtonSegment( - value: NavPillStyle.liquidGlass, - label: Text(l10n.appearanceGlassMaterial), + value: NavPillStyle.auto, + label: Text(l10n.appearanceStyleAuto), ), - ], - selected: {selectable}, - onSelectionChanged: (set) { - if (set.isNotEmpty) { - Haptics.selection(); - AppNavPillStyle.save(set.first); - } - }, + ButtonSegment( + value: NavPillStyle.glossy, + label: Text(l10n.appearanceNavPillGlossy), + ), + ButtonSegment( + value: NavPillStyle.frostBlur, + label: Text(l10n.appearanceNavPillFrost), + ), + if (LiquidGlass.isSupported) + ButtonSegment( + value: NavPillStyle.liquidGlass, + label: Text(l10n.appearanceGlassMaterial), + ), + ], + selected: {selectable}, + onSelectionChanged: (set) { + if (set.isNotEmpty) { + Haptics.selection(); + AppNavPillStyle.save(set.first); + } + }, + ), ); }, ), @@ -479,11 +481,8 @@ class _GradientToggleCard extends StatelessWidget { Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final l10n = AppLocalizations.of(context)!; - return GlossyPill( - color: cs.surfaceContainerHigh, - borderRadius: BorderRadius.circular(28), + return SettingsPanel( padding: const EdgeInsets.fromLTRB(20, 14, 12, 14), - depth: 6, child: Row( children: [ Icon(Symbols.blur_on, color: cs.onSurface, size: 24, weight: 500), @@ -531,11 +530,8 @@ class _SpectrumToggleCard extends StatelessWidget { Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final l10n = AppLocalizations.of(context)!; - return GlossyPill( - color: cs.surfaceContainerHigh, - borderRadius: BorderRadius.circular(28), + return SettingsPanel( padding: const EdgeInsets.fromLTRB(20, 14, 12, 14), - depth: 6, child: Row( children: [ Icon(Symbols.graphic_eq, color: cs.onSurface, size: 24, weight: 500), @@ -675,11 +671,9 @@ class _ChatPreview extends StatelessWidget { builder: (context, _) { final style = AppBubbleShape.current.value; final behavior = AppBubbleBehavior.current.value; - return GlossyPill( + return SettingsPanel( color: cs.surfaceContainerLow, - borderRadius: BorderRadius.circular(28), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 20), - depth: 6, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ @@ -781,10 +775,7 @@ class _ColorPickerCard extends StatelessWidget { ) { final swatchColor = sys ? cs.primary : col; - return GlossyPill( - color: cs.surfaceContainerHigh, - borderRadius: BorderRadius.circular(28), - depth: 6, + return SettingsPanel( child: Column( children: [ InkWell( @@ -863,9 +854,7 @@ class _ColorPickerCard extends StatelessWidget { child: FilledButton.tonal( onPressed: sys ? null : onReset, style: FilledButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(14), - ), + shape: AppShape.buttonBorder, ), child: Row( mainAxisSize: MainAxisSize.min, @@ -906,11 +895,7 @@ class _BubbleShapeCard extends StatelessWidget { 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, + return SettingsPanel( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -967,11 +952,7 @@ class _BubbleBehaviorCard extends StatelessWidget { 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, + return SettingsPanel( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/lib/frontend/screens/profile/chat_background_screen.dart b/lib/frontend/screens/profile/chat_background_screen.dart index 0a10d4a..bde3f98 100644 --- a/lib/frontend/screens/profile/chat_background_screen.dart +++ b/lib/frontend/screens/profile/chat_background_screen.dart @@ -8,6 +8,8 @@ import '../../widgets/chat_wallpaper_sheet.dart'; import '../../widgets/chat_wallpaper_view.dart'; import '../../widgets/custom_notification.dart'; import '../chats/chat_wallpaper_preview_screen.dart'; +import '../../../core/config/app_fonts.dart'; +import '../../../core/config/app_shape.dart'; class ChatBackgroundScreen extends StatefulWidget { const ChatBackgroundScreen({super.key}); @@ -33,8 +35,10 @@ class _ChatBackgroundScreenState extends State { if (!mounted) return; setState(() { _accountId = profile?.id ?? 0; - _wallpaper = ChatWallpaperStore.instance - .get(_accountId, kGlobalWallpaperChatId); + _wallpaper = ChatWallpaperStore.instance.get( + _accountId, + kGlobalWallpaperChatId, + ); _ready = true; }); } @@ -42,8 +46,10 @@ class _ChatBackgroundScreenState extends State { void _refresh() { if (!mounted) return; setState(() { - _wallpaper = ChatWallpaperStore.instance - .get(_accountId, kGlobalWallpaperChatId); + _wallpaper = ChatWallpaperStore.instance.get( + _accountId, + kGlobalWallpaperChatId, + ); }); } @@ -109,12 +115,12 @@ class _ChatBackgroundScreenState extends State { appBar: AppBar( backgroundColor: cs.surface, surfaceTintColor: Colors.transparent, - title: const Text( + title: Text( 'Фон чатов', style: TextStyle( fontSize: 22, fontWeight: FontWeight.w700, - fontFamily: 'Outfit', + fontFamily: displayFontOf(context), ), ), ), @@ -134,7 +140,7 @@ class _ChatBackgroundScreenState extends State { return Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 12), child: ClipRRect( - borderRadius: BorderRadius.circular(28), + borderRadius: AppShape.cardRadius, child: Stack( fit: StackFit.expand, children: [ @@ -222,7 +228,7 @@ class _ChatBackgroundScreenState extends State { color: cs.onPrimary, fontSize: 16, fontWeight: FontWeight.w700, - fontFamily: 'Outfit', + fontFamily: displayFontOf(context), ), ), ), @@ -250,6 +256,7 @@ class _SampleBubbles extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _bubble( + context, text: 'Единый фон для всех чатов', color: cs.surfaceContainerHighest.withValues(alpha: 0.94), textColor: cs.onSurface, @@ -257,6 +264,7 @@ class _SampleBubbles extends StatelessWidget { ), const SizedBox(height: 8), _bubble( + context, text: 'Красиво ✨', color: cs.primary, textColor: cs.onPrimary, @@ -268,7 +276,8 @@ class _SampleBubbles extends StatelessWidget { ); } - Widget _bubble({ + Widget _bubble( + BuildContext context, { required String text, required Color color, required Color textColor, @@ -289,7 +298,7 @@ class _SampleBubbles extends StatelessWidget { style: TextStyle( color: textColor, fontSize: 15, - fontFamily: 'Outfit', + fontFamily: displayFontOf(context), ), ), ), diff --git a/lib/frontend/screens/profile/cloud_storage_screen.dart b/lib/frontend/screens/profile/cloud_storage_screen.dart index 3a797a7..979ab41 100644 --- a/lib/frontend/screens/profile/cloud_storage_screen.dart +++ b/lib/frontend/screens/profile/cloud_storage_screen.dart @@ -20,6 +20,7 @@ import '../../widgets/custom_notification.dart'; import '../../widgets/glossy_pill.dart'; import '../../widgets/sheet_helpers.dart'; import '../../widgets/small_spinner.dart'; +import '../../../core/config/app_shape.dart'; enum _EnvState { loading, notConfigured, ready } @@ -416,9 +417,7 @@ class _CloudStorageScreenState extends State horizontal: 32, vertical: 14, ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(14), - ), + shape: AppShape.buttonBorder, ), child: _isCreatingEnv ? SmallSpinner(size: 18, color: cs.onPrimary) @@ -608,9 +607,7 @@ class _CloudStorageScreenState extends State horizontal: 32, vertical: 14, ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(14), - ), + shape: AppShape.buttonBorder, ), child: Text( l10n.cloudStorageUpload, @@ -1037,7 +1034,9 @@ class _FileDetailsSheetState extends State<_FileDetailsSheet> { return Container( decoration: BoxDecoration( color: cs.surface, - borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), + borderRadius: const BorderRadius.vertical( + top: Radius.circular(AppShape.sheet), + ), ), padding: EdgeInsets.fromLTRB( 24, @@ -1199,7 +1198,9 @@ class _SendByIdSheetState extends State<_SendByIdSheet> { return Container( decoration: BoxDecoration( color: cs.surface, - borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), + borderRadius: const BorderRadius.vertical( + top: Radius.circular(AppShape.sheet), + ), ), padding: EdgeInsets.fromLTRB( 24, @@ -1248,9 +1249,7 @@ class _SendByIdSheetState extends State<_SendByIdSheet> { onPressed: _sending ? null : _submit, style: FilledButton.styleFrom( minimumSize: const Size.fromHeight(48), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), + shape: AppShape.buttonBorder, ), child: _sending ? SmallSpinner(size: 18, color: cs.onPrimary) diff --git a/lib/frontend/screens/profile/devices_screen.dart b/lib/frontend/screens/profile/devices_screen.dart index 9104980..eb50260 100644 --- a/lib/frontend/screens/profile/devices_screen.dart +++ b/lib/frontend/screens/profile/devices_screen.dart @@ -18,6 +18,7 @@ import '../../widgets/prompt_dialog.dart'; import '../../widgets/small_spinner.dart'; import '../../widgets/web_qr_login.dart'; import 'web_qr_scan_screen.dart'; +import '../../../core/config/app_fonts.dart'; class DevicesScreen extends StatefulWidget { const DevicesScreen({super.key}); @@ -212,7 +213,7 @@ class _DevicesScreenState extends State title: ConnectionTitleText( l10n.devicesTitle, style: TextStyle( - fontFamily: 'Outfit', + fontFamily: displayFontOf(context), fontSize: 20, fontWeight: FontWeight.w600, color: cs.onSurface, @@ -264,7 +265,7 @@ class _DevicesScreenState extends State Text( l10n.devicesPromoTitle, style: TextStyle( - fontFamily: 'Outfit', + fontFamily: displayFontOf(context), fontSize: 18, fontWeight: FontWeight.w700, color: cs.onSurface, @@ -287,7 +288,7 @@ class _DevicesScreenState extends State label: Text( l10n.devicesScanQrButton, style: TextStyle( - fontFamily: 'Outfit', + fontFamily: displayFontOf(context), fontSize: 15, fontWeight: FontWeight.w600, ), @@ -463,7 +464,7 @@ class _DevicesScreenState extends State Text( title, style: TextStyle( - fontFamily: 'Outfit', + fontFamily: displayFontOf(context), fontSize: 16, fontWeight: FontWeight.w700, color: cs.onSurface, diff --git a/lib/frontend/screens/profile/font_settings_screen.dart b/lib/frontend/screens/profile/font_settings_screen.dart index 5e255f8..af84f86 100644 --- a/lib/frontend/screens/profile/font_settings_screen.dart +++ b/lib/frontend/screens/profile/font_settings_screen.dart @@ -9,8 +9,8 @@ import '../../../l10n/app_localizations.dart'; import '../../../main.dart'; import '../../widgets/connection_status.dart'; import '../../widgets/custom_notification.dart'; -import '../../widgets/glossy_pill.dart'; import '../../widgets/prompt_dialog.dart'; +import '../../widgets/settings_card.dart'; class FontSettingsScreen extends StatefulWidget { const FontSettingsScreen({super.key}); @@ -198,11 +198,8 @@ class _PreviewCard extends StatelessWidget { @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; - return GlossyPill( - color: cs.surfaceContainerHigh, - borderRadius: BorderRadius.circular(28), + return SettingsPanel( padding: const EdgeInsets.all(24), - depth: 6, child: SizedBox( width: double.infinity, child: Column( @@ -337,11 +334,8 @@ class _FontSizeControl extends StatelessWidget { Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final isDefault = (scale - AppFonts.defaultScale).abs() < 0.001; - return GlossyPill( - color: cs.surfaceContainerHigh, - borderRadius: BorderRadius.circular(28), + return SettingsPanel( padding: const EdgeInsets.fromLTRB(20, 16, 12, 16), - depth: 6, child: Column( children: [ Row( diff --git a/lib/frontend/screens/profile/info_screen.dart b/lib/frontend/screens/profile/info_screen.dart index b2a6cad..aea4395 100644 --- a/lib/frontend/screens/profile/info_screen.dart +++ b/lib/frontend/screens/profile/info_screen.dart @@ -83,9 +83,12 @@ class _InfoScreenState extends State { Widget _buildContent(ColorScheme cs, AppLocalizations l10n) { final info = _info!; - final server = info['server'] as Map?; - final user = info['user'] as Map?; - final yMap = server?['y-map'] as Map?; + final chats = _asStringMap(info['chats']); + final server = _asStringMap(info['server']); + final user = _asStringMap(info['user']); + final experiments = _asStringMap(info['experiments']); + final chatSettings = _asStringMap(info['chatSettings']); + final yMap = _asStringMap(server?['y-map']); final accountKeys = { 'registrationTime': l10n.infoRegistrationTime, @@ -93,7 +96,36 @@ class _InfoScreenState extends State { 'videoChatHistory': l10n.infoVideoChatHistory, 'updateTime': l10n.infoUpdateTime, 'id': l10n.infoId, + 'phone': l10n.infoPhone, + 'photoId': l10n.infoPhotoId, + 'accountStatus': l10n.infoAccountStatus, + 'contactOptions': l10n.infoContactOptions, + 'profileOptions': l10n.infoProfileOptions, + 'names': l10n.infoNames, + 'baseUrl': l10n.infoBaseUrl, + 'baseRawUrl': l10n.infoBaseRawUrl, + }; + + final packetKeys = { 'chatMarker': l10n.infoChatMarker, + 'time': l10n.infoServerTime, + 'updates': l10n.infoUpdates, + 'messagesCount': l10n.infoMessagesCount, + 'contactsCount': l10n.infoContactsCount, + 'presenceCount': l10n.infoPresenceCount, + 'configHash': l10n.infoConfigHash, + }; + + final chatKeys = { + 'count': l10n.infoChatsCount, + 'active': l10n.infoChatsActive, + 'hidden': l10n.infoChatsHidden, + 'dialogs': l10n.infoChatsDialogs, + 'groups': l10n.infoChatsGroups, + 'channels': l10n.infoChatsChannels, + 'unread': l10n.infoChatsUnread, + 'newMessages': l10n.infoChatsNewMessages, + 'messages': l10n.infoChatsMessages, }; final serverKeys = { @@ -117,66 +149,153 @@ class _InfoScreenState extends State { 'reactions-enabled': l10n.infoReactionsEnabled, }; - return ListView( - padding: const EdgeInsets.all(16), - children: [ - SectionHeader(l10n.infoAccountSection), - ...accountKeys.entries.map( - (e) => - _buildRow(e.key, e.value, _formatValue(info[e.key], e.key), cs), - ), + return SelectionArea( + child: ListView( + padding: const EdgeInsets.all(16), + children: [ + SectionHeader(l10n.infoAccountSection), + ...accountKeys.entries.map( + (entry) => + _buildDataRow(entry.key, entry.value, info[entry.key], cs), + ), - const SizedBox(height: 16), - SectionHeader(l10n.infoServerSection), - ...serverKeys.entries.map( - (e) => _buildRow( - e.key, - e.value, - _formatValue(server?[e.key], e.key), + const SizedBox(height: 16), + SectionHeader(l10n.infoPacketSection), + ...packetKeys.entries.map( + (entry) => + _buildDataRow(entry.key, entry.value, info[entry.key], cs), + ), + + const SizedBox(height: 16), + SectionHeader(l10n.infoChatsSection), + ...chatKeys.entries.map( + (entry) => _buildDataRow( + 'chats.${entry.key}', + entry.value, + chats?[entry.key], + cs, + ), + ), + + const SizedBox(height: 16), + SectionHeader(l10n.infoServerSection), + ...serverKeys.entries.map( + (entry) => + _buildDataRow(entry.key, entry.value, server?[entry.key], cs), + ), + ..._buildDynamicRows( + server, + cs, + excludedKeys: { + ...serverKeys.keys, + 'y-map', + 'file-upload-unsupported-types', + 'white-list-links', + }, + ), + + const SizedBox(height: 8), + SectionHeader(l10n.infoYMapSection), + _buildDataRow('y-map.tile', l10n.infoTile, yMap?['tile'], cs), + _buildDataRow( + 'y-map.geocoder', + l10n.infoGeocoder, + yMap?['geocoder'], cs, ), - ), + _buildDataRow('y-map.static', l10n.infoStatic, yMap?['static'], cs), - const SizedBox(height: 8), - SectionHeader(l10n.infoYMapSection), - _buildRow('tile', l10n.infoTile, yMap?['tile']?.toString() ?? '-', cs), - _buildRow( - 'geocoder', - l10n.infoGeocoder, - yMap?['geocoder']?.toString() ?? '-', - cs, - ), - _buildRow( - 'static', - l10n.infoStatic, - yMap?['static']?.toString() ?? '-', - cs, - ), + const SizedBox(height: 8), + SectionHeader(l10n.infoFileUploadTypes), + _buildListValueRow( + 'file-upload-unsupported-types', + l10n.infoFileUploadTypes, + server?['file-upload-unsupported-types'] as List?, + cs, + showLabel: false, + ), - const SizedBox(height: 8), - SectionHeader(l10n.infoFileUploadTypes), - _buildListRow(server?['file-upload-unsupported-types'] as List?, cs), + const SizedBox(height: 8), + SectionHeader(l10n.infoWhiteListLinks), + _buildListValueRow( + 'white-list-links', + l10n.infoWhiteListLinks, + server?['white-list-links'] as List?, + cs, + showLabel: false, + ), - const SizedBox(height: 8), - SectionHeader(l10n.infoWhiteListLinks), - _buildListRow(server?['white-list-links'] as List?, cs), + if (chatSettings != null && chatSettings.isNotEmpty) ...[ + const SizedBox(height: 8), + SectionHeader(l10n.infoChatSettingsSection), + ..._buildDynamicRows(chatSettings, cs), + ], - const SizedBox(height: 8), - SectionHeader(l10n.infoUserSection), - if (user != null) - ...user.entries - .where((e) => e.value != null) - .map((e) => _buildRow(e.key, e.key, e.value.toString(), cs)), + if (experiments != null && experiments.isNotEmpty) ...[ + const SizedBox(height: 8), + SectionHeader(l10n.infoExperimentsSection), + ..._buildDynamicRows(experiments, cs), + ], - const SizedBox(height: 120), - ], + const SizedBox(height: 8), + SectionHeader(l10n.infoUserSection), + ..._buildDynamicRows(user, cs), + + const SizedBox(height: 120), + ], + ), ); } + List _buildDynamicRows( + Map? values, + ColorScheme cs, { + Set excludedKeys = const {}, + }) { + if (values == null) return []; + final entries = >[]; + for (final entry in values.entries) { + if (excludedKeys.contains(entry.key) || entry.value == null) continue; + _flattenEntry(entry.key, entry.value, entries); + } + entries.sort((a, b) => a.key.compareTo(b.key)); + return entries + .map((entry) => _buildDataRow(entry.key, entry.key, entry.value, cs)) + .toList(); + } + + void _flattenEntry( + String key, + dynamic value, + List> target, + ) { + final map = _asStringMap(value); + if (map == null || map.isEmpty) { + target.add(MapEntry(key, value)); + return; + } + for (final entry in map.entries) { + _flattenEntry('$key.${entry.key}', entry.value, target); + } + } + + Widget _buildDataRow( + String key, + String label, + dynamic value, + ColorScheme cs, + ) { + if (value is List) { + return _buildListValueRow(key, label, value, cs); + } + return _buildRow(key, label, _formatValue(value, key), cs); + } + Widget _buildRow(String key, String label, String value, ColorScheme cs) { return Padding( padding: const EdgeInsets.only(bottom: 1), child: GlossyPill( + key: ValueKey('info-$key'), color: cs.surfaceContainerHigh, borderRadius: BorderRadius.circular(12), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 13), @@ -213,42 +332,75 @@ class _InfoScreenState extends State { ); } - Widget _buildListRow(List? items, ColorScheme cs) { - if (items == null || items.isEmpty) { - return GlossyPill( + Widget _buildListValueRow( + String key, + String label, + List? items, + ColorScheme cs, { + bool showLabel = true, + }) { + final values = items ?? const []; + final simple = values.every( + (item) => item == null || item is String || item is num || item is bool, + ); + return Padding( + padding: const EdgeInsets.only(bottom: 1), + child: GlossyPill( + key: ValueKey('info-$key'), color: cs.surfaceContainerHigh, borderRadius: BorderRadius.circular(12), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 13), + padding: const EdgeInsets.all(16), depth: 6, - child: Text('-', style: TextStyle(color: cs.onSurfaceVariant)), - ); - } - return GlossyPill( - color: cs.surfaceContainerHigh, - borderRadius: BorderRadius.circular(12), - padding: const EdgeInsets.all(16), - depth: 6, - child: Wrap( - spacing: 8, - runSpacing: 4, - children: items - .map( - (item) => Container( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 5, - ), - decoration: BoxDecoration( - color: cs.surfaceContainerHighest, - borderRadius: BorderRadius.circular(8), - ), - child: Text( - item.toString(), - style: TextStyle(fontSize: 13, color: cs.onSurface), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (showLabel) ...[ + Text( + label, + style: TextStyle( + color: cs.onSurfaceVariant, + fontSize: 14, + fontWeight: FontWeight.w400, ), ), - ) - .toList(), + const SizedBox(height: 8), + ], + if (values.isEmpty) + Text('-', style: TextStyle(color: cs.onSurfaceVariant)) + else if (simple) + Wrap( + spacing: 8, + runSpacing: 4, + children: values + .map( + (item) => Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 5, + ), + decoration: BoxDecoration( + color: cs.surfaceContainerHighest, + borderRadius: BorderRadius.circular(8), + ), + child: Text( + _formatValue(item, key), + style: TextStyle(fontSize: 13, color: cs.onSurface), + ), + ), + ) + .toList(), + ) + else + Text( + const JsonEncoder.withIndent(' ').convert(values), + style: TextStyle( + color: cs.onSurface, + fontSize: 13, + fontWeight: FontWeight.w500, + ), + ), + ], + ), ), ); } @@ -259,6 +411,7 @@ class _InfoScreenState extends State { final ts = value['chatMarker'] as int?; return ts != null ? _formatTs(ts) : '-'; } + if (key == 'phone' && value is num && value > 0) return '+$value'; if (value is int && value > 1000000000000) return _formatTs(value); if (key == 'edit-timeout' && value is int && value > 0) { final weeks = value ~/ 604800; @@ -280,4 +433,9 @@ class _InfoScreenState extends State { return '${dt.year}-${pad2(dt.month)}-${pad2(dt.day)} ' '${pad2(dt.hour)}:${pad2(dt.minute)}:${pad2(dt.second)}'; } + + Map? _asStringMap(dynamic value) { + if (value is! Map) return null; + return value.map((key, item) => MapEntry(key.toString(), item)); + } } diff --git a/lib/frontend/screens/profile/lottie_polygon_screen.dart b/lib/frontend/screens/profile/lottie_polygon_screen.dart index b2c07cc..a9fa429 100644 --- a/lib/frontend/screens/profile/lottie_polygon_screen.dart +++ b/lib/frontend/screens/profile/lottie_polygon_screen.dart @@ -3,6 +3,7 @@ import 'package:lottie/lottie.dart'; import 'package:material_symbols_icons/symbols.dart'; import '../../../core/config/app_animations.dart'; +import '../../../core/config/app_fonts.dart'; class LottiePolygonScreen extends StatelessWidget { const LottiePolygonScreen({super.key}); @@ -32,7 +33,7 @@ class LottiePolygonScreen extends StatelessWidget { title: Text( 'Lottie полигон', style: TextStyle( - fontFamily: 'Outfit', + fontFamily: displayFontOf(context), fontSize: 20, fontWeight: FontWeight.w600, color: cs.onSurface, @@ -47,9 +48,7 @@ class LottiePolygonScreen extends StatelessWidget { crossAxisCount: 3, mainAxisSpacing: 16, crossAxisSpacing: 16, - children: [ - for (final entry in _entries) _PolygonTile(entry: entry), - ], + children: [for (final entry in _entries) _PolygonTile(entry: entry)], ), ), ); @@ -113,14 +112,10 @@ class _PolygonTileState extends State<_PolygonTile> fit: BoxFit.contain, delegates: LottieDelegates( values: [ - ValueDelegate.color( - const ['**'], - value: cs.onSurface, - ), - ValueDelegate.strokeColor( - const ['**'], - value: cs.onSurface, - ), + ValueDelegate.color(const ['**'], value: cs.onSurface), + ValueDelegate.strokeColor(const [ + '**', + ], value: cs.onSurface), ], ), onLoaded: (composition) { diff --git a/lib/frontend/screens/profile/message_actions_screen.dart b/lib/frontend/screens/profile/message_actions_screen.dart index b6591e7..39714de 100644 --- a/lib/frontend/screens/profile/message_actions_screen.dart +++ b/lib/frontend/screens/profile/message_actions_screen.dart @@ -5,8 +5,8 @@ import '../../widgets/connection_status.dart'; import '../../../core/config/app_message_actions_style.dart'; import '../../../core/utils/haptics.dart'; -import '../../widgets/glossy_pill.dart'; import '../../widgets/settings_radio_tile.dart'; +import '../../widgets/settings_card.dart'; class MessageActionsScreen extends StatelessWidget { const MessageActionsScreen({super.key}); @@ -53,11 +53,8 @@ class _StyleCard extends StatelessWidget { @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; - return GlossyPill( - color: cs.surfaceContainerHigh, - borderRadius: BorderRadius.circular(28), + return SettingsPanel( padding: const EdgeInsets.fromLTRB(20, 18, 20, 12), - depth: 6, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/lib/frontend/screens/profile/password_entry_screen.dart b/lib/frontend/screens/profile/password_entry_screen.dart index 1419864..c5c93a9 100644 --- a/lib/frontend/screens/profile/password_entry_screen.dart +++ b/lib/frontend/screens/profile/password_entry_screen.dart @@ -9,6 +9,8 @@ import '../../widgets/custom_notification.dart'; import '../../widgets/glossy_pill.dart'; import '../../widgets/primary_loading_button.dart'; import '../../widgets/small_spinner.dart'; +import '../../../core/config/app_fonts.dart'; +import '../../../core/config/app_shape.dart'; class PasswordEntryScreen extends StatefulWidget { const PasswordEntryScreen({super.key}); @@ -74,6 +76,7 @@ class _PasswordEntryScreenState extends State { return await showDialog( context: context, builder: (ctx) => AlertDialog( + shape: AppShape.dialogBorder, title: Text(l10n.passwordEntryConfirmTitle), content: TextField( controller: controller, @@ -189,7 +192,7 @@ class _PasswordEntryScreenState extends State { color: cs.onSurface, fontSize: 20, fontWeight: FontWeight.w700, - fontFamily: 'Outfit', + fontFamily: displayFontOf(context), ), ), ], @@ -390,7 +393,7 @@ class _PasswordEntryScreenState extends State { ), _buildActionRow( cs, - icon: Icons.email_outlined, + icon: Symbols.mail, label: l10n.passwordEntryChangeEmailAction, isLast: false, onTap: () => _openWithPassword( @@ -403,7 +406,7 @@ class _PasswordEntryScreenState extends State { ), _buildActionRow( cs, - icon: Icons.delete_outline, + icon: Symbols.delete, label: l10n.passwordEntryDeleteAction, isLast: true, textColor: cs.error, @@ -478,7 +481,9 @@ class _PasswordEntryScreenState extends State { child: InkWell( onTap: onTap, borderRadius: isLast - ? const BorderRadius.vertical(bottom: Radius.circular(20)) + ? const BorderRadius.vertical( + bottom: Radius.circular(AppShape.card), + ) : BorderRadius.zero, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 17), diff --git a/lib/frontend/screens/profile/performance_screen.dart b/lib/frontend/screens/profile/performance_screen.dart index 47fc4ac..8edd49f 100644 --- a/lib/frontend/screens/profile/performance_screen.dart +++ b/lib/frontend/screens/profile/performance_screen.dart @@ -4,7 +4,7 @@ import '../../widgets/connection_status.dart'; import '../../../core/config/app_cache_extent.dart'; import '../../../core/utils/haptics.dart'; import '../../widgets/confirm_dialog.dart'; -import '../../widgets/glossy_pill.dart'; +import '../../widgets/settings_card.dart'; class PerformanceScreen extends StatefulWidget { const PerformanceScreen({super.key}); @@ -103,11 +103,7 @@ class _PerformanceScreenState extends State { physics: const BouncingScrollPhysics(), padding: const EdgeInsets.fromLTRB(16, 12, 16, 120), children: [ - GlossyPill( - color: cs.surfaceContainerHigh, - borderRadius: BorderRadius.circular(28), - padding: const EdgeInsets.fromLTRB(20, 18, 20, 20), - depth: 6, + SettingsPanel( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/lib/frontend/screens/profile/security_screen.dart b/lib/frontend/screens/profile/security_screen.dart index fab53e7..05cbd67 100644 --- a/lib/frontend/screens/profile/security_screen.dart +++ b/lib/frontend/screens/profile/security_screen.dart @@ -15,6 +15,8 @@ import '../../widgets/glossy_pill.dart'; import '../../widgets/sheet_helpers.dart'; import '../../widgets/small_spinner.dart'; import 'password_entry_screen.dart'; +import '../../../core/config/app_fonts.dart'; +import '../../../core/config/app_shape.dart'; class SecurityScreen extends StatefulWidget { const SecurityScreen({super.key}); @@ -217,7 +219,7 @@ class _SecurityScreenState extends State color: cs.onSurface, fontSize: 20, fontWeight: FontWeight.w700, - fontFamily: 'Outfit', + fontFamily: displayFontOf(context), ), ), const Spacer(), @@ -250,7 +252,7 @@ class _SecurityScreenState extends State final l10n = AppLocalizations.of(context)!; return GlossyPill( color: cs.surfaceContainerHigh, - borderRadius: BorderRadius.circular(20), + borderRadius: AppShape.cardRadius, depth: 6, child: Column( children: [ @@ -351,7 +353,7 @@ class _SecurityScreenState extends State final isSafeMode = _privacyConfig?.safeMode ?? false; return GlossyPill( color: cs.surfaceContainerHigh, - borderRadius: BorderRadius.circular(20), + borderRadius: AppShape.cardRadius, depth: 6, child: Column( children: [ @@ -532,7 +534,7 @@ class _SecurityScreenState extends State ), _settingsRow( cs, - icon: Icons.visibility_off_outlined, + icon: Symbols.visibility_off, label: l10n.securityShowOnlineStatus, trailingText: _privacyConfig?.hidden == true ? l10n.securityPrivacyNobody @@ -713,7 +715,7 @@ class _SecurityScreenState extends State _privacyConfig?.audioTranscriptionEnabled ?? true; return GlossyPill( color: cs.surfaceContainerHigh, - borderRadius: BorderRadius.circular(20), + borderRadius: AppShape.cardRadius, depth: 6, child: Column( children: [ @@ -758,7 +760,7 @@ class _SecurityScreenState extends State ), _settingsRow( cs, - icon: Icons.mic_none_outlined, + icon: Symbols.mic, label: l10n.securityAudioTranscription, trailingWidget: Switch( value: audioTranscription, @@ -783,7 +785,7 @@ class _SecurityScreenState extends State final count = _blockedContacts.length; return GlossyPill( color: cs.surfaceContainerHigh, - borderRadius: BorderRadius.circular(20), + borderRadius: AppShape.cardRadius, depth: 6, child: Material( color: Colors.transparent, @@ -873,7 +875,9 @@ class _SecurityScreenState extends State child: InkWell( onTap: onTap ?? () => showCustomNotification(context, label), borderRadius: isLast - ? const BorderRadius.vertical(bottom: Radius.circular(20)) + ? const BorderRadius.vertical( + bottom: Radius.circular(AppShape.card), + ) : BorderRadius.zero, child: Padding( padding: EdgeInsets.symmetric( diff --git a/lib/frontend/screens/profile/settings_tab.dart b/lib/frontend/screens/profile/settings_tab.dart index 18566c2..192a1b7 100644 --- a/lib/frontend/screens/profile/settings_tab.dart +++ b/lib/frontend/screens/profile/settings_tab.dart @@ -46,6 +46,8 @@ import 'notifications_screen.dart'; import 'security_screen.dart'; import 'spoof_screen.dart'; import '../../widgets/media_playback_pill.dart'; +import '../../../core/config/app_fonts.dart'; +import '../../../core/config/app_shape.dart'; class SettingsTab extends StatefulWidget { const SettingsTab({super.key}); @@ -290,9 +292,7 @@ class _SettingsTabState extends State with SpectrumSurface { backgroundColor: cs.error, foregroundColor: cs.onError, padding: const EdgeInsets.symmetric(vertical: 14), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(14), - ), + shape: AppShape.buttonBorder, ), child: const Text('Выйти'), ), @@ -825,7 +825,7 @@ class _SettingsTabState extends State with SpectrumSurface { color: nameColor, fontSize: lerpDouble(20, 26, pt), fontWeight: FontWeight.w700, - fontFamily: 'Outfit', + fontFamily: displayFontOf(context), ), ), ), @@ -1025,7 +1025,7 @@ class _SettingsTabState extends State with SpectrumSurface { Symbols.check_circle, fill: 1, size: 15, - color: online ? kOnlineGreen : cs.mutedText, + color: online ? kSuccessGreen : cs.mutedText, ), const SizedBox(width: 5), Text( diff --git a/lib/frontend/screens/profile/spoof_screen.dart b/lib/frontend/screens/profile/spoof_screen.dart index 0b5be09..91792a9 100644 --- a/lib/frontend/screens/profile/spoof_screen.dart +++ b/lib/frontend/screens/profile/spoof_screen.dart @@ -6,6 +6,7 @@ import 'dart:math'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/material.dart'; import 'package:flutter_timezone/flutter_timezone.dart'; +import 'package:material_symbols_icons/symbols.dart'; import '../../../core/config/device_presets.dart'; import '../../../core/storage/device_identity.dart'; @@ -17,7 +18,10 @@ import '../../../main.dart'; import '../../widgets/connection_status.dart'; import '../../widgets/custom_notification.dart'; import '../../widgets/info_action_sheet.dart'; +import '../../../core/config/app_colors.dart'; +import '../../../core/config/app_shape.dart'; import '../../widgets/section_header.dart'; +import '../../widgets/settings_card.dart'; import '../../widgets/small_spinner.dart'; import '../auth/login_screen.dart'; @@ -74,7 +78,7 @@ class _SpoofScreenState extends State { Future _confirmFullSpoofing() { return showInfoActionSheet( context, - headerIcon: Icons.warning_amber_rounded, + headerIcon: Symbols.warning, title: 'Могут быть последствия.', subtitle: 'Меняй, только если знаешь что делаешь.', confirmLabel: 'ОК', @@ -328,6 +332,7 @@ class _SpoofScreenState extends State { final confirmed = await showDialog( context: context, builder: (context) => AlertDialog( + shape: AppShape.dialogBorder, title: Text(l10n.spoofDialogApplyTitle), content: Column( mainAxisSize: MainAxisSize.min, @@ -436,31 +441,39 @@ class _SpoofScreenState extends State { @override Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; final l10n = AppLocalizations.of(context)!; return Scaffold( - appBar: AppBar( - title: ConnectionTitleText(l10n.spoofScreenTitle), - centerTitle: true, + backgroundColor: cs.surface, + appBar: ConnectionTitleBar( + titleText: l10n.spoofScreenTitle, + backgroundColor: cs.surface, ), body: _isLoading ? const Center(child: SmallSpinner(size: 36)) - : SingleChildScrollView( - padding: const EdgeInsets.fromLTRB(16, 8, 16, 120), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, + : SafeArea( + top: false, + child: ListView( + physics: const BouncingScrollPhysics(), + padding: const EdgeInsets.fromLTRB(16, 12, 16, 120), children: [ _buildEnableCard(), - const SizedBox(height: 16), + const SizedBox(height: 12), _buildInfoCard(), - const SizedBox(height: 16), + const SizedBox(height: 20), + _sectionHeader(l10n.spoofMethodTitle), _buildSpoofingMethodCard(), - const SizedBox(height: 16), + const SizedBox(height: 20), + _sectionHeader(l10n.spoofDeviceTypeTitle), _buildDeviceTypeCard(), - const SizedBox(height: 24), + const SizedBox(height: 20), + _sectionHeader(l10n.spoofMainSectionTitle), _buildMainDataCard(), - const SizedBox(height: 16), + const SizedBox(height: 20), + _sectionHeader(l10n.spoofRegionalSectionTitle), _buildRegionalDataCard(), - const SizedBox(height: 16), + const SizedBox(height: 20), + _sectionHeader(l10n.spoofIdentifiersSectionTitle), _buildIdentifiersCard(), ], ), @@ -470,62 +483,61 @@ class _SpoofScreenState extends State { ); } + Widget _sectionHeader(String title) => SectionHeader( + title, + padding: const EdgeInsets.fromLTRB(8, 0, 8, 8), + fontSize: 14, + ); + Widget _buildEnableCard() { final l10n = AppLocalizations.of(context)!; - return Card( - child: SwitchListTile( - contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), - title: Text( - l10n.spoofEnableTitle, - style: Theme.of(context).textTheme.titleMedium, - ), - subtitle: Text( - _spoofingEnabled + return SettingsCard( + children: [ + SettingsToggleTile( + icon: Symbols.security, + label: l10n.spoofEnableTitle, + subtitle: _spoofingEnabled ? l10n.spoofEnableSubtitleOn : l10n.spoofEnableSubtitleOff, + value: _spoofingEnabled, + onChanged: (value) async { + if (value) { + await _applyGeneratedData(); + } else { + await _loadDeviceData(); + } + }, ), - value: _spoofingEnabled, - onChanged: (value) async { - if (value) { - await _applyGeneratedData(); - } else { - await _loadDeviceData(); - } - }, - ), + ], ); } Widget _buildInfoCard() { + final cs = Theme.of(context).colorScheme; final l10n = AppLocalizations.of(context)!; - return Card( - color: Theme.of( - context, - ).colorScheme.secondaryContainer.withValues(alpha: 0.5), - elevation: 0, - child: Padding( - padding: const EdgeInsets.all(12.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.touch_app, - size: 18, - color: Theme.of(context).colorScheme.onSecondaryContainer, - ), - const SizedBox(width: 8), - Flexible( - child: Text( - l10n.spoofInfoHint, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 14, - color: Theme.of(context).colorScheme.onSecondaryContainer, - ), + return SettingsPanel( + color: cs.secondaryContainer.withValues(alpha: 0.5), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14), + child: Row( + children: [ + Icon( + Symbols.touch_app, + size: 20, + weight: 400, + color: cs.onSecondaryContainer, + ), + const SizedBox(width: 16), + Expanded( + child: Text( + l10n.spoofInfoHint, + style: TextStyle( + fontSize: 13, + height: 1.3, + color: cs.onSecondaryContainer, ), ), - ], - ), + ), + ], ), ); } @@ -537,37 +549,35 @@ class _SpoofScreenState extends State { if (_selectedMethod == SpoofingMethod.partial) { descriptionWidget = _buildDescriptionTile( - icon: Icons.check_circle_outline, - color: Colors.green.shade700, + icon: Symbols.check_circle, + color: kSuccessGreen, text: l10n.spoofMethodPartialDescription, ); } else { descriptionWidget = _buildDescriptionTile( - icon: Icons.warning_amber_rounded, + icon: Symbols.warning, color: theme.colorScheme.error, text: l10n.spoofMethodFullDescription, ); } - return Card( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - children: [ - Text(l10n.spoofMethodTitle, style: theme.textTheme.titleMedium), - const SizedBox(height: 12), - SegmentedButton( - style: SegmentedButton.styleFrom(shape: const StadiumBorder()), + return SettingsPanel( + child: Column( + children: [ + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: SegmentedButton( + showSelectedIcon: false, segments: [ ButtonSegment( value: SpoofingMethod.partial, label: Text(l10n.spoofMethodPartial), - icon: const Icon(Icons.security_outlined), + icon: const Icon(Symbols.security), ), ButtonSegment( value: SpoofingMethod.full, label: Text(l10n.spoofMethodFull), - icon: const Icon(Icons.public_outlined), + icon: const Icon(Symbols.public), ), ], selected: {_selectedMethod}, @@ -586,10 +596,10 @@ class _SpoofScreenState extends State { _syncDeviceLocale(); }, ), - const SizedBox(height: 12), - descriptionWidget, - ], - ), + ), + const SizedBox(height: 12), + descriptionWidget, + ], ), ); } @@ -597,54 +607,41 @@ class _SpoofScreenState extends State { Widget _buildDeviceTypeCard() { final theme = Theme.of(context); final l10n = AppLocalizations.of(context)!; - return Card( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(l10n.spoofDeviceTypeTitle, style: theme.textTheme.titleMedium), - const SizedBox(height: 12), - _buildDescriptionTile( - icon: Icons.info_outline, - color: theme.colorScheme.primary, - text: l10n.spoofDeviceTypeDescription, + return SettingsPanel( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildDescriptionTile( + icon: Symbols.info, + color: theme.colorScheme.primary, + text: l10n.spoofDeviceTypeDescription, + ), + const SizedBox(height: 12), + if (_selectedMethod == SpoofingMethod.full) + _buildChipSelector( + options: const [ + _ChipOption('ANDROID', 'Android', Symbols.android), + _ChipOption('DESKTOP', 'Desktop', Symbols.desktop_windows), + ], + selected: _selectedDeviceType, + onSelected: _onDeviceTypeChanged, + trailing: [ + _buildDisabledChip('iOS', Symbols.phone_iphone, theme), + ], + ) + else + _buildChipSelector( + options: const [ + _ChipOption('ANDROID', 'Android', Symbols.android), + ], + selected: 'ANDROID', + onSelected: _onDeviceTypeChanged, + trailing: [ + _buildDisabledChip('iOS', Symbols.phone_iphone, theme), + _buildDisabledChip('Desktop', Symbols.desktop_windows, theme), + ], ), - const SizedBox(height: 12), - if (_selectedMethod == SpoofingMethod.full) - _buildChipSelector( - options: const [ - _ChipOption('ANDROID', 'Android', Icons.android_outlined), - _ChipOption( - 'DESKTOP', - 'Desktop', - Icons.desktop_windows_outlined, - ), - ], - selected: _selectedDeviceType, - onSelected: _onDeviceTypeChanged, - trailing: [ - _buildDisabledChip('iOS', Icons.phone_iphone_outlined, theme), - ], - ) - else - _buildChipSelector( - options: const [ - _ChipOption('ANDROID', 'Android', Icons.android_outlined), - ], - selected: 'ANDROID', - onSelected: _onDeviceTypeChanged, - trailing: [ - _buildDisabledChip('iOS', Icons.phone_iphone_outlined, theme), - _buildDisabledChip( - 'Desktop', - Icons.desktop_windows_outlined, - theme, - ), - ], - ), - ], - ), + ], ), ); } @@ -663,214 +660,189 @@ class _SpoofScreenState extends State { required Color color, required String text, }) { - return ListTile( - leading: Icon(icon, color: color), - contentPadding: EdgeInsets.zero, - title: Text( - text, - style: TextStyle( - fontSize: 13, - color: Theme.of(context).colorScheme.onSurfaceVariant, + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(icon, color: color, size: 20, weight: 400), + const SizedBox(width: 16), + Expanded( + child: Text( + text, + style: TextStyle( + fontSize: 13, + height: 1.3, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), ), - ), + ], ); } Widget _buildMainDataCard() { final l10n = AppLocalizations.of(context)!; - return Card( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SectionHeader( - l10n.spoofMainSectionTitle, - padding: const EdgeInsets.only(bottom: 16.0, top: 8.0), - fontSize: 22, + return SettingsPanel( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextField( + controller: _deviceNameController, + decoration: _inputDecoration( + l10n.spoofFieldDeviceName, + Symbols.smartphone, ), - TextField( - controller: _deviceNameController, - decoration: _inputDecoration( - l10n.spoofFieldDeviceName, - Icons.smartphone_outlined, - ), + ), + const SizedBox(height: 16), + TextField( + controller: _osVersionController, + decoration: _inputDecoration( + l10n.spoofFieldOsVersion, + Symbols.layers, ), - const SizedBox(height: 16), - TextField( - controller: _osVersionController, - decoration: _inputDecoration( - l10n.spoofFieldOsVersion, - Icons.layers_outlined, - ), - ), - ], - ), + ), + ], ), ); } Widget _buildRegionalDataCard() { final l10n = AppLocalizations.of(context)!; - return Card( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SectionHeader( - l10n.spoofRegionalSectionTitle, - padding: const EdgeInsets.only(bottom: 16.0, top: 8.0), - fontSize: 22, + return SettingsPanel( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextField( + controller: _screenController, + decoration: _inputDecoration( + l10n.spoofFieldScreen, + Symbols.fullscreen, ), - TextField( - controller: _screenController, - decoration: _inputDecoration( - l10n.spoofFieldScreen, - Icons.fullscreen_outlined, - ), + ), + const SizedBox(height: 16), + TextField( + controller: _timezoneController, + enabled: _selectedMethod == SpoofingMethod.full, + decoration: _inputDecoration( + l10n.spoofFieldTimezone, + Symbols.public, ), - const SizedBox(height: 16), - TextField( - controller: _timezoneController, - enabled: _selectedMethod == SpoofingMethod.full, - decoration: _inputDecoration( - l10n.spoofFieldTimezone, - Icons.public_outlined, - ), + ), + const SizedBox(height: 16), + TextField( + controller: _localeController, + enabled: _selectedMethod == SpoofingMethod.full, + decoration: _inputDecoration( + l10n.spoofFieldLocale, + Symbols.language, ), - const SizedBox(height: 16), - TextField( - controller: _localeController, - enabled: _selectedMethod == SpoofingMethod.full, - decoration: _inputDecoration( - l10n.spoofFieldLocale, - Icons.language_outlined, - ), + ), + const SizedBox(height: 16), + TextField( + controller: _deviceLocaleController, + enabled: _selectedMethod == SpoofingMethod.full, + decoration: _inputDecoration( + l10n.spoofFieldDeviceLocale, + Symbols.translate, ), - const SizedBox(height: 16), - TextField( - controller: _deviceLocaleController, - enabled: _selectedMethod == SpoofingMethod.full, - decoration: _inputDecoration( - l10n.spoofFieldDeviceLocale, - Icons.translate_outlined, - ), - ), - ], - ), + ), + ], ), ); } Widget _buildIdentifiersCard() { final l10n = AppLocalizations.of(context)!; - return Card( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SectionHeader( - l10n.spoofIdentifiersSectionTitle, - padding: const EdgeInsets.only(bottom: 16.0, top: 8.0), - fontSize: 22, + return SettingsPanel( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildDescriptionTile( + icon: Symbols.info, + color: Theme.of(context).colorScheme.tertiary, + text: l10n.spoofIdentifiersDescription, + ), + const SizedBox(height: 12), + TextField( + controller: _instanceIdController, + enabled: _selectedMethod == SpoofingMethod.full, + decoration: _inputDecoration( + l10n.spoofFieldInstanceId, + Symbols.fingerprint, ), - _buildDescriptionTile( - icon: Icons.info_outline, - color: Theme.of(context).colorScheme.tertiary, - text: l10n.spoofIdentifiersDescription, + ), + const SizedBox(height: 16), + TextField( + controller: _clientSessionIdController, + enabled: _selectedMethod == SpoofingMethod.full, + decoration: _inputDecoration( + l10n.spoofFieldClientSessionId, + Symbols.vpn_key, ), - const SizedBox(height: 12), - TextField( - controller: _instanceIdController, - enabled: _selectedMethod == SpoofingMethod.full, - decoration: _inputDecoration( - l10n.spoofFieldInstanceId, - Icons.fingerprint_outlined, - ), - ), - const SizedBox(height: 16), - TextField( - controller: _clientSessionIdController, - enabled: _selectedMethod == SpoofingMethod.full, - decoration: _inputDecoration( - l10n.spoofFieldClientSessionId, - Icons.vpn_key_outlined, - ), - ), - const SizedBox(height: 16), - TextField( - controller: _deviceIdController, - decoration: - _inputDecoration( - l10n.spoofFieldDeviceId, - Icons.tag_outlined, - ).copyWith( - suffixIcon: IconButton( - icon: const Icon(Icons.autorenew_outlined), - tooltip: l10n.spoofRegenerateIdTooltip, - onPressed: _generateNewDeviceId, - ), + ), + const SizedBox(height: 16), + TextField( + controller: _deviceIdController, + decoration: _inputDecoration(l10n.spoofFieldDeviceId, Symbols.tag) + .copyWith( + suffixIcon: IconButton( + icon: const Icon(Symbols.autorenew), + tooltip: l10n.spoofRegenerateIdTooltip, + onPressed: _generateNewDeviceId, ), - ), - const SizedBox(height: 16), - TextField( - controller: _appVersionController, - enabled: _selectedMethod == SpoofingMethod.full, - decoration: _inputDecoration( - l10n.spoofFieldAppVersion, - Icons.info_outline_rounded, - ), - ), - const SizedBox(height: 16), - TextField( - controller: _buildNumberController, - enabled: _selectedMethod == SpoofingMethod.full, - keyboardType: TextInputType.number, - decoration: _inputDecoration( - l10n.spoofFieldBuildNumber, - Icons.numbers_outlined, - ), - ), - const SizedBox(height: 16), - TextField( - controller: _pushDeviceTypeController, - enabled: _selectedMethod == SpoofingMethod.full, - decoration: _inputDecoration( - l10n.spoofFieldPushDeviceType, - Icons.notifications_outlined, - ), - ), - const SizedBox(height: 16), - Padding( - padding: const EdgeInsets.only(left: 4, bottom: 8), - child: Text( - l10n.spoofFieldArchitecture, - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w500, - color: Theme.of(context).colorScheme.onSurfaceVariant, ), + ), + const SizedBox(height: 16), + TextField( + controller: _appVersionController, + enabled: _selectedMethod == SpoofingMethod.full, + decoration: _inputDecoration( + l10n.spoofFieldAppVersion, + Symbols.info, + ), + ), + const SizedBox(height: 16), + TextField( + controller: _buildNumberController, + enabled: _selectedMethod == SpoofingMethod.full, + keyboardType: TextInputType.number, + decoration: _inputDecoration( + l10n.spoofFieldBuildNumber, + Symbols.numbers, + ), + ), + const SizedBox(height: 16), + TextField( + controller: _pushDeviceTypeController, + enabled: _selectedMethod == SpoofingMethod.full, + decoration: _inputDecoration( + l10n.spoofFieldPushDeviceType, + Symbols.notifications, + ), + ), + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.only(left: 4, bottom: 8), + child: Text( + l10n.spoofFieldArchitecture, + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), - _buildChipSelector( - options: const [ - _ChipOption('arm64-v8a', 'arm64-v8a', Icons.memory_outlined), - _ChipOption( - 'armeabi-v7a', - 'armeabi-v7a', - Icons.memory_outlined, - ), - _ChipOption('x86', 'x86', Icons.memory_outlined), - _ChipOption('x86_64', 'x86_64', Icons.memory_outlined), - _ChipOption('arm64', 'arm64', Icons.memory_outlined), - ], - selected: _selectedArch, - onSelected: (value) => setState(() => _selectedArch = value), - ), - ], - ), + ), + _buildChipSelector( + options: const [ + _ChipOption('arm64-v8a', 'arm64-v8a', Symbols.memory), + _ChipOption('armeabi-v7a', 'armeabi-v7a', Symbols.memory), + _ChipOption('x86', 'x86', Symbols.memory), + _ChipOption('x86_64', 'x86_64', Symbols.memory), + _ChipOption('arm64', 'arm64', Symbols.memory), + ], + selected: _selectedArch, + onSelected: (value) => setState(() => _selectedArch = value), + ), + ], ), ); } @@ -879,7 +851,7 @@ class _SpoofScreenState extends State { return InputDecoration( labelText: label, prefixIcon: Icon(icon), - border: OutlineInputBorder(borderRadius: BorderRadius.circular(16)), + border: const OutlineInputBorder(borderRadius: AppShape.buttonRadius), filled: true, fillColor: Theme.of(context).colorScheme.surfaceContainerHighest, ); @@ -901,7 +873,7 @@ class _SpoofScreenState extends State { return ChoiceChip( label: Text(opt.label), avatar: isSelected - ? Icon(Icons.check, size: 18, color: cs.onSecondaryContainer) + ? Icon(Symbols.check, size: 18, color: cs.onSecondaryContainer) : (opt.icon != null ? Icon(opt.icon, size: 18, color: cs.onSurfaceVariant) : null), @@ -918,8 +890,8 @@ class _SpoofScreenState extends State { side: BorderSide( color: isSelected ? Colors.transparent : cs.outlineVariant, ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), + shape: const RoundedRectangleBorder( + borderRadius: AppShape.buttonRadius, ), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, @@ -946,7 +918,7 @@ class _SpoofScreenState extends State { vertical: 16, horizontal: 16, ), - shape: const StadiumBorder(), + shape: AppShape.buttonBorder, ), child: Text(l10n.spoofButtonGenerate), ), @@ -961,12 +933,12 @@ class _SpoofScreenState extends State { vertical: 16, horizontal: 16, ), - shape: const StadiumBorder(), + shape: AppShape.buttonBorder, ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Icon(Icons.save_alt_outlined), + const Icon(Symbols.save_alt), const SizedBox(width: 8), Text(l10n.spoofButtonApply), ], diff --git a/lib/frontend/screens/profile/theme_settings_screen.dart b/lib/frontend/screens/profile/theme_settings_screen.dart index 8513039..9f436ff 100644 --- a/lib/frontend/screens/profile/theme_settings_screen.dart +++ b/lib/frontend/screens/profile/theme_settings_screen.dart @@ -11,6 +11,7 @@ import '../../../l10n/app_localizations.dart'; import '../../../main.dart'; import '../../widgets/glossy_pill.dart'; import '../../widgets/settings_radio_tile.dart'; +import '../../widgets/settings_card.dart'; class ThemeSettingsScreen extends StatelessWidget { const ThemeSettingsScreen({super.key}); @@ -71,11 +72,8 @@ class _ThemeModeCard extends StatelessWidget { Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; final l10n = AppLocalizations.of(context)!; - return GlossyPill( - color: cs.surfaceContainerHigh, - borderRadius: BorderRadius.circular(28), + return SettingsPanel( padding: const EdgeInsets.fromLTRB(20, 18, 20, 12), - depth: 6, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -171,11 +169,8 @@ class _AmoledCardState extends State<_AmoledCard> { return Listener( behavior: HitTestBehavior.translucent, onPointerDown: (e) => _lastPointerPosition = e.position, - child: GlossyPill( - color: cs.surfaceContainerHigh, - borderRadius: BorderRadius.circular(28), + child: SettingsPanel( padding: const EdgeInsets.fromLTRB(20, 14, 12, 14), - depth: 6, child: Row( children: [ Icon(Symbols.contrast, color: cs.onSurface, size: 24, weight: 500), @@ -235,11 +230,7 @@ class _ScheduleCard extends StatelessWidget { return AnimatedOpacity( opacity: enabled ? 1 : 0.5, duration: const Duration(milliseconds: 200), - child: GlossyPill( - color: cs.surfaceContainerHigh, - borderRadius: BorderRadius.circular(28), - padding: const EdgeInsets.fromLTRB(20, 18, 20, 20), - depth: 6, + child: SettingsPanel( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/lib/frontend/screens/profile/traffic_monitor_screen.dart b/lib/frontend/screens/profile/traffic_monitor_screen.dart index 3a4ea6b..36ff901 100644 --- a/lib/frontend/screens/profile/traffic_monitor_screen.dart +++ b/lib/frontend/screens/profile/traffic_monitor_screen.dart @@ -12,6 +12,8 @@ import '../../../core/protocol/packet.dart'; import '../../../core/transport/traffic_monitor.dart'; import '../../../core/utils/format.dart'; import '../../widgets/custom_notification.dart'; +import '../../../core/config/app_fonts.dart'; +import '../../../core/config/app_shape.dart'; class TrafficMonitorScreen extends StatefulWidget { const TrafficMonitorScreen({super.key}); @@ -153,7 +155,7 @@ class _TrafficMonitorScreenState extends State { color: cs.onSurface, fontSize: 20, fontWeight: FontWeight.w700, - fontFamily: 'Outfit', + fontFamily: displayFontOf(context), ), ), ), @@ -202,7 +204,7 @@ class _TrafficMonitorScreenState extends State { padding: const EdgeInsets.fromLTRB(16, 10, 12, 10), decoration: BoxDecoration( color: cs.surfaceContainerHigh, - borderRadius: BorderRadius.circular(16), + borderRadius: AppShape.cardRadius, ), child: AnimatedBuilder( animation: _monitor, @@ -216,7 +218,7 @@ class _TrafficMonitorScreenState extends State { height: 10, decoration: BoxDecoration( shape: BoxShape.circle, - color: on ? kOnlineGreen : cs.outline, + color: on ? kSuccessGreen : cs.outline, ), ), const SizedBox(width: 12), diff --git a/lib/frontend/screens/profile/web_qr_scan_screen.dart b/lib/frontend/screens/profile/web_qr_scan_screen.dart index d0e9b55..d01a930 100644 --- a/lib/frontend/screens/profile/web_qr_scan_screen.dart +++ b/lib/frontend/screens/profile/web_qr_scan_screen.dart @@ -5,9 +5,11 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; +import '../../../core/config/app_colors.dart'; import '../../widgets/animated_slash_icon.dart'; import '../../widgets/connection_status.dart'; +import '../../../core/config/app_fonts.dart'; class WebQrScanScreen extends StatefulWidget { const WebQrScanScreen({super.key}); @@ -73,7 +75,7 @@ class _WebQrScanScreenState extends State { title: Text( 'QR для веба и ПК', style: TextStyle( - fontFamily: 'Outfit', + fontFamily: displayFontOf(context), fontSize: 20, fontWeight: FontWeight.w600, color: Colors.white, @@ -138,7 +140,7 @@ class _WebQrScanScreenState extends State { 'Наведите камеру на QR-код на экране компьютера', textAlign: TextAlign.center, style: TextStyle( - fontFamily: 'Outfit', + fontFamily: displayFontOf(context), fontSize: 15, fontWeight: FontWeight.w500, color: Colors.white, @@ -360,7 +362,7 @@ class _TelegramStyleFinderOverlayState final finderRect = _interpolatedFinderRect(); final rrect = _finderRRect(finderRect, _frameCornerRadius); - final frameColor = _qrInView ? const Color(0xFF4ADE80) : Colors.white; + final frameColor = _qrInView ? kSuccessGreen : Colors.white; return IgnorePointer( child: Stack( fit: StackFit.expand, diff --git a/lib/frontend/screens/stories/story_composer_screen.dart b/lib/frontend/screens/stories/story_composer_screen.dart index 916ebc6..acf7531 100644 --- a/lib/frontend/screens/stories/story_composer_screen.dart +++ b/lib/frontend/screens/stories/story_composer_screen.dart @@ -9,6 +9,7 @@ import '../../../core/utils/haptics.dart'; import '../../../main.dart' show fileUploader, messagesModule, storiesModule; import '../../widgets/custom_notification.dart'; import '../../widgets/primary_loading_button.dart'; +import '../../../core/config/app_frost.dart'; const int _storyExpiration = 86400000; @@ -177,7 +178,10 @@ class _StoryComposerScreenState extends State { children: [ Positioned.fill( child: ImageFiltered( - imageFilter: ui.ImageFilter.blur(sigmaX: 30, sigmaY: 30), + imageFilter: ui.ImageFilter.blur( + sigmaX: AppFrost.mediaBackdropSigma, + sigmaY: AppFrost.mediaBackdropSigma, + ), child: backdrop, ), ), diff --git a/lib/frontend/screens/stories/story_ring.dart b/lib/frontend/screens/stories/story_ring.dart index 33f7083..558fa6a 100644 --- a/lib/frontend/screens/stories/story_ring.dart +++ b/lib/frontend/screens/stories/story_ring.dart @@ -2,6 +2,7 @@ import 'dart:math' as math; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:material_symbols_icons/symbols.dart'; import '../../../core/utils/haptics.dart'; import '../../../models/story.dart'; @@ -110,7 +111,8 @@ class _StoryRingState extends State { owner: widget.preview.owner, overrideInfo: widget.ownerOverride, builder: (context, info) { - final name = widget.selfLabel ?? + final name = + widget.selfLabel ?? (info?.name.isNotEmpty == true ? info!.name : '…'); return Padding( padding: const EdgeInsets.only(right: 16), @@ -212,7 +214,13 @@ class SegmentedRingPainter extends CustomPainter { for (var i = 0; i < n; i++) { final start = -math.pi / 2 + gap / 2 + i * segment; - canvas.drawArc(rect, start, sweep, false, i < read ? readPaint : unreadPaint); + canvas.drawArc( + rect, + start, + sweep, + false, + i < read ? readPaint : unreadPaint, + ); } } @@ -355,7 +363,7 @@ class _StorySelfTileState extends State { color: cs.primary, ), child: Icon( - Icons.add, + Symbols.add, size: 14, color: cs.onPrimary, ), diff --git a/lib/frontend/screens/stories/story_viewer_screen.dart b/lib/frontend/screens/stories/story_viewer_screen.dart index b021bd8..ef652b4 100644 --- a/lib/frontend/screens/stories/story_viewer_screen.dart +++ b/lib/frontend/screens/stories/story_viewer_screen.dart @@ -18,6 +18,8 @@ import '../../widgets/liquid_glass.dart'; import '../../widgets/lottie_image.dart'; import '../../widgets/small_spinner.dart'; import 'story_owner_info.dart'; +import '../../../core/config/app_frost.dart'; +import '../../../core/config/app_fonts.dart'; const _quickReactions = ['❤️', '🔥', '😍', '👏', '😂', '😮']; const Duration _photoDuration = Duration(seconds: 5); @@ -64,7 +66,8 @@ Future openStoryViewer( animation: animation, child: child, builder: (context, child) { - final closing = animation.status == AnimationStatus.reverse || + final closing = + animation.status == AnimationStatus.reverse || animation.status == AnimationStatus.dismissed; // Круговое раскрытие — только на открытии; закрытие всегда // мягким fade + scale (круг «схлопыванием» резал кадр). @@ -214,7 +217,9 @@ class _StoryViewerScreenState extends State return; } setState(() => _loading[ownerId] = true); - final stories = await storiesModule.getByOwner(widget.previews[index].owner); + final stories = await storiesModule.getByOwner( + widget.previews[index].owner, + ); if (!mounted) return; setState(() { _stories[ownerId] = stories; @@ -434,8 +439,11 @@ class _StoryViewerScreenState extends State ? _buildActiveOwner() : _OwnerCover( preview: widget.previews[index], - overrideInfo: widget.ownerOverrides[ - widget.previews[index].owner.ownerId], + overrideInfo: + widget.ownerOverrides[widget + .previews[index] + .owner + .ownerId], ); return _CubePage( controller: _ownerController, @@ -530,9 +538,7 @@ class _StoryViewerScreenState extends State ), const _TopScrim(), if (loading) - const Center( - child: SmallSpinner(size: 28, color: Colors.white), - ), + const Center(child: SmallSpinner(size: 28, color: Colors.white)), SafeArea( child: Column( children: [ @@ -612,14 +618,12 @@ class _StoryViewerScreenState extends State info?.name.isNotEmpty == true ? info!.name : '…', maxLines: 1, overflow: TextOverflow.ellipsis, - style: const TextStyle( + style: TextStyle( color: Colors.white, fontSize: 15, fontWeight: FontWeight.w600, - fontFamily: 'Outfit', - shadows: [ - Shadow(color: Colors.black54, blurRadius: 4), - ], + fontFamily: displayFontOf(context), + shadows: [Shadow(color: Colors.black54, blurRadius: 4)], ), ), if (story != null && story.time > 0) @@ -1034,7 +1038,10 @@ class _StoryMediaView extends StatelessWidget { final Widget blurBg = preview != null ? Positioned.fill( child: ImageFiltered( - imageFilter: ui.ImageFilter.blur(sigmaX: 30, sigmaY: 30), + imageFilter: ui.ImageFilter.blur( + sigmaX: AppFrost.mediaBackdropSigma, + sigmaY: AppFrost.mediaBackdropSigma, + ), child: Image(image: preview, fit: BoxFit.cover), ), ) @@ -1056,24 +1063,22 @@ class _StoryMediaView extends StatelessWidget { fit: BoxFit.contain, ); } else if (preview != null) { - fg = Center(child: Image(image: preview, fit: BoxFit.contain)); + fg = Center( + child: Image(image: preview, fit: BoxFit.contain), + ); } else { fg = const SizedBox.shrink(); } - return Stack( - fit: StackFit.expand, - children: [ - blurBg, - fg, - ], - ); + return Stack(fit: StackFit.expand, children: [blurBg, fg]); } final url = media.url; Widget fg; if (url == null || url.isEmpty) { fg = preview != null - ? Center(child: Image(image: preview, fit: BoxFit.contain)) + ? Center( + child: Image(image: preview, fit: BoxFit.contain), + ) : const SizedBox.shrink(); } else { fg = CachedNetworkImage( @@ -1081,22 +1086,24 @@ class _StoryMediaView extends StatelessWidget { fit: BoxFit.contain, fadeInDuration: const Duration(milliseconds: 200), placeholder: preview != null - ? (context, _) => Center(child: Image(image: preview, fit: BoxFit.contain)) + ? (context, _) => Center( + child: Image(image: preview, fit: BoxFit.contain), + ) : null, errorWidget: (context, _, _) => preview != null - ? Center(child: Image(image: preview, fit: BoxFit.contain)) + ? Center( + child: Image(image: preview, fit: BoxFit.contain), + ) : const Center( - child: Icon(Symbols.broken_image, color: Colors.white54, size: 48), + child: Icon( + Symbols.broken_image, + color: Colors.white54, + size: 48, + ), ), ); } - return Stack( - fit: StackFit.expand, - children: [ - blurBg, - fg, - ], - ); + return Stack(fit: StackFit.expand, children: [blurBg, fg]); } } diff --git a/lib/frontend/widgets/account_switcher_overlay.dart b/lib/frontend/widgets/account_switcher_overlay.dart index 983f672..dd7ca00 100644 --- a/lib/frontend/widgets/account_switcher_overlay.dart +++ b/lib/frontend/widgets/account_switcher_overlay.dart @@ -9,6 +9,7 @@ import '../../core/storage/token_storage.dart'; import '../../core/utils/haptics.dart'; import 'animated_overlay_popup.dart'; import 'komet_avatar.dart'; +import '../../core/config/app_frost.dart'; class AccountSwitcherController extends ChangeNotifier { Offset? pointer; @@ -239,7 +240,7 @@ class _AccountSwitcherLayerState extends State<_AccountSwitcherLayer> animation: overlayAnimation, builder: (ctx, _) { final t = overlayAnimation.value.clamp(0.0, 1.0); - final blurSigma = 14.0 * t; + final blurSigma = AppFrost.overlaySigma * t; return GestureDetector( onTap: closeOverlay, behavior: HitTestBehavior.opaque, diff --git a/lib/frontend/widgets/attachment/attachment_sheet.dart b/lib/frontend/widgets/attachment/attachment_sheet.dart index cb7d5dd..8794899 100644 --- a/lib/frontend/widgets/attachment/attachment_sheet.dart +++ b/lib/frontend/widgets/attachment/attachment_sheet.dart @@ -45,7 +45,7 @@ Future showAttachmentSheet( isScrollControlled: true, requestFocus: false, backgroundColor: Colors.transparent, - barrierColor: Colors.black.withValues(alpha: 0.45), + barrierColor: AppFrost.scrim(), builder: (_) => AttachmentSheet( title: title, onSend: onSend, @@ -1093,7 +1093,9 @@ class _ThumbnailState extends State<_Thumbnail> { } void _resolveProvider() { - final file = widget.editedFile ?? (widget.item.isVideo ? null : widget.item.localFile); + final file = + widget.editedFile ?? + (widget.item.isVideo ? null : widget.item.localFile); if (file != null) { _provider = ResizeImage( FileImage(file), diff --git a/lib/frontend/widgets/attachment/media_preview_screen.dart b/lib/frontend/widgets/attachment/media_preview_screen.dart index ad69d83..cb7cb36 100644 --- a/lib/frontend/widgets/attachment/media_preview_screen.dart +++ b/lib/frontend/widgets/attachment/media_preview_screen.dart @@ -356,7 +356,7 @@ class _SelectionToggle extends StatelessWidget { alignment: Alignment.center, decoration: BoxDecoration( shape: BoxShape.circle, - color: isSelected ? kEditorAccent : Colors.transparent, + color: isSelected ? MediaAccent.of(context) : Colors.transparent, border: Border.all(color: Colors.white, width: 2), ), child: isSelected @@ -466,7 +466,7 @@ class _FileToggleState extends State<_FileToggle> { builder: (context, t, _) { final color = Color.lerp( Colors.white54, - Color.lerp(Colors.white, kEditorAccent, 0.4), + Color.lerp(Colors.white, MediaAccent.of(context), 0.4), t, ); return Icon(Symbols.description, color: color, size: 24); @@ -484,7 +484,7 @@ class _SendButton extends StatelessWidget { @override Widget build(BuildContext context) { return Material( - color: kEditorAccent, + color: MediaAccent.of(context), shape: const CircleBorder(), child: InkWell( customBorder: const CircleBorder(), diff --git a/lib/frontend/widgets/attachment/photo_editor.dart b/lib/frontend/widgets/attachment/photo_editor.dart index e238a81..dced092 100644 --- a/lib/frontend/widgets/attachment/photo_editor.dart +++ b/lib/frontend/widgets/attachment/photo_editor.dart @@ -13,9 +13,20 @@ import 'package:komet/frontend/widgets/custom_notification.dart'; import '../../../core/config/app_colors.dart'; import '../../../l10n/app_localizations.dart'; import '../small_spinner.dart'; +import '../../../core/config/app_shape.dart'; const Color _kPanel = Color(0xFF0A0A0A); +const List _kPenWheel = [ + Color(0xFFFF3B30), + Color(0xFFFFCC00), + Color(0xFF34C759), + Color(0xFF00C7BE), + Color(0xFF2F8FFF), + Color(0xFFAF52DE), + Color(0xFFFF3B30), +]; + class CropState { final int quarterTurns; final bool flipH; @@ -404,9 +415,7 @@ class _PhotoCropEditorState extends State { Widget _buildViewport() { final img = _image; if (img == null) { - return const Center( - child: SmallSpinner(size: 36, color: Colors.white), - ); + return const Center(child: SmallSpinner(size: 36, color: Colors.white)); } return LayoutBuilder( builder: (context, constraints) { @@ -445,7 +454,7 @@ class _PhotoCropEditorState extends State { onPressed: _flip, icon: Icon( Symbols.flip, - color: _flipH ? kEditorAccent : Colors.white, + color: _flipH ? MediaAccent.of(context) : Colors.white, ), tooltip: l10n.photoEditorFlipTooltip, ), @@ -501,7 +510,7 @@ class _PhotoCropEditorState extends State { child: Text( l10n.photoEditorDone, style: TextStyle( - color: _baking ? Colors.white38 : kEditorAccent, + color: _baking ? Colors.white38 : MediaAccent.of(context), fontSize: 15, fontWeight: FontWeight.w600, ), @@ -594,7 +603,9 @@ class _StraightenRuler extends StatelessWidget { onDoubleTap: () => onChanged(0), child: SizedBox( height: 56, - child: CustomPaint(painter: _RulerPainter(value)), + child: CustomPaint( + painter: _RulerPainter(value, MediaAccent.of(context)), + ), ), ); } @@ -602,8 +613,9 @@ class _StraightenRuler extends StatelessWidget { class _RulerPainter extends CustomPainter { final double value; + final Color accent; - _RulerPainter(this.value); + _RulerPainter(this.value, this.accent); @override void paint(Canvas canvas, Size size) { @@ -638,14 +650,15 @@ class _RulerPainter extends CustomPainter { Offset(cx, baseY - 18), Offset(cx, baseY + 2), Paint() - ..color = kEditorAccent + ..color = accent ..strokeWidth = 2 ..strokeCap = StrokeCap.round, ); } @override - bool shouldRepaint(covariant _RulerPainter old) => old.value != value; + bool shouldRepaint(covariant _RulerPainter old) => + old.value != value || old.accent != accent; } const Color _kDrawPanel = Color(0xFF101010); @@ -897,6 +910,7 @@ class _PhotoDrawEditorState extends State { context: context, builder: (ctx) => AlertDialog( backgroundColor: const Color(0xFF1E1E1E), + shape: AppShape.dialogBorder, title: Text( l10n.photoEditorTextDialogTitle, style: const TextStyle(color: Colors.white), @@ -1094,7 +1108,12 @@ class _PhotoDrawEditorState extends State { if (selected != null) Positioned.fill( child: IgnorePointer( - child: CustomPaint(painter: _SelectionPainter(selected)), + child: CustomPaint( + painter: _SelectionPainter( + selected, + MediaAccent.of(context), + ), + ), ), ), ], @@ -1235,17 +1254,7 @@ class _PhotoDrawEditorState extends State { padding: const EdgeInsets.all(4), decoration: const BoxDecoration( shape: BoxShape.circle, - gradient: SweepGradient( - colors: [ - Color(0xFFFF3B30), - Color(0xFFFFCC00), - kOnlineGreen, - Color(0xFF00C7BE), - kEditorAccent, - Color(0xFFAF52DE), - Color(0xFFFF3B30), - ], - ), + gradient: SweepGradient(colors: _kPenWheel), ), child: Container( decoration: BoxDecoration( @@ -1650,8 +1659,9 @@ Size textMarkSize(TextMark t) { class _SelectionPainter extends CustomPainter { final TextMark text; + final Color accent; - _SelectionPainter(this.text); + _SelectionPainter(this.text, this.accent); @override void paint(Canvas canvas, Size size) { @@ -1676,7 +1686,7 @@ class _SelectionPainter extends CustomPainter { _dashedLine(canvas, bl, tl, border); final fill = Paint() - ..color = kEditorAccent + ..color = accent ..style = PaintingStyle.fill; final ring = Paint() ..color = Colors.white @@ -2349,9 +2359,7 @@ class _PhotoAdjustEditorState extends State { Widget _buildPreview() { final img = _image; if (img == null) { - return const Center( - child: SmallSpinner(size: 36, color: Colors.white), - ); + return const Center(child: SmallSpinner(size: 36, color: Colors.white)); } return LayoutBuilder( builder: (context, constraints) { @@ -2664,12 +2672,16 @@ class _PhotoAdjustEditorState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - Icon(icon, color: selected ? kEditorAccent : Colors.white, size: 30), + Icon( + icon, + color: selected ? MediaAccent.of(context) : Colors.white, + size: 30, + ), const SizedBox(height: 6), Text( label, style: TextStyle( - color: selected ? kEditorAccent : Colors.white70, + color: selected ? MediaAccent.of(context) : Colors.white70, fontSize: 12, ), ), @@ -2704,7 +2716,7 @@ class _PhotoAdjustEditorState extends State { child: Text( l10n.photoEditorDone, style: TextStyle( - color: _baking ? Colors.white38 : kEditorAccent, + color: _baking ? Colors.white38 : MediaAccent.of(context), fontSize: 15, fontWeight: FontWeight.w600, ), @@ -2720,7 +2732,7 @@ class _PhotoAdjustEditorState extends State { return IconButton( onPressed: disabled ? null : () => setState(() => _tab = tab), icon: Icon(icon), - color: selected ? kEditorAccent : Colors.white, + color: selected ? MediaAccent.of(context) : Colors.white, disabledColor: Colors.white24, ); } diff --git a/lib/frontend/widgets/chat_info/shared_content_tabs.dart b/lib/frontend/widgets/chat_info/shared_content_tabs.dart index d41ad3a..8992e80 100644 --- a/lib/frontend/widgets/chat_info/shared_content_tabs.dart +++ b/lib/frontend/widgets/chat_info/shared_content_tabs.dart @@ -27,6 +27,7 @@ import '../photo_viewer.dart'; import '../reload_on_reconnect.dart'; import '../small_spinner.dart'; import '../swipe_route.dart'; +import '../sheet_helpers.dart'; enum SharedContentKind { media, files, voice, links } @@ -153,9 +154,7 @@ Future _showItemMenu(BuildContext context, List<_MenuAction> actions) { return showModalBottomSheet( context: context, backgroundColor: cs.surfaceContainerHigh, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(20)), - ), + shape: kSheetShape, builder: (sheetContext) => SafeArea( child: Column( mainAxisSize: MainAxisSize.min, @@ -419,7 +418,7 @@ class _CommonChatsTabState extends State final cs = Theme.of(context).colorScheme; if (_loading) return _loadingState(cs); if (_chats.isEmpty) { - return _emptyState(cs, widget.emptyLabel, Icons.group); + return _emptyState(cs, widget.emptyLabel, Symbols.group); } return Container( diff --git a/lib/frontend/widgets/chat_wallpaper_sheet.dart b/lib/frontend/widgets/chat_wallpaper_sheet.dart index aa69820..e51ba8e 100644 --- a/lib/frontend/widgets/chat_wallpaper_sheet.dart +++ b/lib/frontend/widgets/chat_wallpaper_sheet.dart @@ -2,7 +2,9 @@ import 'package:flutter/material.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:komet/core/config/chat_wallpaper_themes.dart'; +import 'package:komet/core/config/app_colors.dart'; import 'package:komet/core/storage/chat_wallpaper_store.dart'; +import '../../core/config/app_fonts.dart'; enum WallpaperPickType { none, theme, gallery } @@ -10,12 +12,10 @@ class WallpaperPick { final WallpaperPickType type; final ChatWallpaperTheme? theme; - const WallpaperPick.none() - : type = WallpaperPickType.none, - theme = null; + const WallpaperPick.none() : type = WallpaperPickType.none, theme = null; const WallpaperPick.gallery() - : type = WallpaperPickType.gallery, - theme = null; + : type = WallpaperPickType.gallery, + theme = null; const WallpaperPick.theme(this.theme) : type = WallpaperPickType.theme; } @@ -81,12 +81,12 @@ class _ChatWallpaperGalleryScreenState icon: const Icon(Symbols.arrow_back), onPressed: () => Navigator.pop(context), ), - title: const Text( + title: Text( 'Обои', style: TextStyle( fontSize: 22, fontWeight: FontWeight.w700, - fontFamily: 'Outfit', + fontFamily: displayFontOf(context), ), ), ), @@ -169,7 +169,9 @@ class _ChatWallpaperGalleryScreenState ), ), const SizedBox(width: 12), - Expanded(child: _ApplyButton(enabled: _changed, onTap: _apply)), + Expanded( + child: _ApplyButton(enabled: _changed, onTap: _apply), + ), ], ), ), @@ -216,6 +218,7 @@ class _SampleBubbles extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _bubble( + context, text: 'Как насчёт новых обоев для этого чата?', color: cs.surfaceContainerHighest.withValues(alpha: 0.94), textColor: cs.onSurface, @@ -223,6 +226,7 @@ class _SampleBubbles extends StatelessWidget { ), const SizedBox(height: 8), _bubble( + context, text: 'Выглядит отлично 🔥', color: cs.primary, textColor: cs.onPrimary, @@ -234,7 +238,8 @@ class _SampleBubbles extends StatelessWidget { ); } - Widget _bubble({ + Widget _bubble( + BuildContext context, { required String text, required Color color, required Color textColor, @@ -255,7 +260,7 @@ class _SampleBubbles extends StatelessWidget { style: TextStyle( color: textColor, fontSize: 15, - fontFamily: 'Outfit', + fontFamily: displayFontOf(context), ), ), ), @@ -336,7 +341,7 @@ class _TileFrame extends StatelessWidget { color: selected ? cs.primary : cs.onSurfaceVariant, fontSize: 12, fontWeight: FontWeight.w600, - fontFamily: 'Outfit', + fontFamily: displayFontOf(context), ), ), ], @@ -363,7 +368,7 @@ class _NoneTile extends StatelessWidget { child: ColoredBox( color: cs.surfaceContainerHighest, child: const Center( - child: Icon(Symbols.block, color: Color(0xFFFF3B30), size: 34), + child: Icon(Symbols.block, color: kDangerRed, size: 34), ), ), ); @@ -419,7 +424,7 @@ class _GalleryButton extends StatelessWidget { color: cs.onSurface, fontSize: 16, fontWeight: FontWeight.w600, - fontFamily: 'Outfit', + fontFamily: displayFontOf(context), ), ), ], @@ -456,7 +461,7 @@ class _ApplyButton extends StatelessWidget { color: cs.onPrimary, fontSize: 16, fontWeight: FontWeight.w700, - fontFamily: 'Outfit', + fontFamily: displayFontOf(context), ), ), ), diff --git a/lib/frontend/widgets/confirm_dialog.dart b/lib/frontend/widgets/confirm_dialog.dart index 36c2ec6..9d56248 100644 --- a/lib/frontend/widgets/confirm_dialog.dart +++ b/lib/frontend/widgets/confirm_dialog.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import '../../core/config/app_shape.dart'; /// Shared confirmation dialog. Returns true if confirmed, false otherwise. Future showConfirmDialog( @@ -14,7 +15,7 @@ Future showConfirmDialog( context: context, builder: (context) => AlertDialog( backgroundColor: cs.surfaceContainerHigh, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)), + shape: AppShape.dialogBorder, title: title == null ? null : Text(title, style: TextStyle(color: cs.onSurface)), diff --git a/lib/frontend/widgets/info_action_sheet.dart b/lib/frontend/widgets/info_action_sheet.dart index d19a2e6..7ec3235 100644 --- a/lib/frontend/widgets/info_action_sheet.dart +++ b/lib/frontend/widgets/info_action_sheet.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'sheet_helpers.dart'; +import '../../core/config/app_shape.dart'; class InfoActionSheetItem { final IconData icon; @@ -188,9 +189,7 @@ class _InfoActionSheetState extends State<_InfoActionSheet> { disabledBackgroundColor: cs.primary.withValues(alpha: 0.45), disabledForegroundColor: cs.onPrimary.withValues(alpha: 0.85), padding: const EdgeInsets.symmetric(vertical: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(28), - ), + shape: AppShape.buttonBorder, ), child: Text( buttonText, diff --git a/lib/frontend/widgets/message_actions_overlay.dart b/lib/frontend/widgets/message_actions_overlay.dart index e35b4f9..97071ac 100644 --- a/lib/frontend/widgets/message_actions_overlay.dart +++ b/lib/frontend/widgets/message_actions_overlay.dart @@ -21,11 +21,7 @@ class ReactionEmoji { final String? animationUrl; final String? staticUrl; - const ReactionEmoji({ - required this.emoji, - this.animationUrl, - this.staticUrl, - }); + const ReactionEmoji({required this.emoji, this.animationUrl, this.staticUrl}); } class MessageReader { @@ -114,6 +110,7 @@ void showMessageActions({ Future Function(int reasonId)? onReport, VoidCallback? onDelete, bool allowDelete = true, + bool allowCopy = true, VoidCallback? onEdit, VoidCallback? onReply, VoidCallback? onForward, @@ -153,6 +150,7 @@ void showMessageActions({ onReport: onReport, onDelete: onDelete, allowDelete: allowDelete, + allowCopy: allowCopy, onEdit: onEdit, onReply: onReply, onForward: onForward, @@ -189,6 +187,7 @@ class _MessageActionsLayer extends StatefulWidget { final Future Function(int reasonId)? onReport; final VoidCallback? onDelete; final bool allowDelete; + final bool allowCopy; final VoidCallback? onEdit; final VoidCallback? onReply; final VoidCallback? onForward; @@ -217,6 +216,7 @@ class _MessageActionsLayer extends StatefulWidget { this.onReport, this.onDelete, this.allowDelete = true, + this.allowCopy = true, this.onEdit, this.onReply, this.onForward, @@ -503,7 +503,8 @@ class _MessageActionsLayerState extends State<_MessageActionsLayer> _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 (hasText && widget.allowCopy) + _Action(Symbols.content_copy, l10n.msgActionsCopy, _copy), if (widget.isMe && widget.onEdit != null) _Action(Symbols.edit, l10n.msgActionsEdit, _edit), if (widget.onPin != null) @@ -1004,7 +1005,11 @@ class _MessageActionsLayerState extends State<_MessageActionsLayer> ); } - Widget _buildQuickRow(ColorScheme cs, double cell, List quick) { + Widget _buildQuickRow( + ColorScheme cs, + double cell, + List quick, + ) { return Center( child: Row( mainAxisSize: MainAxisSize.min, @@ -1232,9 +1237,7 @@ class _MessageActionsLayerState extends State<_MessageActionsLayer> if (_readByLoading) { body = const Padding( padding: EdgeInsets.symmetric(vertical: 28), - child: Center( - child: SmallSpinner(size: 24), - ), + child: Center(child: SmallSpinner(size: 24)), ); } else { final readers = _readers ?? const []; @@ -1315,9 +1318,7 @@ class _MessageActionsLayerState extends State<_MessageActionsLayer> if (_reportLoading) { body = const Padding( padding: EdgeInsets.symmetric(vertical: 28), - child: Center( - child: SmallSpinner(size: 24), - ), + child: Center(child: SmallSpinner(size: 24)), ); } else { final reasons = _reasons ?? const <({int id, String title})>[]; @@ -1644,9 +1645,7 @@ class _ReactionEmojiPickerState extends State<_ReactionEmojiPicker> { _buildSearchField(cs), Expanded( child: !_loaded - ? const Center( - child: SmallSpinner(size: 26), - ) + ? const Center(child: SmallSpinner(size: 26)) : _results.isEmpty ? const SizedBox.shrink() : LottieScrollScope( @@ -1761,7 +1760,8 @@ class _ReactionGlyph extends StatelessWidget { final anim = reaction.animationUrl; final still = reaction.staticUrl; final hasAsset = - (anim != null && anim.isNotEmpty) || (still != null && still.isNotEmpty); + (anim != null && anim.isNotEmpty) || + (still != null && still.isNotEmpty); if (!hasAsset) { return Center( child: Text(reaction.emoji, style: TextStyle(fontSize: size * 0.9)), diff --git a/lib/frontend/widgets/online_dot.dart b/lib/frontend/widgets/online_dot.dart index 14c6bce..2b5b05a 100644 --- a/lib/frontend/widgets/online_dot.dart +++ b/lib/frontend/widgets/online_dot.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import '../../core/cache/info_cache.dart'; +import '../../core/config/app_colors.dart'; class OnlineDot extends StatelessWidget { final int userId; @@ -14,7 +15,7 @@ class OnlineDot extends StatelessWidget { required this.userId, required this.borderColor, this.size = 12, - this.color = const Color(0xFF2EC36B), + this.color = kSuccessGreen, this.borderWidth = 2, }); diff --git a/lib/frontend/widgets/photo_viewer.dart b/lib/frontend/widgets/photo_viewer.dart index 9d28ebc..a45abdd 100644 --- a/lib/frontend/widgets/photo_viewer.dart +++ b/lib/frontend/widgets/photo_viewer.dart @@ -18,6 +18,7 @@ import '../../core/utils/media_cache.dart'; import '../../core/utils/media_saver.dart'; import '../../core/utils/save_file_as.dart'; import '../../l10n/app_localizations.dart'; +import '../../core/config/app_colors.dart'; import '../../main.dart'; import '../../models/attachment.dart'; import 'attachment/photo_hero.dart'; @@ -1498,7 +1499,7 @@ class _VideoSettingsButton extends StatelessWidget { final l10n = AppLocalizations.of(context)!; return PopupMenuButton( key: const ValueKey('video-settings'), - color: const Color(0xFF292326), + color: MediaAccent.schemeOf(context).surfaceContainerHigh, tooltip: l10n.videoViewerSettings, icon: const Icon(Symbols.settings, color: Colors.white), onSelected: (value) { @@ -1564,7 +1565,7 @@ class _SettingChoice extends StatelessWidget { child: Text(label, style: const TextStyle(color: Colors.white)), ), if (selected) - const Icon(Symbols.check, color: Color(0xFFE68ABA), size: 18), + Icon(Symbols.check, color: MediaAccent.of(context), size: 18), ], ); } diff --git a/lib/frontend/widgets/poll_view.dart b/lib/frontend/widgets/poll_view.dart index b68cf62..1a4334e 100644 --- a/lib/frontend/widgets/poll_view.dart +++ b/lib/frontend/widgets/poll_view.dart @@ -7,6 +7,7 @@ import '../../core/utils/haptics.dart'; import '../../models/poll.dart'; import 'custom_notification.dart'; import 'small_spinner.dart'; +import '../../core/config/app_shape.dart'; class PollView extends StatefulWidget { final int chatId; @@ -242,9 +243,7 @@ class _PollViewState extends State style: TextButton.styleFrom( foregroundColor: widget.accentColor, backgroundColor: widget.dimColor.withValues(alpha: 0.12), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), + shape: AppShape.buttonBorder, ), child: _voting ? SmallSpinner(size: 16, color: widget.accentColor) diff --git a/lib/frontend/widgets/primary_loading_button.dart b/lib/frontend/widgets/primary_loading_button.dart index 7581d1c..cb901dd 100644 --- a/lib/frontend/widgets/primary_loading_button.dart +++ b/lib/frontend/widgets/primary_loading_button.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'small_spinner.dart'; import 'springy_tap.dart'; +import '../../core/config/app_shape.dart'; class PrimaryLoadingButton extends StatelessWidget { final ValueListenable loading; @@ -34,13 +35,9 @@ class PrimaryLoadingButton extends StatelessWidget { backgroundColor: background ?? cs.primary, foregroundColor: fg, padding: const EdgeInsets.symmetric(vertical: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), + shape: AppShape.buttonBorder, ), - child: isLoading - ? SmallSpinner(size: 20, color: fg) - : child, + child: isLoading ? SmallSpinner(size: 20, color: fg) : child, ), ), ); diff --git a/lib/frontend/widgets/prompt_dialog.dart b/lib/frontend/widgets/prompt_dialog.dart index 9eab14f..7d1715d 100644 --- a/lib/frontend/widgets/prompt_dialog.dart +++ b/lib/frontend/widgets/prompt_dialog.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import '../../core/config/app_fonts.dart'; +import '../../core/config/app_shape.dart'; Future showTextInputDialog( BuildContext context, { @@ -20,12 +22,13 @@ Future showTextInputDialog( final cs = Theme.of(dialogContext).colorScheme; return AlertDialog( backgroundColor: cs.surfaceContainerHigh, + shape: AppShape.dialogBorder, title: title == null ? null : Text( title, style: TextStyle( - fontFamily: 'Outfit', + fontFamily: displayFontOf(context), fontWeight: FontWeight.w600, fontSize: 18, color: cs.onSurface, diff --git a/lib/frontend/widgets/schedule_time_picker.dart b/lib/frontend/widgets/schedule_time_picker.dart index 04e9624..dde55a8 100644 --- a/lib/frontend/widgets/schedule_time_picker.dart +++ b/lib/frontend/widgets/schedule_time_picker.dart @@ -4,6 +4,8 @@ import 'package:flutter/material.dart'; import '../../core/utils/format.dart'; import 'custom_notification.dart'; import 'sheet_helpers.dart'; +import '../../core/config/app_fonts.dart'; +import '../../core/config/app_shape.dart'; const List _weekdayShort = ['пн', 'вт', 'ср', 'чт', 'пт', 'сб', 'вс']; @@ -132,7 +134,7 @@ class _ScheduleSheetState extends State<_ScheduleSheet> { color: cs.onSurface, fontSize: 18, fontWeight: FontWeight.w600, - fontFamily: 'Outfit', + fontFamily: displayFontOf(context), ), ), const SizedBox(height: 8), @@ -172,9 +174,7 @@ class _ScheduleSheetState extends State<_ScheduleSheet> { child: FilledButton( style: FilledButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 16), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), + shape: AppShape.buttonBorder, ), onPressed: _confirm, child: Text( diff --git a/lib/frontend/widgets/settings_card.dart b/lib/frontend/widgets/settings_card.dart index b8a1870..8a4d66f 100644 --- a/lib/frontend/widgets/settings_card.dart +++ b/lib/frontend/widgets/settings_card.dart @@ -1,8 +1,34 @@ import 'package:flutter/material.dart'; import 'package:material_symbols_icons/symbols.dart'; +import '../../core/config/app_shape.dart'; import 'glossy_pill.dart'; +class SettingsPanel extends StatelessWidget { + final Widget child; + final EdgeInsetsGeometry padding; + final Color? color; + + const SettingsPanel({ + super.key, + required this.child, + this.padding = const EdgeInsets.fromLTRB(20, 18, 20, 20), + this.color, + }); + + @override + Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; + return GlossyPill( + color: color ?? cs.surfaceContainerHigh, + borderRadius: AppShape.cardRadius, + padding: padding, + depth: 6, + child: child, + ); + } +} + class SettingsCard extends StatelessWidget { final List children; @@ -13,7 +39,7 @@ class SettingsCard extends StatelessWidget { final cs = Theme.of(context).colorScheme; return GlossyPill( color: cs.surfaceContainerHigh, - borderRadius: BorderRadius.circular(20), + borderRadius: AppShape.cardRadius, depth: 6, child: Column( children: [ @@ -135,7 +161,9 @@ class SettingsNavTile extends StatelessWidget { child: InkWell( onTap: onTap ?? () {}, borderRadius: isLast - ? const BorderRadius.vertical(bottom: Radius.circular(20)) + ? const BorderRadius.vertical( + bottom: Radius.circular(AppShape.card), + ) : null, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 17), diff --git a/lib/frontend/widgets/sheet_helpers.dart b/lib/frontend/widgets/sheet_helpers.dart index 64909a0..67e7e45 100644 --- a/lib/frontend/widgets/sheet_helpers.dart +++ b/lib/frontend/widgets/sheet_helpers.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; +import '../../core/config/app_shape.dart'; + /// Standard rounded top shape for modal bottom sheets. -const RoundedRectangleBorder kSheetShape = RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(24)), -); +const RoundedRectangleBorder kSheetShape = AppShape.sheetBorder; /// The little drag "grabber" pill shown at the top of a bottom sheet. class SheetGrabber extends StatelessWidget { diff --git a/lib/frontend/widgets/sticker_pack_sheet.dart b/lib/frontend/widgets/sticker_pack_sheet.dart index eae24c4..bea045f 100644 --- a/lib/frontend/widgets/sticker_pack_sheet.dart +++ b/lib/frontend/widgets/sticker_pack_sheet.dart @@ -10,6 +10,7 @@ import 'custom_notification.dart'; import 'small_spinner.dart'; import 'lottie_image.dart'; import 'sticker_peek.dart'; +import '../../core/config/app_shape.dart'; enum _PackAction { forward, copyLink } @@ -304,9 +305,7 @@ class _StickerPackSheetState extends State<_StickerPackSheet> { ? cs.surfaceContainerHighest : cs.primary, foregroundColor: _isFavorite ? cs.onSurface : cs.onPrimary, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(14), - ), + shape: AppShape.buttonBorder, ), onPressed: _busy ? null : _toggle, child: _busy diff --git a/lib/frontend/widgets/sticker_peek.dart b/lib/frontend/widgets/sticker_peek.dart index 99d5330..aaf08bb 100644 --- a/lib/frontend/widgets/sticker_peek.dart +++ b/lib/frontend/widgets/sticker_peek.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import '../../core/utils/haptics.dart'; import 'lottie_image.dart'; +import '../../core/config/app_frost.dart'; class _PeekData { final String? url; @@ -210,7 +211,10 @@ class _PeekOverlay extends StatelessWidget { children: [ Positioned.fill( child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 20 * t, sigmaY: 20 * t), + filter: ImageFilter.blur( + sigmaX: AppFrost.overlaySigma * t, + sigmaY: AppFrost.overlaySigma * t, + ), child: ColoredBox( color: Colors.black.withValues(alpha: 0.3 * t), ), diff --git a/lib/frontend/widgets/update_dialog.dart b/lib/frontend/widgets/update_dialog.dart index 8cb9f9a..56ddf70 100644 --- a/lib/frontend/widgets/update_dialog.dart +++ b/lib/frontend/widgets/update_dialog.dart @@ -5,11 +5,10 @@ import '../../core/utils/update_installer.dart'; import '../../core/utils/link_opener.dart'; import '../../l10n/app_localizations.dart'; import 'custom_notification.dart'; +import '../../core/config/app_fonts.dart'; +import '../../core/config/app_shape.dart'; -Future showUpdateDialog( - BuildContext context, - AppUpdateInfo info, -) async { +Future showUpdateDialog(BuildContext context, AppUpdateInfo info) async { final l10n = AppLocalizations.of(context)!; await showDialog( context: context, @@ -18,10 +17,11 @@ Future showUpdateDialog( final notes = info.notes; return AlertDialog( backgroundColor: cs.surfaceContainerHigh, + shape: AppShape.dialogBorder, title: Text( l10n.updateAvailableTitle, style: TextStyle( - fontFamily: 'Outfit', + fontFamily: displayFontOf(context), fontWeight: FontWeight.w600, fontSize: 18, color: cs.onSurface, @@ -152,6 +152,7 @@ class _UpdateProgressDialogState extends State<_UpdateProgressDialog> { final percent = (_progress * 100).round(); return AlertDialog( backgroundColor: cs.surfaceContainerHigh, + shape: AppShape.dialogBorder, content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/frontend/widgets/web_qr_login.dart b/lib/frontend/widgets/web_qr_login.dart index d216afd..2af6b0a 100644 --- a/lib/frontend/widgets/web_qr_login.dart +++ b/lib/frontend/widgets/web_qr_login.dart @@ -4,6 +4,7 @@ import '../../main.dart' show accountModule; import 'custom_notification.dart'; import 'sheet_helpers.dart'; import 'small_spinner.dart'; +import '../../core/config/app_fonts.dart'; Future showWebQrLoginConfirmSheet(BuildContext context) async { final agreed = await showModalBottomSheet( @@ -24,7 +25,7 @@ Future showWebQrLoginConfirmSheet(BuildContext context) async { Text( 'Вход по QR', style: TextStyle( - fontFamily: 'Outfit', + fontFamily: displayFontOf(context), fontSize: 20, fontWeight: FontWeight.w700, color: cs.onSurface, diff --git a/lib/frontend/widgets/webview_permission_prompt.dart b/lib/frontend/widgets/webview_permission_prompt.dart index 552ccac..0a77724 100644 --- a/lib/frontend/widgets/webview_permission_prompt.dart +++ b/lib/frontend/widgets/webview_permission_prompt.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; +import '../../core/config/app_shape.dart'; String _resourceLabel(PermissionResourceType type) { if (type == PermissionResourceType.CAMERA) return 'камера'; @@ -16,9 +17,9 @@ Future askWebViewPermission( PermissionRequest request, ) async { PermissionResponse deny() => PermissionResponse( - resources: request.resources, - action: PermissionResponseAction.DENY, - ); + resources: request.resources, + action: PermissionResponseAction.DENY, + ); if (!context.mounted) return deny(); @@ -32,6 +33,7 @@ Future askWebViewPermission( final granted = await showDialog( context: context, builder: (ctx) => AlertDialog( + shape: AppShape.dialogBorder, title: const Text('Запрос доступа'), content: Text('$host запрашивает доступ к: $labels.'), actions: [ diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 1796720..b5cb329 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -123,8 +123,12 @@ "profileMenuSpoof": "Spoofing", "infoTitle": "Info", "infoAccountSection": "Account", + "infoPacketSection": "Login packet", + "infoChatsSection": "Chats in login packet", + "infoChatSettingsSection": "Per-chat settings", "infoServerSection": "Server", "infoUserSection": "User", + "infoExperimentsSection": "Experiments", "infoYMapSection": "Y-Map", "infoFileUploadTypes": "file-upload-unsupported-types", "infoWhiteListLinks": "white-list-links", @@ -133,7 +137,30 @@ "infoVideoChatHistory": "videoChatHistory", "infoUpdateTime": "updateTime", "infoId": "id", + "infoPhone": "phone", + "infoPhotoId": "photoId", + "infoAccountStatus": "accountStatus", + "infoContactOptions": "contact options", + "infoProfileOptions": "profile options", + "infoNames": "names", + "infoBaseUrl": "baseUrl", + "infoBaseRawUrl": "baseRawUrl", "infoChatMarker": "chatMarker", + "infoServerTime": "server time", + "infoUpdates": "updates", + "infoMessagesCount": "messages in packet", + "infoContactsCount": "contacts in packet", + "infoPresenceCount": "presence records", + "infoConfigHash": "config hash", + "infoChatsCount": "chats loaded", + "infoChatsActive": "active", + "infoChatsHidden": "hidden", + "infoChatsDialogs": "dialogs", + "infoChatsGroups": "groups", + "infoChatsChannels": "channels", + "infoChatsUnread": "unread chats", + "infoChatsNewMessages": "new messages", + "infoChatsMessages": "messages in loaded chats", "infoAccountRemovalEnabled": "account-removal-enabled", "infoImageSize": "image-size", "infoGce": "gce", @@ -300,6 +327,7 @@ "appearanceTitle": "Appearance", "appearanceVisualStyleTitle": "Visual style", "appearanceVisualStyleSubtitle": "Material You or dimensional Glossy capsules", + "appearanceStyleAuto": "Match theme", "appearanceVisualStyleMaterialYou": "Material You", "appearanceVisualStyleGlossy": "Glossy", "appearanceVisualStyleLiquidGlass": "Liquid Glass", @@ -683,6 +711,17 @@ "chatInfoNoData": "No data", "chatInfoHideExtra": "Hide", "chatInfoShowMoreExtra": "Details", + "chatSendConfirmMessage": "Send this message to the chat?", + "chatSendConfirmAction": "Send", + "chatInfoRowDisableForward": "Forwarding disabled", + "chatInfoRowCopyDisabled": "Copying disabled", + "chatInfoRowOnlyAdminCall": "Admins can call", + "chatInfoRowAllCanPin": "Anyone can pin", + "chatInfoRowMembersSeeLink": "Members see the link", + "chatInfoRowConfirmBeforeSend": "Confirm before sending", + "chatInfoRowOnlyOwnerIconTitle": "Owner edits title and icon", + "chatInfoRowPromotedDisabled": "Promoted content off", + "chatInfoRowUserId": "User ID", "chatInfoRowId": "Chat ID", "chatInfoRowCreated": "Created", "chatInfoRowModified": "Modified", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 7f40c4c..240898e 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -740,6 +740,24 @@ abstract class AppLocalizations { /// **'Account'** String get infoAccountSection; + /// No description provided for @infoPacketSection. + /// + /// In en, this message translates to: + /// **'Login packet'** + String get infoPacketSection; + + /// No description provided for @infoChatsSection. + /// + /// In en, this message translates to: + /// **'Chats in login packet'** + String get infoChatsSection; + + /// No description provided for @infoChatSettingsSection. + /// + /// In en, this message translates to: + /// **'Per-chat settings'** + String get infoChatSettingsSection; + /// No description provided for @infoServerSection. /// /// In en, this message translates to: @@ -752,6 +770,12 @@ abstract class AppLocalizations { /// **'User'** String get infoUserSection; + /// No description provided for @infoExperimentsSection. + /// + /// In en, this message translates to: + /// **'Experiments'** + String get infoExperimentsSection; + /// No description provided for @infoYMapSection. /// /// In en, this message translates to: @@ -800,12 +824,150 @@ abstract class AppLocalizations { /// **'id'** String get infoId; + /// No description provided for @infoPhone. + /// + /// In en, this message translates to: + /// **'phone'** + String get infoPhone; + + /// No description provided for @infoPhotoId. + /// + /// In en, this message translates to: + /// **'photoId'** + String get infoPhotoId; + + /// No description provided for @infoAccountStatus. + /// + /// In en, this message translates to: + /// **'accountStatus'** + String get infoAccountStatus; + + /// No description provided for @infoContactOptions. + /// + /// In en, this message translates to: + /// **'contact options'** + String get infoContactOptions; + + /// No description provided for @infoProfileOptions. + /// + /// In en, this message translates to: + /// **'profile options'** + String get infoProfileOptions; + + /// No description provided for @infoNames. + /// + /// In en, this message translates to: + /// **'names'** + String get infoNames; + + /// No description provided for @infoBaseUrl. + /// + /// In en, this message translates to: + /// **'baseUrl'** + String get infoBaseUrl; + + /// No description provided for @infoBaseRawUrl. + /// + /// In en, this message translates to: + /// **'baseRawUrl'** + String get infoBaseRawUrl; + /// No description provided for @infoChatMarker. /// /// In en, this message translates to: /// **'chatMarker'** String get infoChatMarker; + /// No description provided for @infoServerTime. + /// + /// In en, this message translates to: + /// **'server time'** + String get infoServerTime; + + /// No description provided for @infoUpdates. + /// + /// In en, this message translates to: + /// **'updates'** + String get infoUpdates; + + /// No description provided for @infoMessagesCount. + /// + /// In en, this message translates to: + /// **'messages in packet'** + String get infoMessagesCount; + + /// No description provided for @infoContactsCount. + /// + /// In en, this message translates to: + /// **'contacts in packet'** + String get infoContactsCount; + + /// No description provided for @infoPresenceCount. + /// + /// In en, this message translates to: + /// **'presence records'** + String get infoPresenceCount; + + /// No description provided for @infoConfigHash. + /// + /// In en, this message translates to: + /// **'config hash'** + String get infoConfigHash; + + /// No description provided for @infoChatsCount. + /// + /// In en, this message translates to: + /// **'chats loaded'** + String get infoChatsCount; + + /// No description provided for @infoChatsActive. + /// + /// In en, this message translates to: + /// **'active'** + String get infoChatsActive; + + /// No description provided for @infoChatsHidden. + /// + /// In en, this message translates to: + /// **'hidden'** + String get infoChatsHidden; + + /// No description provided for @infoChatsDialogs. + /// + /// In en, this message translates to: + /// **'dialogs'** + String get infoChatsDialogs; + + /// No description provided for @infoChatsGroups. + /// + /// In en, this message translates to: + /// **'groups'** + String get infoChatsGroups; + + /// No description provided for @infoChatsChannels. + /// + /// In en, this message translates to: + /// **'channels'** + String get infoChatsChannels; + + /// No description provided for @infoChatsUnread. + /// + /// In en, this message translates to: + /// **'unread chats'** + String get infoChatsUnread; + + /// No description provided for @infoChatsNewMessages. + /// + /// In en, this message translates to: + /// **'new messages'** + String get infoChatsNewMessages; + + /// No description provided for @infoChatsMessages. + /// + /// In en, this message translates to: + /// **'messages in loaded chats'** + String get infoChatsMessages; + /// No description provided for @infoAccountRemovalEnabled. /// /// In en, this message translates to: @@ -1544,6 +1706,12 @@ abstract class AppLocalizations { /// **'Material You or dimensional Glossy capsules'** String get appearanceVisualStyleSubtitle; + /// No description provided for @appearanceStyleAuto. + /// + /// In en, this message translates to: + /// **'Match theme'** + String get appearanceStyleAuto; + /// No description provided for @appearanceVisualStyleMaterialYou. /// /// In en, this message translates to: @@ -3110,6 +3278,72 @@ abstract class AppLocalizations { /// **'Details'** String get chatInfoShowMoreExtra; + /// No description provided for @chatSendConfirmMessage. + /// + /// In en, this message translates to: + /// **'Send this message to the chat?'** + String get chatSendConfirmMessage; + + /// No description provided for @chatSendConfirmAction. + /// + /// In en, this message translates to: + /// **'Send'** + String get chatSendConfirmAction; + + /// No description provided for @chatInfoRowDisableForward. + /// + /// In en, this message translates to: + /// **'Forwarding disabled'** + String get chatInfoRowDisableForward; + + /// No description provided for @chatInfoRowCopyDisabled. + /// + /// In en, this message translates to: + /// **'Copying disabled'** + String get chatInfoRowCopyDisabled; + + /// No description provided for @chatInfoRowOnlyAdminCall. + /// + /// In en, this message translates to: + /// **'Admins can call'** + String get chatInfoRowOnlyAdminCall; + + /// No description provided for @chatInfoRowAllCanPin. + /// + /// In en, this message translates to: + /// **'Anyone can pin'** + String get chatInfoRowAllCanPin; + + /// No description provided for @chatInfoRowMembersSeeLink. + /// + /// In en, this message translates to: + /// **'Members see the link'** + String get chatInfoRowMembersSeeLink; + + /// No description provided for @chatInfoRowConfirmBeforeSend. + /// + /// In en, this message translates to: + /// **'Confirm before sending'** + String get chatInfoRowConfirmBeforeSend; + + /// No description provided for @chatInfoRowOnlyOwnerIconTitle. + /// + /// In en, this message translates to: + /// **'Owner edits title and icon'** + String get chatInfoRowOnlyOwnerIconTitle; + + /// No description provided for @chatInfoRowPromotedDisabled. + /// + /// In en, this message translates to: + /// **'Promoted content off'** + String get chatInfoRowPromotedDisabled; + + /// No description provided for @chatInfoRowUserId. + /// + /// In en, this message translates to: + /// **'User ID'** + String get chatInfoRowUserId; + /// No description provided for @chatInfoRowId. /// /// In en, this message translates to: diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 0f48a1c..cfc8ac6 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -348,12 +348,24 @@ class AppLocalizationsEn extends AppLocalizations { @override String get infoAccountSection => 'Account'; + @override + String get infoPacketSection => 'Login packet'; + + @override + String get infoChatsSection => 'Chats in login packet'; + + @override + String get infoChatSettingsSection => 'Per-chat settings'; + @override String get infoServerSection => 'Server'; @override String get infoUserSection => 'User'; + @override + String get infoExperimentsSection => 'Experiments'; + @override String get infoYMapSection => 'Y-Map'; @@ -378,9 +390,78 @@ class AppLocalizationsEn extends AppLocalizations { @override String get infoId => 'id'; + @override + String get infoPhone => 'phone'; + + @override + String get infoPhotoId => 'photoId'; + + @override + String get infoAccountStatus => 'accountStatus'; + + @override + String get infoContactOptions => 'contact options'; + + @override + String get infoProfileOptions => 'profile options'; + + @override + String get infoNames => 'names'; + + @override + String get infoBaseUrl => 'baseUrl'; + + @override + String get infoBaseRawUrl => 'baseRawUrl'; + @override String get infoChatMarker => 'chatMarker'; + @override + String get infoServerTime => 'server time'; + + @override + String get infoUpdates => 'updates'; + + @override + String get infoMessagesCount => 'messages in packet'; + + @override + String get infoContactsCount => 'contacts in packet'; + + @override + String get infoPresenceCount => 'presence records'; + + @override + String get infoConfigHash => 'config hash'; + + @override + String get infoChatsCount => 'chats loaded'; + + @override + String get infoChatsActive => 'active'; + + @override + String get infoChatsHidden => 'hidden'; + + @override + String get infoChatsDialogs => 'dialogs'; + + @override + String get infoChatsGroups => 'groups'; + + @override + String get infoChatsChannels => 'channels'; + + @override + String get infoChatsUnread => 'unread chats'; + + @override + String get infoChatsNewMessages => 'new messages'; + + @override + String get infoChatsMessages => 'messages in loaded chats'; + @override String get infoAccountRemovalEnabled => 'account-removal-enabled'; @@ -776,6 +857,9 @@ class AppLocalizationsEn extends AppLocalizations { String get appearanceVisualStyleSubtitle => 'Material You or dimensional Glossy capsules'; + @override + String get appearanceStyleAuto => 'Match theme'; + @override String get appearanceVisualStyleMaterialYou => 'Material You'; @@ -1606,6 +1690,39 @@ class AppLocalizationsEn extends AppLocalizations { @override String get chatInfoShowMoreExtra => 'Details'; + @override + String get chatSendConfirmMessage => 'Send this message to the chat?'; + + @override + String get chatSendConfirmAction => 'Send'; + + @override + String get chatInfoRowDisableForward => 'Forwarding disabled'; + + @override + String get chatInfoRowCopyDisabled => 'Copying disabled'; + + @override + String get chatInfoRowOnlyAdminCall => 'Admins can call'; + + @override + String get chatInfoRowAllCanPin => 'Anyone can pin'; + + @override + String get chatInfoRowMembersSeeLink => 'Members see the link'; + + @override + String get chatInfoRowConfirmBeforeSend => 'Confirm before sending'; + + @override + String get chatInfoRowOnlyOwnerIconTitle => 'Owner edits title and icon'; + + @override + String get chatInfoRowPromotedDisabled => 'Promoted content off'; + + @override + String get chatInfoRowUserId => 'User ID'; + @override String get chatInfoRowId => 'Chat ID'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 5984028..56889e0 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -350,12 +350,24 @@ class AppLocalizationsRu extends AppLocalizations { @override String get infoAccountSection => 'Аккаунт'; + @override + String get infoPacketSection => 'Пакет входа'; + + @override + String get infoChatsSection => 'Чаты в пакете входа'; + + @override + String get infoChatSettingsSection => 'Настройки отдельных чатов'; + @override String get infoServerSection => 'Сервер'; @override String get infoUserSection => 'Пользователь'; + @override + String get infoExperimentsSection => 'Эксперименты'; + @override String get infoYMapSection => 'Y-Map'; @@ -380,9 +392,78 @@ class AppLocalizationsRu extends AppLocalizations { @override String get infoId => 'id аккаунта:'; + @override + String get infoPhone => 'Телефон:'; + + @override + String get infoPhotoId => 'id аватарки:'; + + @override + String get infoAccountStatus => 'Статус аккаунта:'; + + @override + String get infoContactOptions => 'Опции контакта:'; + + @override + String get infoProfileOptions => 'Опции профиля:'; + + @override + String get infoNames => 'Имена:'; + + @override + String get infoBaseUrl => 'Ссылка на аватарку:'; + + @override + String get infoBaseRawUrl => 'Исходная аватарка:'; + @override String get infoChatMarker => 'chatMarker'; + @override + String get infoServerTime => 'Время сервера:'; + + @override + String get infoUpdates => 'Количество обновлений:'; + + @override + String get infoMessagesCount => 'Сообщений в пакете:'; + + @override + String get infoContactsCount => 'Контактов в пакете:'; + + @override + String get infoPresenceCount => 'Статусов присутствия:'; + + @override + String get infoConfigHash => 'Хеш конфигурации:'; + + @override + String get infoChatsCount => 'Загружено чатов:'; + + @override + String get infoChatsActive => 'Активных:'; + + @override + String get infoChatsHidden => 'Скрытых:'; + + @override + String get infoChatsDialogs => 'Диалогов:'; + + @override + String get infoChatsGroups => 'Групп:'; + + @override + String get infoChatsChannels => 'Каналов:'; + + @override + String get infoChatsUnread => 'Непрочитанных чатов:'; + + @override + String get infoChatsNewMessages => 'Новых сообщений:'; + + @override + String get infoChatsMessages => 'Сообщений в загруженных чатах:'; + @override String get infoAccountRemovalEnabled => 'Мгновенное удаление аккаунта:'; @@ -781,6 +862,9 @@ class AppLocalizationsRu extends AppLocalizations { String get appearanceVisualStyleSubtitle => 'Material You или объёмные Glossy-капсулы'; + @override + String get appearanceStyleAuto => 'Как в теме'; + @override String get appearanceVisualStyleMaterialYou => 'Material You'; @@ -1615,6 +1699,39 @@ class AppLocalizationsRu extends AppLocalizations { @override String get chatInfoShowMoreExtra => 'Подробнее'; + @override + String get chatSendConfirmMessage => 'Отправить это сообщение в чат?'; + + @override + String get chatSendConfirmAction => 'Отправить'; + + @override + String get chatInfoRowDisableForward => 'Пересылка запрещена'; + + @override + String get chatInfoRowCopyDisabled => 'Копирование запрещено'; + + @override + String get chatInfoRowOnlyAdminCall => 'Звонить могут админы'; + + @override + String get chatInfoRowAllCanPin => 'Все могут закреплять'; + + @override + String get chatInfoRowMembersSeeLink => 'Ссылка видна участникам'; + + @override + String get chatInfoRowConfirmBeforeSend => 'Подтверждать отправку'; + + @override + String get chatInfoRowOnlyOwnerIconTitle => 'Название меняет владелец'; + + @override + String get chatInfoRowPromotedDisabled => 'Реклама отключена'; + + @override + String get chatInfoRowUserId => 'ID пользователя'; + @override String get chatInfoRowId => 'ID чата'; diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index b88ff1c..f656361 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -123,8 +123,12 @@ "profileMenuSpoof": "Подмена данных", "infoTitle": "Info", "infoAccountSection": "Аккаунт", + "infoPacketSection": "Пакет входа", + "infoChatsSection": "Чаты в пакете входа", + "infoChatSettingsSection": "Настройки отдельных чатов", "infoServerSection": "Сервер", "infoUserSection": "Пользователь", + "infoExperimentsSection": "Эксперименты", "infoYMapSection": "Y-Map", "infoFileUploadTypes": "запрещённые типы файлов", "infoWhiteListLinks": "безопасные ссылки", @@ -133,7 +137,30 @@ "infoVideoChatHistory": "videoChatHistory", "infoUpdateTime": "Последнее обновление аватарки:", "infoId": "id аккаунта:", + "infoPhone": "Телефон:", + "infoPhotoId": "id аватарки:", + "infoAccountStatus": "Статус аккаунта:", + "infoContactOptions": "Опции контакта:", + "infoProfileOptions": "Опции профиля:", + "infoNames": "Имена:", + "infoBaseUrl": "Ссылка на аватарку:", + "infoBaseRawUrl": "Исходная аватарка:", "infoChatMarker": "chatMarker", + "infoServerTime": "Время сервера:", + "infoUpdates": "Количество обновлений:", + "infoMessagesCount": "Сообщений в пакете:", + "infoContactsCount": "Контактов в пакете:", + "infoPresenceCount": "Статусов присутствия:", + "infoConfigHash": "Хеш конфигурации:", + "infoChatsCount": "Загружено чатов:", + "infoChatsActive": "Активных:", + "infoChatsHidden": "Скрытых:", + "infoChatsDialogs": "Диалогов:", + "infoChatsGroups": "Групп:", + "infoChatsChannels": "Каналов:", + "infoChatsUnread": "Непрочитанных чатов:", + "infoChatsNewMessages": "Новых сообщений:", + "infoChatsMessages": "Сообщений в загруженных чатах:", "infoAccountRemovalEnabled": "Мгновенное удаление аккаунта:", "infoImageSize": "image-size", "infoGce": "gce", @@ -258,6 +285,7 @@ "appearanceTitle": "Внешний вид", "appearanceVisualStyleTitle": "Визуал", "appearanceVisualStyleSubtitle": "Material You или объёмные Glossy-капсулы", + "appearanceStyleAuto": "Как в теме", "appearanceVisualStyleMaterialYou": "Material You", "appearanceVisualStyleGlossy": "Glossy", "appearanceVisualStyleLiquidGlass": "Liquid Glass", @@ -525,6 +553,17 @@ "chatInfoNoData": "Нет данных", "chatInfoHideExtra": "Скрыть", "chatInfoShowMoreExtra": "Подробнее", + "chatSendConfirmMessage": "Отправить это сообщение в чат?", + "chatSendConfirmAction": "Отправить", + "chatInfoRowDisableForward": "Пересылка запрещена", + "chatInfoRowCopyDisabled": "Копирование запрещено", + "chatInfoRowOnlyAdminCall": "Звонить могут админы", + "chatInfoRowAllCanPin": "Все могут закреплять", + "chatInfoRowMembersSeeLink": "Ссылка видна участникам", + "chatInfoRowConfirmBeforeSend": "Подтверждать отправку", + "chatInfoRowOnlyOwnerIconTitle": "Название меняет владелец", + "chatInfoRowPromotedDisabled": "Реклама отключена", + "chatInfoRowUserId": "ID пользователя", "chatInfoRowId": "ID чата", "chatInfoRowCreated": "Создан", "chatInfoRowModified": "Изменён", diff --git a/lib/main.dart b/lib/main.dart index eb1c0ff..8b7c858 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -871,12 +871,14 @@ class KometAppState extends State _themeCacheFontId = _fontId; _themeCacheLight = light; _themeCacheDark = dark; + final displayFont = AppDisplayFont(AppFonts.displayFamily(_fontId)); _lightTheme = withM3ETheme( ThemeData( useMaterial3: true, colorScheme: light, pageTransitionsTheme: _appPageTransitions, progressIndicatorTheme: _expressiveProgressTheme, + extensions: [displayFont], textTheme: AppFonts.textTheme( _fontId, ThemeData(brightness: Brightness.light).textTheme, @@ -889,6 +891,7 @@ class KometAppState extends State colorScheme: dark, pageTransitionsTheme: _appPageTransitions, progressIndicatorTheme: _expressiveProgressTheme, + extensions: [displayFont], textTheme: AppFonts.textTheme( _fontId, ThemeData(brightness: Brightness.dark).textTheme, @@ -1109,9 +1112,7 @@ class _StartupScreenState extends State<_StartupScreen> { final cs = Theme.of(context).colorScheme; return Scaffold( backgroundColor: cs.surface, - body: Center( - child: SmallSpinner(size: 36, color: cs.primary), - ), + body: Center(child: SmallSpinner(size: 36, color: cs.primary)), ); } } diff --git a/lib/models/chat_info.dart b/lib/models/chat_info.dart index d418f20..935f89a 100644 --- a/lib/models/chat_info.dart +++ b/lib/models/chat_info.dart @@ -23,6 +23,14 @@ class ChatInfo { bool isAdmin(int id) => adminIds.contains(id); bool isOwner(int id) => owner != null && id == owner; + bool option(String name) { + final opts = raw['options']; + return opts is Map && opts[name] == true; + } + + bool canSeeInviteLink(int id) => + isAdmin(id) || isOwner(id) || option('MEMBERS_CAN_SEE_PRIVATE_LINK'); + String? adminAlias(int id) { final source = raw['adminParticipants']; if (source is! Map) return null; diff --git a/lib/models/login_info.dart b/lib/models/login_info.dart new file mode 100644 index 0000000..0db80f5 --- /dev/null +++ b/lib/models/login_info.dart @@ -0,0 +1,141 @@ +class LoginInfo { + const LoginInfo._(); + + static Map fromPayload(Map payload) { + final profile = _asMap(payload['profile']); + final contact = _asMap(profile?['contact']); + final chats = _asList(payload['chats']); + final config = _asMap(payload['config']); + final server = _asMap(config?['server']); + final user = _asMap(config?['user']); + final experiments = _asMap(config?['experiments']); + final chatSettings = _asMap(config?['chats']); + + return { + 'registrationTime': contact?['registrationTime'], + 'country': contact?['country'], + 'videoChatHistory': payload['videoChatHistory'], + 'updateTime': contact?['updateTime'], + 'id': contact?['id'], + 'phone': contact?['phone'], + 'photoId': contact?['photoId'], + 'accountStatus': contact?['accountStatus'], + 'contactOptions': _copyJsonValue(contact?['options']), + 'profileOptions': _copyJsonValue(profile?['profileOptions']), + 'names': _copyJsonValue(contact?['names']), + 'baseUrl': contact?['baseUrl'], + 'baseRawUrl': contact?['baseRawUrl'], + 'chatMarker': payload['chatMarker'] ?? _latestChatEventTime(chats), + 'time': payload['time'], + 'updates': payload['updates'], + 'messagesCount': _collectionLength(payload['messages']), + 'contactsCount': _collectionLength(payload['contacts']), + 'presenceCount': _collectionLength(payload['presence']), + 'configHash': config?['hash'], + 'chats': _buildChatsSummary(chats), + 'server': _copyMap(server), + 'user': _copyMap(user), + 'experiments': _copyMap(experiments), + 'chatSettings': _copyMap(chatSettings), + }; + } + + static Map _buildChatsSummary(List chats) { + var active = 0; + var hidden = 0; + var dialogs = 0; + var groups = 0; + var channels = 0; + var unread = 0; + var newMessages = 0; + var messages = 0; + + for (final rawChat in chats) { + final chat = _asMap(rawChat); + if (chat == null) continue; + switch (chat['status']) { + case 'ACTIVE': + active++; + case 'HIDDEN': + hidden++; + } + switch (chat['type']) { + case 'DIALOG': + dialogs++; + case 'CHAT': + groups++; + case 'CHANNEL': + channels++; + } + final chatNewMessages = _asInt(chat['newMessages']) ?? 0; + if (chatNewMessages > 0) unread++; + newMessages += chatNewMessages; + messages += _asInt(chat['messagesCount']) ?? 0; + } + + return { + 'count': chats.length, + 'active': active, + 'hidden': hidden, + 'dialogs': dialogs, + 'groups': groups, + 'channels': channels, + 'unread': unread, + 'newMessages': newMessages, + 'messages': messages, + }; + } + + static int? _latestChatEventTime(List chats) { + int? latest; + for (final rawChat in chats) { + final chat = _asMap(rawChat); + final value = _asInt(chat?['lastEventTime']); + if (value != null && (latest == null || value > latest)) { + latest = value; + } + } + return latest; + } + + static Map? _asMap(dynamic value) { + return value is Map ? value : null; + } + + static List _asList(dynamic value) { + return value is List ? value : const []; + } + + static int? _asInt(dynamic value) { + return switch (value) { + int number => number, + num number => number.toInt(), + String text => int.tryParse(text), + _ => null, + }; + } + + static int _collectionLength(dynamic value) { + return switch (value) { + Map items => items.length, + List items => items.length, + _ => 0, + }; + } + + static Map? _copyMap(Map? value) { + if (value == null) return null; + return value.map( + (key, item) => MapEntry(key.toString(), _copyJsonValue(item)), + ); + } + + static dynamic _copyJsonValue(dynamic value) { + if (value is Map) return _copyMap(value); + if (value is List) return value.map(_copyJsonValue).toList(); + if (value == null || value is String || value is num || value is bool) { + return value; + } + return value.toString(); + } +} diff --git a/test/login_info_test.dart b/test/login_info_test.dart new file mode 100644 index 0000000..49ee826 --- /dev/null +++ b/test/login_info_test.dart @@ -0,0 +1,141 @@ +import 'dart:convert'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:komet/models/login_info.dart'; + +void main() { + group('LoginInfo', () { + test('extracts profile, packet, chat, and config data', () { + final info = LoginInfo.fromPayload({ + 'profile': { + 'contact': { + 'id': 100200300, + 'updateTime': 1700000002000, + 'registrationTime': 1700000001000, + 'baseUrl': 'https://example.invalid/avatar', + 'baseRawUrl': 'https://example.invalid/avatar/raw', + 'photoId': 400500600, + 'phone': 70000000000, + 'names': [ + { + 'name': 'Тест Пользователь', + 'firstName': 'Тест', + 'lastName': 'Пользователь', + 'type': 'SYNTHETIC', + }, + ], + 'options': ['SYNTHETIC'], + 'accountStatus': 2, + 'country': 'ZZ', + }, + 'profileOptions': [7], + }, + 'chats': [ + { + 'id': -1001, + 'type': 'CHAT', + 'status': 'ACTIVE', + 'lastEventTime': 1700000008000, + 'newMessages': 3, + 'messagesCount': 12, + }, + { + 'id': -1002, + 'type': 'CHANNEL', + 'status': 'HIDDEN', + 'lastEventTime': 1700000007000, + }, + { + 'id': 1003, + 'type': 'DIALOG', + 'status': 'ACTIVE', + 'lastEventTime': 1700000006000, + }, + ], + 'chatMarker': 1700000009000, + 'contacts': [ + {'id': 2001}, + {'id': 2002}, + ], + 'presence': {'synthetic-peer': 1700000003000}, + 'messages': {'synthetic-message': {}}, + 'config': { + 'hash': 'synthetic-config-hash', + 'server': { + 'known-flag': true, + 'nested': {'limit': 4}, + }, + 'user': {'SYNTHETIC_SETTING': 'ON'}, + 'chats': { + 'synthetic-chat': {'sound': false}, + }, + 'experiments': { + 'synthetic-experiment': {'enabled': true}, + }, + }, + 'videoChatHistory': true, + 'time': 1700000010000, + 'updates': 4, + }); + + expect(info['id'], 100200300); + expect(info['phone'], 70000000000); + expect(info['photoId'], 400500600); + expect(info['accountStatus'], 2); + expect(info['country'], 'ZZ'); + expect(info['profileOptions'], [7]); + expect(info['contactOptions'], ['SYNTHETIC']); + expect(info['chatMarker'], 1700000009000); + expect(info['contactsCount'], 2); + expect(info['presenceCount'], 1); + expect(info['messagesCount'], 1); + expect(info['configHash'], 'synthetic-config-hash'); + expect(info['chats'], { + 'count': 3, + 'active': 2, + 'hidden': 1, + 'dialogs': 1, + 'groups': 1, + 'channels': 1, + 'unread': 1, + 'newMessages': 3, + 'messages': 12, + }); + expect(info['server'], { + 'known-flag': true, + 'nested': {'limit': 4}, + }); + expect(info['user'], {'SYNTHETIC_SETTING': 'ON'}); + expect(info['chatSettings'], { + 'synthetic-chat': {'sound': false}, + }); + expect(info['experiments'], { + 'synthetic-experiment': {'enabled': true}, + }); + expect(() => jsonEncode(info), returnsNormally); + }); + + test('falls back to latest chat event when marker is absent', () { + final info = LoginInfo.fromPayload({ + 'chats': [ + {'lastEventTime': 1700000004000}, + {'lastEventTime': 1700000006000}, + {'lastEventTime': 1700000005000}, + ], + }); + + expect(info['chatMarker'], 1700000006000); + expect(info['chats'], { + 'count': 3, + 'active': 0, + 'hidden': 0, + 'dialogs': 0, + 'groups': 0, + 'channels': 0, + 'unread': 0, + 'newMessages': 0, + 'messages': 0, + }); + }); + }); +} diff --git a/test/max_link_test.dart b/test/max_link_test.dart index e12c095..ee8d5cd 100644 --- a/test/max_link_test.dart +++ b/test/max_link_test.dart @@ -101,18 +101,18 @@ void main() { test('message links carry the message id', () { final byName = - MaxLink.parse('max.ru/somechannel/117008613873053494') + MaxLink.parse('max.ru/somechannel/900000000000000001') as MaxContentLink; expect(byName.kind, MaxContentKind.public); - expect(byName.messageId, 117008613873053494); + expect(byName.messageId, 900000000000000001); expect(byName.baseUrl, 'https://max.ru/somechannel'); final byId = - MaxLink.parse('max.ru/c/1673760/117008613873053494') + MaxLink.parse('max.ru/c/424242/900000000000000001') as MaxContentLink; expect(byId.kind, MaxContentKind.content); - expect(byId.messageId, 117008613873053494); - expect(byId.baseUrl, 'https://max.ru/c/1673760'); + expect(byId.messageId, 900000000000000001); + expect(byId.baseUrl, 'https://max.ru/c/424242'); }); test('invite, call and sticker links keep their own types', () { @@ -129,12 +129,12 @@ void main() { test('uid and cid open a contact and a chat', () { expect( - (MaxLink.parse('max://max.ru/?uid=105587131') as MaxContactIdLink) + (MaxLink.parse('max://max.ru/?uid=434343') as MaxContactIdLink) .userId, - 105587131, + 434343, ); - final chat = MaxLink.parse('max://max.ru/?cid=1673760') as MaxChatIdLink; - expect(chat.chatId, 1673760); + final chat = MaxLink.parse('max://max.ru/?cid=424242') as MaxChatIdLink; + expect(chat.chatId, 424242); expect(chat.messageId, isNull); }); });