diff --git a/lib/backend/modules/messages.dart b/lib/backend/modules/messages.dart index e64996e..2255a74 100644 --- a/lib/backend/modules/messages.dart +++ b/lib/backend/modules/messages.dart @@ -24,6 +24,12 @@ class ContactCache { static String? getAvatar(int id) => _avatarCache[id]; static Set? getOptions(int id) => _optionsCache[id]; static bool isOfficial(int id) => _optionsCache[id]?.contains('OFFICIAL') ?? false; + + static void clear() { + _nameCache.clear(); + _avatarCache.clear(); + _optionsCache.clear(); + } } class TranscriptionResult { @@ -52,6 +58,8 @@ class TranscriptionCache { static TranscriptionResult? get(String messageId) => _cache[messageId]; static bool has(String messageId) => _cache.containsKey(messageId); + + static void clear() => _cache.clear(); } class FileHistoryEntry { diff --git a/lib/frontend/screens/auth/code_confirmation_screen.dart b/lib/frontend/screens/auth/code_confirmation_screen.dart index 1f776aa..5a52c01 100644 --- a/lib/frontend/screens/auth/code_confirmation_screen.dart +++ b/lib/frontend/screens/auth/code_confirmation_screen.dart @@ -167,7 +167,14 @@ class _CodeConfirmationScreenState extends State return; } - await accountModule.login(); + final loginResult = await accountModule.login(); + + if (!mounted) return; + + final avatar = await precacheLoginAvatar( + context, + loginResult.profile.baseUrl, + ); if (!mounted) return; @@ -175,7 +182,7 @@ class _CodeConfirmationScreenState extends State context, PageRouteBuilder( transitionDuration: const Duration(milliseconds: 240), - pageBuilder: (_, __, ___) => const LoginSuccessScreen(), + pageBuilder: (_, __, ___) => LoginSuccessScreen(avatar: avatar), transitionsBuilder: (_, animation, __, child) => FadeTransition( opacity: animation, child: child, diff --git a/lib/frontend/screens/auth/password_2fa_screen.dart b/lib/frontend/screens/auth/password_2fa_screen.dart index fae5643..a6716b0 100644 --- a/lib/frontend/screens/auth/password_2fa_screen.dart +++ b/lib/frontend/screens/auth/password_2fa_screen.dart @@ -40,7 +40,14 @@ class _Password2FAScreenState extends State { if (!mounted) return; - await accountModule.login(token: result.loginToken); + final loginResult = await accountModule.login(token: result.loginToken); + + if (!mounted) return; + + final avatar = await precacheLoginAvatar( + context, + loginResult.profile.baseUrl, + ); if (!mounted) return; @@ -48,7 +55,7 @@ class _Password2FAScreenState extends State { context, PageRouteBuilder( transitionDuration: const Duration(milliseconds: 240), - pageBuilder: (_, __, ___) => const LoginSuccessScreen(), + pageBuilder: (_, __, ___) => LoginSuccessScreen(avatar: avatar), transitionsBuilder: (_, animation, __, child) => FadeTransition( opacity: animation, child: child, diff --git a/lib/frontend/screens/profile/settings_tab.dart b/lib/frontend/screens/profile/settings_tab.dart index 5f9c7d5..dd40169 100644 --- a/lib/frontend/screens/profile/settings_tab.dart +++ b/lib/frontend/screens/profile/settings_tab.dart @@ -4,6 +4,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:package_info_plus/package_info_plus.dart'; +import '../../../backend/modules/messages.dart'; import '../../../core/storage/app_database.dart'; import '../../../core/storage/token_storage.dart'; import '../../../core/utils/haptics.dart'; @@ -30,8 +31,6 @@ class SettingsTab extends StatefulWidget { } class _SettingsTabState extends State { - static const bool _showLogoutButton = false; - ProfileData? _profile; bool _isPhoneVisible = false; String? _appVersionLabel; @@ -164,7 +163,7 @@ class _SettingsTabState extends State { ), const SizedBox(height: 8), Text( - 'Сессия будет сброшена. Локальный кеш сохранится — войдёшь снова в этот же аккаунт.', + 'Данные аккаунта будут удалены с этого устройства.', style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13), ), const SizedBox(height: 20), @@ -202,8 +201,11 @@ class _SettingsTabState extends State { } catch (_) {} final accountId = await TokenStorage.getActiveAccountId(); if (accountId != null) { - await TokenStorage.deleteToken(accountId); + await TokenStorage.deleteAccount(accountId); + await AppDatabase.deleteAccount(accountId); } + ContactCache.clear(); + TranscriptionCache.clear(); try { await api.connect(); } catch (_) {} @@ -442,6 +444,23 @@ child: _buildSection( ), ), ), + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 0), + child: _buildSection( + context, + cs, + items: [ + _SettingsItem( + icon: Symbols.logout, + label: 'Выйти из аккаунта', + tintColor: cs.error, + onTap: _confirmLogout, + ), + ], + ), + ), + ), if (_appVersionLabel != null) SliverToBoxAdapter( child: Padding( @@ -552,51 +571,31 @@ child: _buildSection( ), ), const SizedBox(height: 4), - Stack( + Row( + mainAxisAlignment: MainAxisAlignment.center, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - GestureDetector( - onTap: () => setState(() => _isPhoneVisible = !_isPhoneVisible), - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: _PhoneSpoiler( - text: phone, - isVisible: _isPhoneVisible, - style: TextStyle( - color: cs.onSurfaceVariant, - fontSize: 14, - fontWeight: FontWeight.w400, - letterSpacing: 0.5, - ), - ), - ), - ), - const SizedBox(width: 4), - Icon( - _isPhoneVisible ? Symbols.visibility : Symbols.visibility_off, - size: 14, - color: cs.onSurfaceVariant.withValues(alpha: 0.6), - ), - ], - ), - if (_showLogoutButton) - Positioned.fill( - child: Align( - alignment: Alignment.centerRight, - child: IconButton( - tooltip: 'Выйти', - icon: Icon( - Symbols.logout, - color: cs.error, - size: 22, - weight: 400, - ), - onPressed: _confirmLogout, + GestureDetector( + onTap: () => setState(() => _isPhoneVisible = !_isPhoneVisible), + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: _PhoneSpoiler( + text: phone, + isVisible: _isPhoneVisible, + style: TextStyle( + color: cs.onSurfaceVariant, + fontSize: 14, + fontWeight: FontWeight.w400, + letterSpacing: 0.5, ), ), ), + ), + const SizedBox(width: 4), + Icon( + _isPhoneVisible ? Symbols.visibility : Symbols.visibility_off, + size: 14, + color: cs.onSurfaceVariant.withValues(alpha: 0.6), + ), ], ), ], @@ -662,7 +661,7 @@ child: _buildSection( children: [ Icon( item.icon, - color: cs.onSurfaceVariant, + color: item.tintColor ?? cs.onSurfaceVariant, size: 22, weight: 400, ), @@ -671,7 +670,7 @@ child: _buildSection( child: Text( item.label, style: TextStyle( - color: cs.onSurface, + color: item.tintColor ?? cs.onSurface, fontSize: 16, fontWeight: FontWeight.w500, ), @@ -712,6 +711,7 @@ class _SettingsItem { final IconData icon; final String label; final VoidCallback? onTap; + final Color? tintColor; /// When [onToggle] is set the row renders a trailing switch instead of a /// chevron, and [toggleValue] reflects its current state. @@ -722,6 +722,7 @@ class _SettingsItem { required this.icon, required this.label, this.onTap, + this.tintColor, this.toggleValue, this.onToggle, }); diff --git a/lib/frontend/widgets/login_success_screen.dart b/lib/frontend/widgets/login_success_screen.dart index 74aa9e9..fa35339 100644 --- a/lib/frontend/widgets/login_success_screen.dart +++ b/lib/frontend/widgets/login_success_screen.dart @@ -1,12 +1,29 @@ import 'dart:math' as math; +import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import '../../core/utils/haptics.dart'; import '../screens/chats/chat_list_screen.dart'; +Future precacheLoginAvatar( + BuildContext context, + String? url, +) async { + if (url == null || url.isEmpty) return null; + final provider = CachedNetworkImageProvider(url); + try { + await precacheImage(provider, context); + return provider; + } catch (_) { + return null; + } +} + class LoginSuccessScreen extends StatefulWidget { - const LoginSuccessScreen({super.key}); + final ImageProvider? avatar; + + const LoginSuccessScreen({super.key, this.avatar}); @override State createState() => _LoginSuccessScreenState(); @@ -16,6 +33,13 @@ class _LoginSuccessScreenState extends State with SingleTickerProviderStateMixin { static const Duration _duration = Duration(milliseconds: 1900); + static const List _greetings = [ + 'С большой силой приходит большая ответственность', + 'All in your hands', + 'Иногда забавные вещи могут быть уголовно наказуемы', + ]; + + late final String _greeting; late final AnimationController _controller; late final Animation _circleScale; late final Animation _ringSweep; @@ -34,6 +58,7 @@ class _LoginSuccessScreenState extends State @override void initState() { super.initState(); + _greeting = _greetings[math.Random().nextInt(_greetings.length)]; _controller = AnimationController(vsync: this, duration: _duration); _circleScale = CurvedAnimation( @@ -227,6 +252,7 @@ class _LoginSuccessScreenState extends State Widget _buildCircle(ColorScheme cs) { final scale = _circleScale.value.clamp(0.0, 1.0); + final avatar = widget.avatar; return Transform.scale( scale: scale, child: Container( @@ -243,12 +269,21 @@ class _LoginSuccessScreenState extends State ), ], ), - child: CustomPaint( - painter: _CheckPainter( - progress: _checkProgress.value, - color: cs.onPrimary, - ), - ), + child: avatar != null + ? ClipOval( + child: Image( + image: avatar, + fit: BoxFit.cover, + width: 120, + height: 120, + ), + ) + : CustomPaint( + painter: _CheckPainter( + progress: _checkProgress.value, + color: cs.onPrimary, + ), + ), ), ); } @@ -276,12 +311,17 @@ class _LoginSuccessScreenState extends State opacity: _subtitleOpacity.value, child: Transform.translate( offset: Offset(0, 14 * (1 - _subtitleOffset.value)), - child: Text( - 'Добро пожаловать в Komet', - style: TextStyle( - color: cs.onSurfaceVariant, - fontSize: 15, - fontWeight: FontWeight.w500, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 40), + child: Text( + _greeting, + textAlign: TextAlign.center, + style: TextStyle( + color: cs.onSurfaceVariant, + fontSize: 15, + fontWeight: FontWeight.w500, + height: 1.3, + ), ), ), ),