набурмалдил

This commit is contained in:
Jganenok
2026-05-29 13:52:13 +07:00
parent b3a7fd081c
commit 0a23b33cfd
9 changed files with 369 additions and 108 deletions
+14
View File
@@ -937,6 +937,20 @@ class AccountModule {
_checkPacketError(packet, 'authorizeWebQrLogin'); _checkPacketError(packet, 'authorizeWebQrLogin');
} }
Future<void> beginAddAccount() async {
try {
await _api.disconnect();
} catch (_) {}
await TokenStorage.clearActiveAccount();
ContactCache.clear();
TranscriptionCache.clear();
ChatsModule.resetForAccountSwitch();
logger.i('Добавление аккаунта: сессия сброшена, активный аккаунт очищен');
}
Future<ProfileData> switchAccount(int accountId) async { Future<ProfileData> switchAccount(int accountId) async {
final profile = await AppDatabase.loadProfile(accountId); final profile = await AppDatabase.loadProfile(accountId);
if (profile == null) { if (profile == null) {
+5
View File
@@ -24,6 +24,11 @@ class TokenStorage {
await prefs.setString(_activeAccountKey, accountId.toString()); await prefs.setString(_activeAccountKey, accountId.toString());
} }
static Future<void> clearActiveAccount() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_activeAccountKey);
}
static Future<int?> getActiveAccountId() async { static Future<int?> getActiveAccountId() async {
final prefs = await SharedPreferences.getInstance(); final prefs = await SharedPreferences.getInstance();
final val = prefs.getString(_activeAccountKey); final val = prefs.getString(_activeAccountKey);
+27 -3
View File
@@ -14,10 +14,14 @@ import 'proxy_settings_sheet.dart';
import 'server_settings_sheet.dart'; import 'server_settings_sheet.dart';
import '../profile/spoof_screen.dart'; import '../profile/spoof_screen.dart';
import '../../widgets/custom_notification.dart'; import '../../widgets/custom_notification.dart';
import '../../widgets/adaptive_shell.dart';
import '../../../backend/api.dart';
import '../../../main.dart'; import '../../../main.dart';
class LoginScreen extends StatefulWidget { class LoginScreen extends StatefulWidget {
const LoginScreen({super.key}); final int? returnToAccountId;
const LoginScreen({super.key, this.returnToAccountId});
@override @override
State<LoginScreen> createState() => _LoginScreenState(); State<LoginScreen> createState() => _LoginScreenState();
@@ -34,11 +38,30 @@ class _LoginScreenState extends State<LoginScreen> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
if (api.state == SessionState.disconnected) {
unawaited(api.connect());
}
_selectedCountry = countriesByCode['RU'] ?? allCountries.first; _selectedCountry = countriesByCode['RU'] ?? allCountries.first;
_clampCountryToAllowed(); _clampCountryToAllowed();
_checkTOS(); _checkTOS();
} }
Future<void> _onBackPressed() async {
final returnId = widget.returnToAccountId;
if (returnId != null) {
try {
await accountModule.switchAccount(returnId);
} catch (_) {}
if (!mounted) return;
await Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (_) => const AdaptiveShell()),
(route) => false,
);
return;
}
if (Navigator.canPop(context)) Navigator.pop(context);
}
void _clampCountryToAllowed() { void _clampCountryToAllowed() {
final allowed = api.registrationCountries; final allowed = api.registrationCountries;
if (allowed.any((c) => c.code == _selectedCountry.code)) return; if (allowed.any((c) => c.code == _selectedCountry.code)) return;
@@ -666,9 +689,10 @@ class _LoginScreenState extends State<LoginScreen> {
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
if (Navigator.canPop(context)) if (Navigator.canPop(context) ||
widget.returnToAccountId != null)
IconButton( IconButton(
onPressed: () => Navigator.pop(context), onPressed: _onBackPressed,
icon: Icon( icon: Icon(
Symbols.arrow_back, Symbols.arrow_back,
color: cs.onSurfaceVariant, color: cs.onSurfaceVariant,
@@ -25,6 +25,7 @@ import '../../../backend/modules/chats.dart';
import '../../../backend/modules/cloud_storage.dart'; import '../../../backend/modules/cloud_storage.dart';
import '../../../backend/modules/folders.dart'; import '../../../backend/modules/folders.dart';
import '../../../core/storage/app_database.dart'; import '../../../core/storage/app_database.dart';
import '../../../core/storage/token_storage.dart';
import '../../../main.dart' show accountModule, api, messagesModule; import '../../../main.dart' show accountModule, api, messagesModule;
class _StoriesScrollPhysics extends BouncingScrollPhysics { class _StoriesScrollPhysics extends BouncingScrollPhysics {
@@ -100,7 +101,6 @@ class _ChatListScreenState extends State<ChatListScreen>
bool _navDragging = false; bool _navDragging = false;
bool _isFabOpen = false; bool _isFabOpen = false;
bool _showCacheWarning = false;
bool _storiesAnimClosing = false; bool _storiesAnimClosing = false;
Timer? _contactRebuildTimer; Timer? _contactRebuildTimer;
bool get _isSelectionMode => _selectedChats.isNotEmpty; bool get _isSelectionMode => _selectedChats.isNotEmpty;
@@ -140,7 +140,6 @@ class _ChatListScreenState extends State<ChatListScreen>
_selectedFolderId, _selectedFolderId,
_isInitialLoading, _isInitialLoading,
_foldersListKnown, _foldersListKnown,
_showCacheWarning,
_isSelectionMode, _isSelectionMode,
_shouldCollapseSearch, _shouldCollapseSearch,
_selectedChats.length, _selectedChats.length,
@@ -446,12 +445,6 @@ class _ChatListScreenState extends State<ChatListScreen>
if (mounted) { if (mounted) {
setState(() { setState(() {
_sessionState = state; _sessionState = state;
if (state == SessionState.disconnected && _chats.isNotEmpty) {
_showCacheWarning = true;
}
if (state == SessionState.online) {
_showCacheWarning = false;
}
}); });
if (state == SessionState.online) { if (state == SessionState.online) {
_reloadChatsAndFolders(); _reloadChatsAndFolders();
@@ -1145,42 +1138,6 @@ class _ChatListScreenState extends State<ChatListScreen>
), ),
), ),
), ),
if (_showCacheWarning)
Padding(
padding: const EdgeInsets.fromLTRB(20, 0, 20, 8),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
decoration: BoxDecoration(
color: cs.errorContainer.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: cs.error.withValues(alpha: 0.2),
),
),
child: Row(
children: [
Icon(
Symbols.cloud_off,
size: 18,
color: cs.error,
),
const SizedBox(width: 12),
const Expanded(
child: Text(
'Ошибка соединения, сейчас вы смотрите КЕШ',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
),
],
),
),
),
Padding( Padding(
padding: const EdgeInsets.fromLTRB(20, 3, 20, 4), padding: const EdgeInsets.fromLTRB(20, 3, 20, 4),
child: Container( child: Container(
@@ -2393,9 +2350,16 @@ class _ChatListScreenState extends State<ChatListScreen>
controller.dispose(); controller.dispose();
if (!mounted) return; if (!mounted) return;
if (accountId == null) { if (accountId == null) {
await Navigator.push( final previousId = await TokenStorage.getActiveAccountId();
context, try {
MaterialPageRoute(builder: (_) => const LoginScreen()), await accountModule.beginAddAccount();
} catch (_) {}
if (!mounted) return;
await Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(
builder: (_) => LoginScreen(returnToAccountId: previousId),
),
(route) => false,
); );
return; return;
} }
+17 -2
View File
@@ -114,6 +114,7 @@ class _ChatScreenState extends State<ChatScreen>
final Map<int, Timer> _typingTimers = {}; final Map<int, Timer> _typingTimers = {};
int _otherStatus = 0; int _otherStatus = 0;
int? _otherSeenTime; int? _otherSeenTime;
int? _participantsCount;
final ValueNotifier<String> _headerStatusNotifier = ValueNotifier(''); final ValueNotifier<String> _headerStatusNotifier = ValueNotifier('');
final ValueNotifier<int> _otherReadTime = ValueNotifier(0); final ValueNotifier<int> _otherReadTime = ValueNotifier(0);
int _tempIdCounter = 0; int _tempIdCounter = 0;
@@ -172,9 +173,21 @@ class _ChatScreenState extends State<ChatScreen>
); );
unawaited(_fastPreloadCache()); unawaited(_fastPreloadCache());
unawaited(_loadParticipantsCount());
WidgetsBinding.instance.addPostFrameCallback(_onFirstFrameRendered); WidgetsBinding.instance.addPostFrameCallback(_onFirstFrameRendered);
} }
Future<void> _loadParticipantsCount() async {
if (widget.chatType != 'CHAT' && widget.chatType != 'CHANNEL') return;
final info = await ChatsModule.getChatInfo(api, widget.chatId);
if (!mounted) return;
final count = info?['participantsCount'] as int?;
if (count != null && count != _participantsCount) {
_participantsCount = count;
_recomputeHeaderStatus();
}
}
Future<void> _fastPreloadCache() async { Future<void> _fastPreloadCache() async {
final p = await AppDatabase.loadActiveProfile(); final p = await AppDatabase.loadActiveProfile();
if (!mounted) return; if (!mounted) return;
@@ -521,10 +534,12 @@ class _ChatScreenState extends State<ChatScreen>
String _headerStatus() { String _headerStatus() {
if (_typingUserIds.isNotEmpty) return 'Печатает...'; if (_typingUserIds.isNotEmpty) return 'Печатает...';
if (widget.chatType == 'CHAT') { if (widget.chatType == 'CHAT') {
return '${chat?.participants.length ?? 0} участников'; final count = _participantsCount ?? chat?.participants.length ?? 0;
return '$count участников';
} }
if (widget.chatType == 'CHANNEL') { if (widget.chatType == 'CHANNEL') {
return '${chat?.participants.length ?? 0} подписчиков'; final count = _participantsCount ?? chat?.participants.length ?? 0;
return '$count подписчиков';
} }
if (_otherStatus == 1) return 'В сети'; if (_otherStatus == 1) return 'В сети';
if (_otherStatus == 3) return 'Был(-а) недавно'; if (_otherStatus == 3) return 'Был(-а) недавно';
@@ -9,6 +9,7 @@ import '../../../core/protocol/packet.dart';
import '../../../core/utils/logger.dart'; import '../../../core/utils/logger.dart';
import '../../../main.dart'; import '../../../main.dart';
import '../../widgets/custom_notification.dart'; import '../../widgets/custom_notification.dart';
import '../../widgets/login_success_screen.dart';
class DebugMenuScreen extends StatefulWidget { class DebugMenuScreen extends StatefulWidget {
const DebugMenuScreen({super.key}); const DebugMenuScreen({super.key});
@@ -397,6 +398,73 @@ class _DebugMenuScreenState extends State<DebugMenuScreen> {
), ),
), ),
), ),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
child: Material(
color: cs.surfaceContainerHigh,
borderRadius: BorderRadius.circular(20),
child: InkWell(
borderRadius: BorderRadius.circular(20),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) =>
const LoginSuccessScreen(preview: true),
),
);
},
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 17,
),
child: Row(
children: [
Icon(
Symbols.celebration,
color: cs.onSurfaceVariant,
size: 22,
weight: 400,
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'test hello',
style: TextStyle(
color: cs.onSurface,
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 2),
Text(
'Показать приветственную анимацию входа',
style: TextStyle(
color: cs.onSurfaceVariant,
fontSize: 13,
),
),
],
),
),
Icon(
Symbols.chevron_right,
color: cs.onSurfaceVariant,
size: 22,
weight: 400,
),
],
),
),
),
),
),
),
SliverToBoxAdapter( SliverToBoxAdapter(
child: Padding( child: Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0), padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
+163 -36
View File
@@ -23,7 +23,9 @@ Future<ImageProvider?> precacheLoginAvatar(
class LoginSuccessScreen extends StatefulWidget { class LoginSuccessScreen extends StatefulWidget {
final ImageProvider? avatar; final ImageProvider? avatar;
const LoginSuccessScreen({super.key, this.avatar}); final bool preview;
const LoginSuccessScreen({super.key, this.avatar, this.preview = false});
@override @override
State<LoginSuccessScreen> createState() => _LoginSuccessScreenState(); State<LoginSuccessScreen> createState() => _LoginSuccessScreenState();
@@ -54,6 +56,8 @@ class _LoginSuccessScreenState extends State<LoginSuccessScreen>
late final Animation<double> _fadeOut; late final Animation<double> _fadeOut;
bool _navigated = false; bool _navigated = false;
bool _handedOff = false;
final GlobalKey _subtitleKey = GlobalKey();
@override @override
void initState() { void initState() {
@@ -114,26 +118,66 @@ class _LoginSuccessScreenState extends State<LoginSuccessScreen>
void _onStatus(AnimationStatus status) { void _onStatus(AnimationStatus status) {
if (status == AnimationStatus.completed && !_navigated && mounted) { if (status == AnimationStatus.completed && !_navigated && mounted) {
_navigated = true; _navigated = true;
Navigator.of(context).pushAndRemoveUntil(
PageRouteBuilder( final navigator = Navigator.of(context);
transitionDuration: const Duration(milliseconds: 360), final overlay = navigator.overlay;
reverseTransitionDuration: const Duration(milliseconds: 200), final entry = _buildGreetingEntry();
pageBuilder: (_, __, ___) => const AdaptiveShell(), if (entry != null) {
transitionsBuilder: (_, animation, __, child) { setState(() => _handedOff = true);
return FadeTransition( }
opacity: CurvedAnimation(
parent: animation, if (widget.preview) {
curve: Curves.easeOutCubic, navigator.pop();
), } else {
child: child, navigator.pushAndRemoveUntil(
); PageRouteBuilder(
}, transitionDuration: const Duration(milliseconds: 360),
), reverseTransitionDuration: const Duration(milliseconds: 200),
(route) => false, pageBuilder: (_, _, _) => const AdaptiveShell(),
); transitionsBuilder: (_, animation, _, child) {
return FadeTransition(
opacity: CurvedAnimation(
parent: animation,
curve: Curves.easeOutCubic,
),
child: child,
);
},
),
(route) => false,
);
}
if (entry != null && overlay != null) {
overlay.insert(entry);
}
} }
} }
OverlayEntry? _buildGreetingEntry() {
final box = _subtitleKey.currentContext?.findRenderObject() as RenderBox?;
if (box == null || !box.attached) return null;
final topLeft = box.localToGlobal(Offset.zero);
final size = box.size;
final color = Theme.of(context).colorScheme.onSurfaceVariant;
late final OverlayEntry entry;
entry = OverlayEntry(
builder: (_) => Positioned(
left: topLeft.dx,
top: topLeft.dy,
width: size.width,
child: _GreetingLinger(
greeting: _greeting,
color: color,
onDone: () => entry.remove(),
),
),
);
return entry;
}
@override @override
void dispose() { void dispose() {
_controller.removeStatusListener(_onStatus); _controller.removeStatusListener(_onStatus);
@@ -149,11 +193,12 @@ class _LoginSuccessScreenState extends State<LoginSuccessScreen>
body: AnimatedBuilder( body: AnimatedBuilder(
animation: _controller, animation: _controller,
builder: (context, _) { builder: (context, _) {
return Opacity( final celebrationOpacity = 1.0 - _fadeOut.value;
opacity: 1.0 - _fadeOut.value, return Stack(
child: Stack( children: [
children: [ Positioned.fill(
Positioned.fill( child: Opacity(
opacity: celebrationOpacity,
child: DecoratedBox( child: DecoratedBox(
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: RadialGradient( gradient: RadialGradient(
@@ -169,11 +214,14 @@ class _LoginSuccessScreenState extends State<LoginSuccessScreen>
), ),
), ),
), ),
Center( ),
child: Column( Center(
mainAxisSize: MainAxisSize.min, child: Column(
children: [ mainAxisSize: MainAxisSize.min,
SizedBox( children: [
Opacity(
opacity: celebrationOpacity,
child: SizedBox(
width: 220, width: 220,
height: 220, height: 220,
child: Stack( child: Stack(
@@ -186,15 +234,21 @@ class _LoginSuccessScreenState extends State<LoginSuccessScreen>
], ],
), ),
), ),
const SizedBox(height: 28), ),
_buildTitle(cs), const SizedBox(height: 28),
const SizedBox(height: 8), Opacity(
_buildSubtitle(cs), opacity: celebrationOpacity,
], child: _buildTitle(cs),
), ),
const SizedBox(height: 8),
Opacity(
opacity: _handedOff ? 0.0 : 1.0,
child: _buildSubtitle(cs),
),
],
), ),
], ),
), ],
); );
}, },
), ),
@@ -315,6 +369,7 @@ class _LoginSuccessScreenState extends State<LoginSuccessScreen>
padding: const EdgeInsets.symmetric(horizontal: 40), padding: const EdgeInsets.symmetric(horizontal: 40),
child: Text( child: Text(
_greeting, _greeting,
key: _subtitleKey,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
color: cs.onSurfaceVariant, color: cs.onSurfaceVariant,
@@ -329,6 +384,78 @@ class _LoginSuccessScreenState extends State<LoginSuccessScreen>
} }
} }
class _GreetingLinger extends StatefulWidget {
final String greeting;
final Color color;
final VoidCallback onDone;
const _GreetingLinger({
required this.greeting,
required this.color,
required this.onDone,
});
@override
State<_GreetingLinger> createState() => _GreetingLingerState();
}
class _GreetingLingerState extends State<_GreetingLinger>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
late final Animation<double> _opacity;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1200),
);
_opacity = TweenSequence<double>([
TweenSequenceItem(tween: ConstantTween<double>(1.0), weight: 55),
TweenSequenceItem(
tween: Tween<double>(begin: 1.0, end: 0.0)
.chain(CurveTween(curve: Curves.easeInCubic)),
weight: 45,
),
]).animate(_controller);
_controller.addStatusListener((status) {
if (status == AnimationStatus.completed) widget.onDone();
});
_controller.forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return IgnorePointer(
child: AnimatedBuilder(
animation: _opacity,
builder: (context, _) {
return Opacity(
opacity: _opacity.value,
child: Text(
widget.greeting,
textAlign: TextAlign.center,
style: TextStyle(
color: widget.color,
fontSize: 15,
fontWeight: FontWeight.w500,
height: 1.3,
),
),
);
},
),
);
}
}
class _CheckPainter extends CustomPainter { class _CheckPainter extends CustomPainter {
final double progress; final double progress;
final Color color; final Color color;
+45 -19
View File
@@ -300,6 +300,10 @@ class MessageBubble extends StatelessWidget {
) )
: _buildContent(makeCtx()); : _buildContent(makeCtx());
final reactionsUnder = _reactionsUnderBubble(contentType);
final reactionsInside =
contentType != MessageType.text && !reactionsUnder;
return GestureDetector( return GestureDetector(
onTap: Haptics.tap, onTap: Haptics.tap,
child: Padding( child: Padding(
@@ -349,16 +353,15 @@ class MessageBubble extends StatelessWidget {
padding: padding, padding: padding,
child: child, child: child,
), ),
child: bubbleContent, child: reactionsInside
), ? Column(
if (contentType != MessageType.text) mainAxisSize: MainAxisSize.min,
reactionsListenable != null crossAxisAlignment: CrossAxisAlignment.start,
? ValueListenableBuilder<Map<String, dynamic>?>( children: [bubbleContent, _reactionsBar(cs)],
valueListenable: reactionsListenable!,
builder: (context, info, _) =>
_buildReactionsBarFor(cs, info),
) )
: _buildReactionsBar(cs), : bubbleContent,
),
if (reactionsUnder) _reactionsBar(cs),
], ],
), ),
], ],
@@ -378,6 +381,27 @@ class MessageBubble extends StatelessWidget {
return null; return null;
} }
bool _reactionsUnderBubble(MessageType contentType) {
if (contentType != MessageType.attachment) return false;
final attachments = message.attachments;
if (attachments == null || attachments.isEmpty) return false;
if (attachments.first is ForwardedMessageAttachment) return false;
if (attachments.any((a) => a is ContactAttachment)) return false;
if (attachments.whereType<PhotoAttachment>().length >= 2) return false;
return true;
}
Widget _reactionsBar(ColorScheme cs) {
final listenable = reactionsListenable;
if (listenable != null) {
return ValueListenableBuilder<Map<String, dynamic>?>(
valueListenable: listenable,
builder: (context, info, _) => _buildReactionsBarFor(cs, info),
);
}
return _buildReactionsBar(cs);
}
Widget _buildContent(_BubbleCtx ctx) { Widget _buildContent(_BubbleCtx ctx) {
switch (ctx.contentType) { switch (ctx.contentType) {
case MessageType.control: case MessageType.control:
@@ -1752,10 +1776,7 @@ class _VoiceMessageBubbleState extends State<_VoiceMessageBubble> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
if (widget.preloadedText != null) { _transcriptionText = widget.preloadedText;
_transcriptionText = widget.preloadedText;
_transcriptionVisible = true;
}
} }
String _formatDuration(int seconds) { String _formatDuration(int seconds) {
@@ -1917,14 +1938,19 @@ class _VoiceMessageBubbleState extends State<_VoiceMessageBubble> {
Row( Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( SizedBox(
_formatDuration(widget.duration), width: 32,
style: TextStyle( child: Center(
color: widget.textColor.withValues(alpha: 0.7), child: Text(
fontSize: 11, _formatDuration(widget.duration),
style: TextStyle(
color: widget.textColor.withValues(alpha: 0.7),
fontSize: 11,
),
),
), ),
), ),
const SizedBox(width: 8), const SizedBox(width: 10),
Expanded( Expanded(
child: AnimatedSize( child: AnimatedSize(
duration: const Duration(milliseconds: 200), duration: const Duration(milliseconds: 200),
+19 -1
View File
@@ -700,8 +700,13 @@ class _StartupScreenState extends State<_StartupScreen> {
} }
Future<void> _tryAutoLogin() async { Future<void> _tryAutoLogin() async {
final accountId = await TokenStorage.getActiveAccountId(); int? accountId = await TokenStorage.getActiveAccountId();
if (accountId == null || await TokenStorage.readToken(accountId) == null) { if (accountId == null || await TokenStorage.readToken(accountId) == null) {
accountId = await _recoverActiveAccount();
}
if (accountId == null) {
_goToLogin(); _goToLogin();
return; return;
} }
@@ -718,6 +723,19 @@ class _StartupScreenState extends State<_StartupScreen> {
} }
} }
Future<int?> _recoverActiveAccount() async {
final profiles = await AppDatabase.loadAllProfiles();
for (final profile in profiles) {
if (await TokenStorage.readToken(profile.id) != null) {
await TokenStorage.setActiveAccount(profile.id);
await AppDatabase.setActiveAccount(profile.id);
await ContactsModule.primeCacheFromDb(profile.id);
return profile.id;
}
}
return null;
}
void _goToLogin() { void _goToLogin() {
if (mounted) { if (mounted) {
Navigator.pushReplacement( Navigator.pushReplacement(