feat: аватар после входа, кнопка выхода в настройках
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user