Files
Codex Bot 32b38820d6
build / Build Linux wallet (push) Successful in 2m18s
build / Build Windows wallet (push) Successful in 20m41s
Use opaque surfaces for wallet overlays and dropdowns
2026-04-20 22:19:08 +02:00

892 lines
29 KiB
Dart

import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:path/path.dart' as p;
import 'package:window_manager/window_manager.dart';
import 'domain/models.dart';
import 'package:salvium_wallet/l10n/app_localizations.dart';
import 'services/app_paths.dart';
import 'services/local_node_service.dart';
import 'state/providers.dart';
import 'state/wallet_controller.dart';
import 'ui/screens/home_shell.dart';
import 'ui/screens/wallet_setup_screen.dart';
import 'ui/widgets/password_prompt_dialog.dart';
class PeyaApp extends ConsumerStatefulWidget {
const PeyaApp({super.key});
@override
ConsumerState<PeyaApp> createState() => _PeyaAppState();
}
class _PeyaAppState extends ConsumerState<PeyaApp> with WindowListener {
static const _bgPanel = Color(0xFF0C1824);
static const _bgPanelStrong = Color(0xFF0F1C2B);
static const _line = Color(0x1F88FFE1);
static const _textMain = Color(0xFFEFFAF8);
static const _textSoft = Color(0xFF9DB7B2);
static const _accentMint = Color(0xFF42F5C8);
static const _accentCyan = Color(0xFF4CC8FF);
static const _accentGold = Color(0xFFD4FF69);
bool _didInit = false;
bool _didApplyStartMinimized = false;
ProviderSubscription<AppConfig>? _configSubscription;
ProviderSubscription<WalletState>? _walletSubscription;
final GlobalKey<ScaffoldMessengerState> _scaffoldMessengerKey =
GlobalKey<ScaffoldMessengerState>();
final GlobalKey<NavigatorState> _navigatorKey = GlobalKey<NavigatorState>();
String? _lastErrorMessage;
String? _lastStatusMessage;
bool _statusSnackVisible = false;
bool _desktopCloseHookInitialized = false;
bool _handlingWindowClose = false;
Future<void> _logCloseDebug(String message) async {
try {
final file = File(p.join((await AppPaths.appSupportDir()).path, 'close-debug.log'));
await file.parent.create(recursive: true);
await file.writeAsString(
'[${DateTime.now().toIso8601String()}] $message\n',
mode: FileMode.writeOnlyAppend,
);
} catch (_) {}
}
@override
void initState() {
super.initState();
_bootstrap();
_configSubscription = ref
.listenManual<AppConfig>(appConfigControllerProvider, (previous, next) {
_updateTrayMenu();
_connectNodeIfPossible(next);
if (!_didApplyStartMinimized) {
_applyStartMinimized(next);
}
});
_walletSubscription = ref
.listenManual<WalletState>(walletControllerProvider, (previous, next) {
_handleOperationStatusChange(previous, next);
if (previous?.walletInfo == null && next.walletInfo != null) {
final config = ref.read(appConfigControllerProvider);
_connectNodeIfPossible(config);
}
final error = next.error;
if (error == null || error.isEmpty || error == _lastErrorMessage) {
if (error == null) {
_lastErrorMessage = null;
}
return;
}
_lastErrorMessage = error;
_showErrorSnackBar(error);
});
}
void _handleOperationStatusChange(WalletState? previous, WalletState next) {
final messenger = _scaffoldMessengerKey.currentState;
if (messenger == null) {
return;
}
final shouldShowStatus = !next.isBlockingOperation &&
next.operationMessage != null &&
next.operationMessage!.isNotEmpty;
if (shouldShowStatus) {
final message = _localizeOperationStatus(next.operationMessage!);
if (_statusSnackVisible && _lastStatusMessage == message) {
return;
}
messenger.clearSnackBars();
messenger.removeCurrentSnackBar();
messenger.showSnackBar(
SnackBar(
content: Text(message),
duration: const Duration(days: 1),
),
);
_statusSnackVisible = true;
_lastStatusMessage = message;
return;
}
if (_statusSnackVisible) {
messenger.clearSnackBars();
messenger.removeCurrentSnackBar();
_statusSnackVisible = false;
_lastStatusMessage = null;
}
}
String _localizeOperationStatus(String message) {
final l10n = AppLocalizations.of(
_navigatorKey.currentContext ?? _scaffoldMessengerKey.currentContext ?? context,
);
if (l10n == null) {
return message;
}
switch (message) {
case 'Preparing wallet directory...':
return 'Preparing wallet directory...'.startsWith('Preparing') &&
Localizations.localeOf(
_navigatorKey.currentContext ?? _scaffoldMessengerKey.currentContext ?? context,
).languageCode ==
'pl'
? 'Przygotowywanie katalogu portfela...'
: message;
case 'Creating wallet...':
return Localizations.localeOf(
_navigatorKey.currentContext ?? _scaffoldMessengerKey.currentContext ?? context,
).languageCode ==
'pl'
? 'Tworzenie portfela...'
: message;
case 'Preparing temporary wallet...':
return Localizations.localeOf(
_navigatorKey.currentContext ?? _scaffoldMessengerKey.currentContext ?? context,
).languageCode ==
'pl'
? 'Przygotowywanie tymczasowego portfela...'
: message;
case 'Generating seed phrase...':
return Localizations.localeOf(
_navigatorKey.currentContext ?? _scaffoldMessengerKey.currentContext ?? context,
).languageCode ==
'pl'
? 'Generowanie seedu...'
: message;
case 'Restoring wallet from seed...':
return Localizations.localeOf(
_navigatorKey.currentContext ?? _scaffoldMessengerKey.currentContext ?? context,
).languageCode ==
'pl'
? 'Przywracanie portfela z seedu...'
: message;
case 'Checking wallet files...':
return Localizations.localeOf(
_navigatorKey.currentContext ?? _scaffoldMessengerKey.currentContext ?? context,
).languageCode ==
'pl'
? 'Sprawdzanie plików portfela...'
: message;
case 'Opening wallet...':
return Localizations.localeOf(
_navigatorKey.currentContext ?? _scaffoldMessengerKey.currentContext ?? context,
).languageCode ==
'pl'
? 'Otwieranie portfela...'
: message;
case 'Loading wallet data...':
return Localizations.localeOf(
_navigatorKey.currentContext ?? _scaffoldMessengerKey.currentContext ?? context,
).languageCode ==
'pl'
? 'Wczytywanie danych portfela...'
: message;
case 'Connecting to node...':
return Localizations.localeOf(
_navigatorKey.currentContext ?? _scaffoldMessengerKey.currentContext ?? context,
).languageCode ==
'pl'
? 'Łączenie z nodem...'
: message;
case 'Creating subaddress...':
return Localizations.localeOf(
_navigatorKey.currentContext ?? _scaffoldMessengerKey.currentContext ?? context,
).languageCode ==
'pl'
? 'Tworzenie subadresu...'
: message;
case 'Updating subaddress...':
return Localizations.localeOf(
_navigatorKey.currentContext ?? _scaffoldMessengerKey.currentContext ?? context,
).languageCode ==
'pl'
? 'Aktualizowanie subadresu...'
: message;
case 'Preparing transaction...':
return Localizations.localeOf(
_navigatorKey.currentContext ?? _scaffoldMessengerKey.currentContext ?? context,
).languageCode ==
'pl'
? 'Przygotowywanie transakcji...'
: message;
case 'Submitting transaction...':
return Localizations.localeOf(
_navigatorKey.currentContext ?? _scaffoldMessengerKey.currentContext ?? context,
).languageCode ==
'pl'
? 'Wysyłanie transakcji...'
: message;
case 'Preparing stake...':
return Localizations.localeOf(
_navigatorKey.currentContext ?? _scaffoldMessengerKey.currentContext ?? context,
).languageCode ==
'pl'
? 'Przygotowywanie stake...'
: message;
case 'Submitting stake...':
return Localizations.localeOf(
_navigatorKey.currentContext ?? _scaffoldMessengerKey.currentContext ?? context,
).languageCode ==
'pl'
? 'Wysyłanie stake...'
: message;
case 'Refreshing balances...':
return Localizations.localeOf(
_navigatorKey.currentContext ?? _scaffoldMessengerKey.currentContext ?? context,
).languageCode ==
'pl'
? 'Odświeżanie sald...'
: message;
case 'Loading transactions...':
return Localizations.localeOf(
_navigatorKey.currentContext ?? _scaffoldMessengerKey.currentContext ?? context,
).languageCode ==
'pl'
? 'Wczytywanie transakcji...'
: message;
default:
return message;
}
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_updateTrayMenu();
}
@override
void dispose() {
if (_desktopCloseHookInitialized) {
windowManager.removeListener(this);
}
_configSubscription?.close();
_walletSubscription?.close();
super.dispose();
}
Future<void> _bootstrap() async {
if (_didInit) {
return;
}
_didInit = true;
final tray = ref.read(trayServiceProvider);
await tray.init();
await _initDesktopCloseHook();
ref.read(syncSchedulerProvider);
ref.read(walletConfigSyncProvider);
final config = ref.read(appConfigControllerProvider);
final requiresPassword =
await ref.read(walletControllerProvider.notifier).tryAutoOpen(config);
if (requiresPassword && config.lastWallet != null) {
await _promptForSavedWalletPassword(config.lastWallet!);
}
await _connectNodeIfPossible(config);
await _updateTrayMenu();
}
Future<void> _initDesktopCloseHook() async {
if (_desktopCloseHookInitialized ||
!(Platform.isLinux || Platform.isWindows)) {
await _logCloseDebug(
'initDesktopCloseHook skipped: initialized=$_desktopCloseHookInitialized platform=${Platform.operatingSystem}',
);
return;
}
await windowManager.ensureInitialized();
windowManager.addListener(this);
await windowManager.setPreventClose(true);
final preventClose = await windowManager.isPreventClose();
_desktopCloseHookInitialized = true;
await _logCloseDebug(
'initDesktopCloseHook ready: platform=${Platform.operatingSystem} preventClose=$preventClose',
);
}
@override
void onWindowClose() {
unawaited(_logCloseDebug('onWindowClose fired'));
unawaited(_handleNativeWindowClose());
}
Future<void> _handleNativeWindowClose() async {
if (_handlingWindowClose) {
await _logCloseDebug('handleNativeWindowClose skipped: already handling');
return;
}
_handlingWindowClose = true;
try {
await _logCloseDebug('handleNativeWindowClose entered');
final shouldClose = await _handleAppCloseRequest();
await _logCloseDebug('handleNativeWindowClose decision: shouldClose=$shouldClose');
if (!shouldClose) {
return;
}
await windowManager.setPreventClose(false);
await _logCloseDebug('handleNativeWindowClose destroying window');
await windowManager.destroy();
} catch (error, stack) {
await _logCloseDebug('handleNativeWindowClose error: $error\n$stack');
} finally {
await _logCloseDebug('handleNativeWindowClose finished');
_handlingWindowClose = false;
}
}
Future<void> _promptForSavedWalletPassword(WalletInfo wallet) async {
await WidgetsBinding.instance.endOfFrame;
while (mounted && ref.read(walletControllerProvider).walletInfo == null) {
final l10n = AppLocalizations.of(context);
if (l10n == null) {
return;
}
final password = await promptForPassword(
context: context,
title: l10n.openWalletAction,
label: l10n.passwordLabel,
confirmLabel: l10n.openAction,
cancelLabel: l10n.cancelAction,
message: wallet.name,
);
if (password == null) {
return;
}
try {
await ref.read(walletControllerProvider.notifier).openWallet(
path: wallet.path,
password: password,
);
return;
} catch (error) {
_showErrorSnackBar(error.toString());
}
}
}
Future<void> _connectNodeIfPossible(AppConfig config) async {
final walletState = ref.read(walletControllerProvider);
if (walletState.walletInfo == null) {
return;
}
if (config.nodeConfig.mode == NodeMode.local) {
final localNode = ref.read(localNodeServiceProvider);
final started = await localNode.ensureRunning(
config: LocalNodeConfig(extraArgs: config.localNodeArgs),
);
if (!started) {
ref.read(loggerProvider).w('Failed to ensure local node is running.');
return;
}
}
final connected = await _connectAndSyncWithRetry(
config,
attempts: config.nodeConfig.mode == NodeMode.local ? 10 : 1,
);
if (!connected) {
ref.read(loggerProvider).w('Failed to connect node after retries.');
return;
}
_scheduleBurstSync();
}
Future<bool> _connectAndSyncWithRetry(
AppConfig config, {
required int attempts,
}) async {
var sawConnected = false;
for (var attempt = 0; attempt < attempts; attempt++) {
try {
await ref
.read(walletControllerProvider.notifier)
.connectNode(config.nodeConfig);
await ref
.read(walletControllerProvider.notifier)
.syncNow(announceStages: true);
final syncedState = ref.read(walletControllerProvider);
if (syncedState.syncStatus.lastSync != null) {
return true;
}
} catch (_) {}
sawConnected = await ref.read(walletRepositoryProvider).isConnected();
if (sawConnected) {
final currentState = ref.read(walletControllerProvider);
if (currentState.syncStatus.lastSync != null) {
return true;
}
}
if (attempt + 1 < attempts) {
await Future.delayed(const Duration(milliseconds: 500));
}
}
return sawConnected;
}
void _scheduleBurstSync() {
unawaited(
ref.read(syncSchedulerProvider).burstSync(
count: 4,
interval: const Duration(seconds: 2),
),
);
}
Future<void> _applyStartMinimized(AppConfig config) async {
if (_didApplyStartMinimized) {
return;
}
_didApplyStartMinimized = true;
if (config.startMinimized && config.minimizeToTray) {
WidgetsBinding.instance.addPostFrameCallback((_) async {
await ref.read(trayServiceProvider).hideWindow();
});
}
}
Future<void> _updateTrayMenu() async {
final tray = ref.read(trayServiceProvider);
if (!tray.isAvailable) {
return;
}
final l10n = AppLocalizations.of(context);
if (l10n == null) {
return;
}
final visible = await windowManager.isVisible();
await tray.updateMenu(
isWindowVisible: visible,
showLabel: l10n.trayShow,
hideLabel: l10n.trayHide,
syncLabel: l10n.traySyncNow,
quitLabel: l10n.trayQuit,
onToggleVisibility: () async {
if (await windowManager.isVisible()) {
await tray.hideWindow();
} else {
await tray.showWindow();
}
await _updateTrayMenu();
},
onSyncNow: () => ref.read(walletControllerProvider.notifier).syncNow(),
onQuit: () async {
final shouldClose = await _handleAppCloseRequest();
if (!shouldClose) {
return;
}
await tray.dispose();
await windowManager.setPreventClose(false);
await windowManager.close();
},
);
}
Future<bool> _handleAppCloseRequest() async {
final config = ref.read(appConfigControllerProvider);
final localNodeService = ref.read(localNodeServiceProvider);
final shouldPromptForLocalNode = config.nodeConfig.mode == NodeMode.local &&
((await localNodeService.isRunning(
config: LocalNodeConfig(extraArgs: config.localNodeArgs),
)) ||
(await ref.read(walletRepositoryProvider).isConnected()));
await _logCloseDebug(
'handleAppCloseRequest: mode=${config.nodeConfig.mode} prompt=$shouldPromptForLocalNode mounted=$mounted',
);
var shouldStopLocalNode = false;
if (shouldPromptForLocalNode) {
if (!mounted) {
await _logCloseDebug('handleAppCloseRequest aborted: not mounted');
return false;
}
final dialogContext =
_navigatorKey.currentContext ?? _scaffoldMessengerKey.currentContext ?? context;
final l10n = AppLocalizations.of(dialogContext);
await _logCloseDebug('handleAppCloseRequest showing dialog');
final decision = await showDialog<bool?>(
context: dialogContext,
builder: (dialogContext) {
return AlertDialog(
title: Text(
l10n?.closeWalletNodeDialogTitle ?? 'Close wallet?',
),
content: Text(
l10n?.closeWalletNodeDialogBody ??
'A local node is still running. Do you want to keep it running after closing the wallet?',
),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(null),
child: Text(l10n?.cancelAction ?? 'Cancel'),
),
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(false),
child: Text(
l10n?.closeWalletKeepNodeAction ?? 'Keep node running',
),
),
ElevatedButton(
onPressed: () => Navigator.of(dialogContext).pop(true),
child: Text(
l10n?.closeWalletStopNodeAction ?? 'Stop node and close',
),
),
],
);
},
);
if (decision == null) {
await _logCloseDebug('handleAppCloseRequest dialog cancelled');
return false;
}
shouldStopLocalNode = decision;
await _logCloseDebug('handleAppCloseRequest dialog decision: stopNode=$shouldStopLocalNode');
}
if (shouldStopLocalNode) {
final stopped = await localNodeService.stop();
await _logCloseDebug('handleAppCloseRequest stop result: $stopped');
if (!stopped) {
if (mounted) {
final l10n = AppLocalizations.of(context);
_scaffoldMessengerKey.currentState?.showSnackBar(
SnackBar(
content: Text(
l10n?.localNodeStopFailure ?? 'Failed to stop local node',
),
),
);
}
return false;
}
}
await ref.read(walletRepositoryProvider).closeWallet();
await _logCloseDebug('handleAppCloseRequest wallet closed');
return true;
}
void _showErrorSnackBar(String message) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) {
return;
}
final messenger = _scaffoldMessengerKey.currentState;
if (messenger == null) {
return;
}
final l10n = AppLocalizations.of(context);
final text = l10n?.errorMessage(message) ?? message;
messenger.showSnackBar(
SnackBar(content: Text(text)),
);
});
}
@override
Widget build(BuildContext context) {
final config = ref.watch(appConfigControllerProvider);
return MaterialApp(
onGenerateTitle: (context) => AppLocalizations.of(context)!.appTitle,
debugShowCheckedModeBanner: false,
scaffoldMessengerKey: _scaffoldMessengerKey,
navigatorKey: _navigatorKey,
theme: _buildTheme(),
locale: config.resolveLocale(),
supportedLocales: const [
Locale('en'),
Locale('pl'),
],
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
builder: (context, child) {
return DecoratedBox(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
Color(0xFF04090D),
Color(0xFF09131C),
Color(0xFF060D14),
],
stops: [0.0, 0.55, 1.0],
),
),
child: Stack(
fit: StackFit.expand,
children: [
const Positioned(
top: -120,
left: -80,
child: _GlowOrb(
size: 320,
color: Color(0x334CC8FF),
),
),
const Positioned(
top: -60,
right: -40,
child: _GlowOrb(
size: 260,
color: Color(0x2942F5C8),
),
),
if (child != null) child,
],
),
);
},
home: _RootRouter(),
);
}
ThemeData _buildTheme() {
const colorScheme = ColorScheme.dark(
brightness: Brightness.dark,
primary: _accentMint,
onPrimary: Color(0xFF02120E),
secondary: _accentCyan,
onSecondary: Color(0xFF03131A),
tertiary: _accentGold,
onTertiary: Color(0xFF111600),
error: Color(0xFFFF8F8F),
onError: Color(0xFF290000),
surface: _bgPanel,
onSurface: _textMain,
surfaceContainerHighest: _bgPanelStrong,
onSurfaceVariant: _textSoft,
outline: _line,
outlineVariant: Color(0x1AFFFFFF),
);
final base = ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
colorScheme: colorScheme,
scaffoldBackgroundColor: Colors.transparent,
canvasColor: Colors.transparent,
splashFactory: InkSparkle.splashFactory,
);
return base.copyWith(
textTheme: base.textTheme.apply(
bodyColor: _textMain,
displayColor: _textMain,
),
appBarTheme: const AppBarTheme(
backgroundColor: Colors.transparent,
foregroundColor: _textMain,
elevation: 0,
scrolledUnderElevation: 0,
),
cardTheme: CardThemeData(
color: _bgPanel,
elevation: 0,
margin: EdgeInsets.zero,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(26),
side: const BorderSide(color: _line),
),
),
dialogTheme: DialogThemeData(
backgroundColor: _bgPanelStrong,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(28),
side: const BorderSide(color: _line),
),
),
dividerTheme: const DividerThemeData(
color: Color(0x1AFFFFFF),
space: 1,
thickness: 1,
),
snackBarTheme: SnackBarThemeData(
backgroundColor: _bgPanelStrong,
contentTextStyle: base.textTheme.bodyMedium?.copyWith(color: _textMain),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(18),
side: const BorderSide(color: _line),
),
behavior: SnackBarBehavior.floating,
),
listTileTheme: const ListTileThemeData(
iconColor: _accentCyan,
textColor: _textMain,
),
navigationRailTheme: NavigationRailThemeData(
backgroundColor: const Color(0x880C1824),
selectedIconTheme: const IconThemeData(color: _accentMint),
selectedLabelTextStyle: base.textTheme.labelMedium?.copyWith(
color: _accentMint,
fontWeight: FontWeight.w700,
),
unselectedIconTheme: const IconThemeData(color: _textSoft),
unselectedLabelTextStyle: base.textTheme.labelMedium?.copyWith(
color: _textSoft,
),
indicatorColor: const Color(0x2242F5C8),
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: const Color(0xFF122031),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(18),
borderSide: const BorderSide(color: _line),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(18),
borderSide: const BorderSide(color: _line),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(18),
borderSide: const BorderSide(color: _accentCyan),
),
labelStyle: const TextStyle(color: _textSoft),
),
dropdownMenuTheme: DropdownMenuThemeData(
menuStyle: MenuStyle(
backgroundColor: WidgetStatePropertyAll(_bgPanelStrong),
surfaceTintColor: const WidgetStatePropertyAll(Colors.transparent),
shape: WidgetStatePropertyAll(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(18),
side: const BorderSide(color: _line),
),
),
),
textStyle: const TextStyle(color: _textMain),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: _bgPanel,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(18),
borderSide: const BorderSide(color: _line),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(18),
borderSide: const BorderSide(color: _line),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(18),
borderSide: const BorderSide(color: _accentCyan),
),
),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: _accentMint,
foregroundColor: const Color(0xFF03120D),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(18),
),
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 16),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
foregroundColor: _accentCyan,
side: const BorderSide(color: _line),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(18),
),
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 16),
),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: _accentGold,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(18),
),
),
),
);
}
}
class _GlowOrb extends StatelessWidget {
const _GlowOrb({
required this.size,
required this.color,
});
final double size;
final Color color;
@override
Widget build(BuildContext context) {
return IgnorePointer(
child: Container(
width: size,
height: size,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: RadialGradient(
colors: [
color,
color.withValues(alpha: 0),
],
),
),
),
);
}
}
class _RootRouter extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(walletControllerProvider);
final config = ref.watch(appConfigControllerProvider);
final child = state.walletInfo == null
? WalletSetupScreen(key: const Key('wallet-setup'))
: HomeShell(key: Key('home-shell-${config.languagePreference.name}'));
return Stack(
children: [
child,
if (state.isLoading && state.isBlockingOperation)
Positioned.fill(
child: ColoredBox(
color: const Color(0x66000000),
child: Center(
child: Card(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 20,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const CircularProgressIndicator(),
const SizedBox(height: 16),
Text(
state.operationMessage ??
AppLocalizations.of(context)
?.walletOperationInProgress ??
'Working...',
),
],
),
),
),
),
),
),
],
);
}
}