Files
notas/lib/app.dart
T
Marcos 1dede9eb78 Refactor theme management: Replace AppColors with AppPalette
- Removed AppColors class and migrated all references to AppPalette.
- Updated VaultAccessScreen, MenuDrawer, NoteCard, SearchAppBar, and other widgets to use AppPalette for color management.
- Introduced AppPalette to handle light and dark themes with appropriate color schemes.
- Adjusted theme application in AppTheme to utilize AppPalette extensions.
- Updated tests to reflect changes in theme structure and color references.
2026-05-23 13:55:40 +02:00

1114 lines
30 KiB
Dart

import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:notas/data/app_database.dart';
import 'package:notas/data/api_client.dart';
import 'package:notas/data/local_vault_service.dart';
import 'package:notas/data/note_repository.dart';
import 'package:notas/platform/app_platform.dart';
import 'package:notas/platform/window_state.dart';
import 'package:notas/screens/biometric_choice_screen.dart';
import 'package:notas/screens/biometric_gate_screen.dart';
import 'package:notas/screens/home_screen.dart';
import 'package:notas/screens/settings_screen.dart';
import 'package:notas/screens/vault_access_screen.dart';
import 'package:notas/theme/app_palette.dart';
import 'package:notas/theme/app_theme.dart';
import 'package:notas/widgets/sync_status.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:window_manager/window_manager.dart';
enum _AppSection { home, settings }
enum _AppPhase { loading, access, biometricChoice, biometricGate, notes }
class PerformSyncIntent extends Intent {
const PerformSyncIntent();
}
class NotesApp extends StatefulWidget {
const NotesApp({super.key});
@override
State<NotesApp> createState() => _NotesAppState();
}
class _NotesAppState extends State<NotesApp>
with WindowListener, WidgetsBindingObserver {
static const Duration _screenTransitionDuration = Duration(milliseconds: 280);
static const Duration _biometricInactivityTimeout = Duration(minutes: 5);
static const Duration _syncInterval = Duration(minutes: 5);
static const Duration _windowSizeSaveDelay = Duration(milliseconds: 350);
static const String _themeSeedColorKey = 'theme_seed_color_v1';
static const String _themeModeKey = 'theme_mode_v1';
final LocalVaultService _vaultService = LocalVaultService.instance;
final GlobalKey<ScaffoldMessengerState> _scaffoldMessengerKey =
GlobalKey<ScaffoldMessengerState>();
final GlobalKey<NavigatorState> _navigatorKey = GlobalKey<NavigatorState>();
AppDatabase? _database;
NoteRepository? _repository;
String? _pendingEncryptionKey;
bool _isBootstrapping = true;
bool _isUnlocking = false;
bool _biometricGateEnabled = false;
int _biometricGateSession = 0;
Timer? _biometricLockTimer;
Timer? _windowSizeSaveTimer;
Timer? _syncTimer;
bool _isHandlingWindowClose = false;
_AppPhase _phase = _AppPhase.loading;
_AppSection _currentSection = _AppSection.home;
SyncStatus _syncStatus = SyncStatus.idle;
double? _syncProgress;
String? _syncDetailMessage;
String? _syncErrorMessage;
int _syncOperationId = 0;
int _homeRefreshToken = 0;
Color _themeSeedColor = Colors.amber;
ThemeMode _themeMode = ThemeMode.system;
// Cached ThemeData for light and dark variants.
ThemeData? _lightTheme;
ThemeData? _darkTheme;
Brightness _effectiveBrightness() {
switch (_themeMode) {
case ThemeMode.dark:
return Brightness.dark;
case ThemeMode.light:
return Brightness.light;
case ThemeMode.system:
return WidgetsBinding.instance.platformDispatcher.platformBrightness;
}
}
AppPalette _activePalette() {
return AppPalette.fromBrightness(
_effectiveBrightness(),
seedColor: _themeSeedColor,
);
}
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
if (isDesktop) {
windowManager.addListener(this);
windowManager.setPreventClose(true);
}
_loadThemeSeedColor();
_loadThemeMode();
_bootstrapVault();
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
if (isDesktop) {
windowManager.removeListener(this);
windowManager.setPreventClose(false);
}
_biometricLockTimer?.cancel();
_windowSizeSaveTimer?.cancel();
_syncTimer?.cancel();
_database?.close();
super.dispose();
}
Future<void> _loadThemeSeedColor() async {
final SharedPreferences prefs = await SharedPreferences.getInstance();
final int? storedColorValue = prefs.getInt(_themeSeedColorKey);
if (storedColorValue == null || !mounted) {
return;
}
setState(() {
_themeSeedColor = Color(storedColorValue);
_updateThemeData();
});
}
Future<void> _setThemeSeedColor(Color color) async {
final SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setInt(_themeSeedColorKey, color.value);
if (!mounted) {
return;
}
setState(() {
_themeSeedColor = color;
_updateThemeData();
});
}
Future<void> _loadThemeMode() async {
final SharedPreferences prefs = await SharedPreferences.getInstance();
final int? stored = prefs.getInt(_themeModeKey);
if (!mounted) return;
setState(() {
if (stored == null) {
_themeMode = ThemeMode.system;
} else if (stored == 1) {
_themeMode = ThemeMode.light;
} else if (stored == 2) {
_themeMode = ThemeMode.dark;
} else {
_themeMode = ThemeMode.system;
}
_updateThemeData();
});
}
Future<void> _setThemeMode(ThemeMode mode) async {
final SharedPreferences prefs = await SharedPreferences.getInstance();
final int stored = mode == ThemeMode.system
? 0
: (mode == ThemeMode.light ? 1 : 2);
await prefs.setInt(_themeModeKey, stored);
if (!mounted) return;
setState(() {
_themeMode = mode;
});
}
void _updateThemeData() {
_lightTheme = AppTheme.theme(
seedColor: _themeSeedColor,
brightness: Brightness.light,
);
_darkTheme = AppTheme.theme(
seedColor: _themeSeedColor,
brightness: Brightness.dark,
);
// Updated light/dark themes regenerated
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (_isUnlocking) {
return;
}
switch (state) {
case AppLifecycleState.resumed:
_cancelBiometricLockTimer();
return;
case AppLifecycleState.inactive:
case AppLifecycleState.paused:
_scheduleBiometricLockTimer();
return;
case AppLifecycleState.detached:
case AppLifecycleState.hidden:
return;
}
}
Future<void> _bootstrapVault() async {
try {
final bool hasEncryptionKey = await _vaultService.hasEncryptionKey();
_biometricGateEnabled =
hasEncryptionKey && await _vaultService.isBiometricGateEnabled();
if (!hasEncryptionKey) {
_pendingEncryptionKey = null;
if (mounted) {
setState(() {
_phase = _AppPhase.access;
});
}
return;
}
final bool accessCompleted = await _vaultService.isVaultAccessCompleted();
final bool biometricChoicePending = await _vaultService
.isBiometricChoicePending();
if (!accessCompleted) {
_pendingEncryptionKey = await _vaultService
.readStoredEncryptionKeyRaw();
if (mounted) {
setState(() {
_phase = _AppPhase.access;
});
}
return;
}
if (biometricChoicePending) {
_pendingEncryptionKey = await _vaultService
.readStoredEncryptionKeyRaw();
if (mounted) {
setState(() {
_phase = _AppPhase.biometricChoice;
});
}
return;
}
if (_biometricGateEnabled) {
if (mounted) {
setState(() {
_phase = _AppPhase.biometricGate;
});
}
return;
}
final String? encryptionKey = await _vaultService
.readStoredEncryptionKeyRaw();
if (encryptionKey != null) {
await _openVault(encryptionKey);
} else if (mounted) {
setState(() {
_phase = _AppPhase.access;
});
}
} finally {
if (mounted) {
setState(() {
_isBootstrapping = false;
});
}
}
}
Future<void> _openVault(String encryptionKey) async {
await _database?.close();
try {
final AppDatabase database = AppDatabase(encryptionKey: encryptionKey);
if (!mounted) {
await database.close();
return;
}
setState(() {
_database = database;
_repository = NoteRepository(
database: database,
authApi: AuthApi.instance,
masterKey: encryptionKey,
);
_phase = _AppPhase.notes;
});
// Start periodic sync
_startPeriodicSync();
// Run an initial sync immediately and let the repository use the
// stored lastSyncAt when it exists.
unawaited(_performSync());
} catch (e) {
// If the database file is not a valid SQLite DB (e.g., wrong key or corruption),
// reset the local vault so the app doesn't crash. The reset will delete DB files
// and clear the stored encryption key.
await _resetLocalVaultData();
if (mounted) {
_scaffoldMessengerKey.currentState?.showSnackBar(
const SnackBar(
content: Text(
'El vault local estaba corrupto y ha sido reiniciado.',
),
),
);
}
}
}
Future<void> _beginInitialVaultFlow({String? actionLabel}) async {
if (_isUnlocking) {
return;
}
setState(() {
_isUnlocking = true;
});
try {
if (actionLabel != null) {
_showAccountPlaceholder(actionLabel);
}
final String? existingKey = await _vaultService
.readStoredEncryptionKeyRaw();
final String encryptionKey =
existingKey ?? await _vaultService.createEncryptionKey();
_pendingEncryptionKey = encryptionKey;
await _vaultService.setVaultAccessCompleted(true);
await _vaultService.setBiometricChoicePending(true);
await _vaultService.setBiometricGateEnabled(false);
if (mounted) {
setState(() {
_phase = _AppPhase.biometricChoice;
_biometricGateEnabled = false;
});
}
} catch (error) {
_scaffoldMessengerKey.currentState?.showSnackBar(
SnackBar(content: Text('No se pudo preparar el vault local: $error')),
);
} finally {
if (mounted) {
setState(() {
_isUnlocking = false;
_isBootstrapping = false;
});
}
}
}
Future<void> _beginRemoteVaultFlow({
required String username,
required String password,
required bool isRegister,
}) async {
if (_isUnlocking) {
return;
}
setState(() {
_isUnlocking = true;
});
try {
if (isRegister) {
final String encryptionKey = _vaultService.generateEncryptionKey();
final String encryptedMasterKey = await AuthApi.instance
.encryptWithPassword(encryptionKey, password);
final Map<String, dynamic> response = await AuthApi.instance.register(
username,
password,
encryptedMasterKey: encryptedMasterKey,
);
if (response['error'] == true) {
throw StateError('No se pudo registrar el usuario.');
}
await _vaultService.storeEncryptionKey(encryptionKey);
_pendingEncryptionKey = encryptionKey;
} else {
final Map<String, dynamic> response = await AuthApi.instance.login(
username,
password,
);
if (response['error'] == true) {
throw StateError('No se pudo iniciar sesión.');
}
final String? encryptedMasterKey =
(response['encrypted_master_key'] as String?) ??
(response['encryptedMasterKey'] as String?);
if (encryptedMasterKey == null || encryptedMasterKey.isEmpty) {
throw StateError('La API no devolvió la clave de encriptación.');
}
final String encryptionKey = await AuthApi.instance.decryptWithPassword(
encryptedMasterKey,
password,
);
await _vaultService.storeEncryptionKey(encryptionKey);
_pendingEncryptionKey = encryptionKey;
}
await _vaultService.setVaultAccessCompleted(true);
await _vaultService.setBiometricChoicePending(true);
await _vaultService.setBiometricGateEnabled(false);
if (mounted) {
setState(() {
_phase = _AppPhase.biometricChoice;
_biometricGateEnabled = false;
});
}
} catch (error) {
_scaffoldMessengerKey.currentState?.showSnackBar(
SnackBar(
content: Text('No se pudo completar la autenticación: $error'),
),
);
} finally {
if (mounted) {
setState(() {
_isUnlocking = false;
_isBootstrapping = false;
});
}
}
}
Future<void> _enterWithoutAccount() {
return _beginInitialVaultFlow();
}
Future<void> _completeBiometricChoice({
required bool enableBiometrics,
}) async {
if (_isUnlocking) {
return;
}
setState(() {
_isUnlocking = true;
});
try {
final String? pendingKey =
_pendingEncryptionKey ??
await _vaultService.readStoredEncryptionKeyRaw();
if (pendingKey == null) {
throw StateError('No se encontró la llave local.');
}
if (enableBiometrics) {
final bool available = await _vaultService.isBiometricAvailable();
if (available) {
bool activated = await _vaultService.enableBiometricProtection();
while (!activated) {
// Ask the user to retry or skip
final BuildContext? dialogCtx = _navigatorKey.currentContext;
if (dialogCtx == null) {
break;
}
final NavigatorState navigator = Navigator.of(dialogCtx);
final bool? retry = await showDialog<bool>(
context: dialogCtx,
builder: (BuildContext context) {
final AppPalette palette = _activePalette();
return AlertDialog(
backgroundColor: palette.cardBackground,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(color: palette.border),
),
title: const Text('No se pudo activar la biometría'),
content: const Text(
'No se pudo activar la biometría. ¿Quieres intentarlo de nuevo o entrar sin huella?',
),
actions: [
TextButton(
onPressed: () => navigator.pop(false),
child: const Text('Entrar sin huella'),
),
FilledButton(
onPressed: () => navigator.pop(true),
child: const Text('Reintentar'),
),
],
);
},
);
if (retry != true) {
// User chose to skip biometric activation
break;
}
activated = await _vaultService.enableBiometricProtection();
}
if (activated) {
await _vaultService.setBiometricChoicePending(false);
await _vaultService.setVaultAccessCompleted(true);
_biometricGateEnabled = true;
_pendingEncryptionKey = pendingKey;
await _openVault(pendingKey);
return;
}
}
_scaffoldMessengerKey.currentState?.showSnackBar(
const SnackBar(
content: Text(
'La biometría no está disponible en este dispositivo.',
),
),
);
return;
}
await _vaultService.setBiometricGateEnabled(false);
await _vaultService.setBiometricChoicePending(false);
await _vaultService.setVaultAccessCompleted(true);
_biometricGateEnabled = false;
_pendingEncryptionKey = pendingKey;
await _openVault(pendingKey);
} catch (error) {
_scaffoldMessengerKey.currentState?.showSnackBar(
SnackBar(
content: Text(
'No se pudo finalizar la configuración del vault: $error',
),
),
);
} finally {
if (mounted) {
setState(() {
_isUnlocking = false;
_isBootstrapping = false;
});
}
}
}
Future<void> _unlockBiometricGate() async {
if (_isUnlocking) {
return;
}
setState(() {
_isUnlocking = true;
});
try {
final String? encryptionKey = await _vaultService.readEncryptionKey();
if (encryptionKey != null) {
await _openVault(encryptionKey);
}
} catch (error) {
_scaffoldMessengerKey.currentState?.showSnackBar(
SnackBar(content: Text('No se pudo desbloquear el vault: $error')),
);
} finally {
if (mounted) {
setState(() {
_isUnlocking = false;
_isBootstrapping = false;
});
}
}
}
void _openSettings() {
if (!mounted) {
return;
}
setState(() {
_currentSection = _AppSection.settings;
});
}
void _openHome() {
if (!mounted) {
return;
}
setState(() {
_currentSection = _AppSection.home;
});
}
Future<void> _resetLocalVaultData() async {
final AppDatabase? database = _database;
setState(() {
_repository = null;
_database = null;
_isBootstrapping = true;
_phase = _AppPhase.loading;
});
await database?.close();
await _vaultService.clearEncryptionKey();
await AuthApi.instance.clearTokens();
final SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.clear();
final Directory supportDir = await getApplicationSupportDirectory();
final String dbPath = p.join(supportDir.path, 'notes.sqlite');
final List<String> filesToDelete = <String>[
dbPath,
'$dbPath-wal',
'$dbPath-shm',
'$dbPath-journal',
];
for (final String filePath in filesToDelete) {
final File file = File(filePath);
if (await file.exists()) {
await file.delete();
}
}
if (!mounted) {
return;
}
setState(() {
_isBootstrapping = false;
_isUnlocking = false;
_biometricGateEnabled = false;
_pendingEncryptionKey = null;
_phase = _AppPhase.access;
});
}
Future<void> _lockVault() async {
final AppDatabase? database = _database;
if (database == null && _repository == null) {
return;
}
await database?.close();
if (!mounted) {
return;
}
setState(() {
_database = null;
_repository = null;
_isBootstrapping = false;
_biometricGateSession += 1;
_phase = _AppPhase.biometricGate;
_currentSection = _AppSection.home;
});
}
bool get _needsBiometricLock => _biometricGateEnabled && _repository != null;
void _cancelBiometricLockTimer() {
_biometricLockTimer?.cancel();
_biometricLockTimer = null;
}
void _scheduleBiometricLockTimer() {
if (!_needsBiometricLock) {
return;
}
_biometricLockTimer?.cancel();
_biometricLockTimer = Timer(_biometricInactivityTimeout, () {
if (!mounted || !_needsBiometricLock) {
return;
}
unawaited(_lockVault());
});
}
Future<void> _allowWindowClose() async {
if (!isDesktop) {
return;
}
await windowManager.setPreventClose(false);
await windowManager.close();
}
void _showAccountPlaceholder(String actionLabel) {
_scaffoldMessengerKey.currentState?.showSnackBar(
SnackBar(
content: Text(
'$actionLabel todavía no está conectado con la API. Usa "Entrar sin cuenta" para empezar en local.',
),
),
);
}
Future<void> _saveWindowSize() async {
if (await windowManager.isFullScreen()) {
return;
}
if (await windowManager.isMaximized()) {
return;
}
final Size currentSize = await windowManager.getSize();
await WindowStateStore.instance.saveWindowSize(currentSize);
}
void _scheduleWindowSizeSave() {
if (!isDesktop) {
return;
}
_windowSizeSaveTimer?.cancel();
_windowSizeSaveTimer = Timer(_windowSizeSaveDelay, () {
_windowSizeSaveTimer = null;
if (!mounted) {
return;
}
unawaited(_saveWindowSize());
});
}
void _startPeriodicSync() {
_syncTimer?.cancel();
_syncTimer = Timer.periodic(_syncInterval, (_) {
_performSync();
});
}
Future<void> _performSync({bool forceFull = false}) async {
if (_repository == null) {
return;
}
if (!mounted) {
return;
}
if (_syncStatus == SyncStatus.preparing ||
_syncStatus == SyncStatus.encrypting ||
_syncStatus == SyncStatus.uploading ||
_syncStatus == SyncStatus.waitingResponse ||
_syncStatus == SyncStatus.decrypting ||
_syncStatus == SyncStatus.syncing) {
return;
}
final int syncOperationId = ++_syncOperationId;
setState(() {
_syncStatus = SyncStatus.preparing;
_syncProgress = null;
_syncDetailMessage = 'Preparando sincronización...';
_syncErrorMessage = null;
});
void updateSyncState(
SyncStatus status, {
double? progress,
String? message,
}) {
if (!mounted || syncOperationId != _syncOperationId) {
return;
}
setState(() {
_syncStatus = status;
_syncProgress = progress;
_syncDetailMessage = message;
if (status != SyncStatus.error) {
_syncErrorMessage = null;
}
});
}
try {
final Map<String, dynamic> result = await _repository!.performSync(
forceFull: forceFull,
onProgress: updateSyncState,
);
if (!mounted) {
return;
}
if (result['error'] == true) {
setState(() {
_syncStatus = SyncStatus.error;
_syncErrorMessage = result['message'] as String?;
_syncProgress = null;
_syncDetailMessage = null;
});
} else {
setState(() {
_syncStatus = SyncStatus.synced;
_syncErrorMessage = null;
_syncProgress = null;
_syncDetailMessage = 'Sincronización completada';
_homeRefreshToken += 1;
});
// Reset to idle after 3 seconds
Future<void>.delayed(const Duration(seconds: 3), () {
if (mounted && syncOperationId == _syncOperationId) {
setState(() {
_syncStatus = SyncStatus.idle;
_syncProgress = null;
_syncDetailMessage = null;
});
}
});
}
} catch (e, st) {
if (!mounted) {
return;
}
setState(() {
_syncStatus = SyncStatus.error;
_syncErrorMessage = '$e\n\nStackTrace: $st';
_syncProgress = null;
_syncDetailMessage = null;
});
}
}
Widget _buildLoadingScreen() {
return const Scaffold(
body: SafeArea(
child: Column(
children: [
Expanded(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('Preparando el vault local...'),
],
),
),
),
],
),
),
);
}
Widget _buildAppShell({required Widget home}) {
return home;
}
Widget _buildMainShell(NoteRepository repository) {
final AppPalette palette = _activePalette();
final Widget activeScreen = _currentSection == _AppSection.home
? HomeScreen(
key: const ValueKey<String>('home-screen'),
repository: repository,
onOpenSettings: _openSettings,
onRequestSync: _performSync,
onVaultInvalid: _resetLocalVaultData,
syncStatus: _syncStatus,
syncProgress: _syncProgress,
syncDetailMessage: _syncDetailMessage,
syncErrorMessage: _syncErrorMessage,
refreshToken: _homeRefreshToken,
)
: SettingsScreen(
key: const ValueKey<String>('settings-screen'),
onDeleteAllData: _resetLocalVaultData,
onBackToHome: _openHome,
onForceSync: () => _performSync(forceFull: true),
currentSeedColor: _themeSeedColor,
onThemeColorSelected: _setThemeSeedColor,
currentThemeMode: _themeMode,
onThemeModeSelected: _setThemeMode,
);
return Shortcuts(
shortcuts: <LogicalKeySet, Intent>{
LogicalKeySet(LogicalKeyboardKey.f5): const PerformSyncIntent(),
},
child: Actions(
actions: <Type, Action<Intent>>{
PerformSyncIntent: CallbackAction<PerformSyncIntent>(
onInvoke: (PerformSyncIntent intent) {
_performSync();
return null;
},
),
},
child: Focus(
autofocus: true,
child: Scaffold(
body: Container(
decoration: BoxDecoration(gradient: palette.backdropGradient),
child: SafeArea(
child: Column(
children: [
Expanded(
child: AnimatedSwitcher(
duration: _screenTransitionDuration,
switchInCurve: Curves.easeOutCubic,
switchOutCurve: Curves.easeInCubic,
transitionBuilder:
(Widget child, Animation<double> animation) {
final Animation<Offset> offsetAnimation =
Tween<Offset>(
begin: const Offset(0.08, 0.0),
end: Offset.zero,
).animate(animation);
return FadeTransition(
opacity: animation,
child: SlideTransition(
position: offsetAnimation,
child: child,
),
);
},
child: activeScreen,
),
),
],
),
),
),
),
),
),
);
}
@override
void onWindowResize() {
_scheduleWindowSizeSave();
}
@override
void onWindowResized() {
_scheduleWindowSizeSave();
}
@override
void onWindowFocus() {
_cancelBiometricLockTimer();
}
@override
void onWindowBlur() {
_scheduleBiometricLockTimer();
}
@override
void onWindowClose() {
if (_isHandlingWindowClose) {
return;
}
_windowSizeSaveTimer?.cancel();
_windowSizeSaveTimer = null;
unawaited(_saveWindowSize());
if (!_needsBiometricLock) {
unawaited(_allowWindowClose());
return;
}
_isHandlingWindowClose = true;
_cancelBiometricLockTimer();
unawaited(() async {
try {
final String? encryptionKey = await _vaultService.readEncryptionKey();
if (encryptionKey == null) {
return;
}
await _allowWindowClose();
} finally {
_isHandlingWindowClose = false;
}
}());
}
@override
Widget build(BuildContext context) {
Widget homeWidget;
if (_isBootstrapping) {
homeWidget = _buildLoadingScreen();
} else {
final NoteRepository? repository = _repository;
if (repository != null) {
homeWidget = _buildMainShell(repository);
} else {
switch (_phase) {
case _AppPhase.loading:
homeWidget = _buildLoadingScreen();
break;
case _AppPhase.access:
homeWidget = _buildAppShell(
home: VaultAccessScreen(
isBusy: _isUnlocking,
onCreateAccountPressed: (String email, String password) async {
await _beginRemoteVaultFlow(
username: email,
password: password,
isRegister: true,
);
},
onSignInPressed: (String email, String password) async {
await _beginRemoteVaultFlow(
username: email,
password: password,
isRegister: false,
);
},
onContinueWithoutAccount: _enterWithoutAccount,
),
);
break;
case _AppPhase.biometricChoice:
homeWidget = _buildAppShell(
home: BiometricChoiceScreen(
isBusy: _isUnlocking,
onEnableBiometrics: () =>
_completeBiometricChoice(enableBiometrics: true),
onSkipBiometrics: () =>
_completeBiometricChoice(enableBiometrics: false),
),
);
break;
case _AppPhase.biometricGate:
homeWidget = _buildAppShell(
home: BiometricGateScreen(
key: ValueKey<int>(_biometricGateSession),
isBusy: _isUnlocking,
onUnlockRequested: _unlockBiometricGate,
),
);
break;
case _AppPhase.notes:
homeWidget = _buildLoadingScreen();
break;
}
}
}
return MaterialApp(
navigatorKey: _navigatorKey,
title: 'Mis Notas',
debugShowCheckedModeBanner: false,
scaffoldMessengerKey: _scaffoldMessengerKey,
theme:
_lightTheme ??
AppTheme.theme(
seedColor: _themeSeedColor,
brightness: Brightness.light,
),
darkTheme:
_darkTheme ??
AppTheme.theme(
seedColor: _themeSeedColor,
brightness: Brightness.dark,
),
themeMode: _themeMode,
home: homeWidget,
);
}
}