feat: аватар после входа, кнопка выхода в настройках

This commit is contained in:
klockky
2026-05-26 17:13:26 +03:00
parent 221487ca28
commit a6aab0a7e3
5 changed files with 127 additions and 64 deletions
+8
View File
@@ -24,6 +24,12 @@ class ContactCache {
static String? getAvatar(int id) => _avatarCache[id];
static Set<String>? 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 {
@@ -167,7 +167,14 @@ class _CodeConfirmationScreenState extends State<CodeConfirmationScreen>
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<CodeConfirmationScreen>
context,
PageRouteBuilder(
transitionDuration: const Duration(milliseconds: 240),
pageBuilder: (_, __, ___) => const LoginSuccessScreen(),
pageBuilder: (_, __, ___) => LoginSuccessScreen(avatar: avatar),
transitionsBuilder: (_, animation, __, child) => FadeTransition(
opacity: animation,
child: child,
@@ -40,7 +40,14 @@ class _Password2FAScreenState extends State<Password2FAScreen> {
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<Password2FAScreen> {
context,
PageRouteBuilder(
transitionDuration: const Duration(milliseconds: 240),
pageBuilder: (_, __, ___) => const LoginSuccessScreen(),
pageBuilder: (_, __, ___) => LoginSuccessScreen(avatar: avatar),
transitionsBuilder: (_, animation, __, child) => FadeTransition(
opacity: animation,
child: child,
+48 -47
View File
@@ -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<SettingsTab> {
static const bool _showLogoutButton = false;
ProfileData? _profile;
bool _isPhoneVisible = false;
String? _appVersionLabel;
@@ -164,7 +163,7 @@ class _SettingsTabState extends State<SettingsTab> {
),
const SizedBox(height: 8),
Text(
'Сессия будет сброшена. Локальный кеш сохранится — войдёшь снова в этот же аккаунт.',
'Данные аккаунта будут удалены с этого устройства.',
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13),
),
const SizedBox(height: 20),
@@ -202,8 +201,11 @@ class _SettingsTabState extends State<SettingsTab> {
} 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,
});
+53 -13
View File
@@ -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<ImageProvider?> 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<LoginSuccessScreen> createState() => _LoginSuccessScreenState();
@@ -16,6 +33,13 @@ class _LoginSuccessScreenState extends State<LoginSuccessScreen>
with SingleTickerProviderStateMixin {
static const Duration _duration = Duration(milliseconds: 1900);
static const List<String> _greetings = [
'С большой силой приходит большая ответственность',
'All in your hands',
'Иногда забавные вещи могут быть уголовно наказуемы',
];
late final String _greeting;
late final AnimationController _controller;
late final Animation<double> _circleScale;
late final Animation<double> _ringSweep;
@@ -34,6 +58,7 @@ class _LoginSuccessScreenState extends State<LoginSuccessScreen>
@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<LoginSuccessScreen>
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<LoginSuccessScreen>
),
],
),
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<LoginSuccessScreen>
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,
),
),
),
),