import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_quill/flutter_quill.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 createState() => _NotesAppState(); } class _NotesAppState extends State 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 _scaffoldMessengerKey = GlobalKey(); final GlobalKey _navigatorKey = GlobalKey(); 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; bool _isSyncBannerVisible() { switch (_syncStatus) { case SyncStatus.preparing: case SyncStatus.encrypting: case SyncStatus.uploading: case SyncStatus.waitingResponse: case SyncStatus.decrypting: case SyncStatus.syncing: case SyncStatus.synced: case SyncStatus.error: return true; case SyncStatus.idle: return false; } } Widget _buildSyncBanner(BuildContext context) { if (!_isSyncBannerVisible()) { return const SizedBox.shrink(); } final AppPalette palette = _activePalette(); final String message = _syncErrorMessage ?? _syncDetailMessage ?? 'Sincronizando...'; final double? progress = _syncProgress; final IconData icon; final Color accentColor; switch (_syncStatus) { case SyncStatus.preparing: case SyncStatus.encrypting: case SyncStatus.uploading: case SyncStatus.waitingResponse: case SyncStatus.decrypting: case SyncStatus.syncing: icon = Icons.cloud_sync_outlined; accentColor = palette.textSecondary; break; case SyncStatus.synced: icon = Icons.check_circle; accentColor = palette.success; break; case SyncStatus.error: icon = Icons.error; accentColor = palette.destructiveAccent; break; case SyncStatus.idle: icon = Icons.cloud_sync_outlined; accentColor = palette.textSecondary; break; } return Material( color: palette.surfaceElevated, elevation: 12, child: SafeArea( top: false, child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), decoration: BoxDecoration( border: Border( top: BorderSide(color: accentColor.withValues(alpha: 0.45)), ), ), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Row( children: [ Icon(icon, color: accentColor, size: 18), const SizedBox(width: 10), Expanded( child: Text( message, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( color: palette.textPrimary, fontSize: 13, fontWeight: FontWeight.w600, ), ), ), ], ), if (_syncStatus == SyncStatus.preparing || _syncStatus == SyncStatus.encrypting || _syncStatus == SyncStatus.uploading || _syncStatus == SyncStatus.waitingResponse || _syncStatus == SyncStatus.decrypting || _syncStatus == SyncStatus.syncing) ...[ const SizedBox(height: 8), ClipRRect( borderRadius: BorderRadius.circular(999), child: LinearProgressIndicator( minHeight: 4, value: progress, backgroundColor: palette.borderMuted, ), ), ], ], ), ), ), ); } 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 _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 _setThemeSeedColor(Color color) async { final SharedPreferences prefs = await SharedPreferences.getInstance(); await prefs.setInt(_themeSeedColorKey, color.toARGB32()); if (!mounted) { return; } setState(() { _themeSeedColor = color; _updateThemeData(); }); } Future _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 _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 _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 _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 _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 _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 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 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 _enterWithoutAccount() { return _beginInitialVaultFlow(); } Future _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 - pass currentContext directly in builder final NavigatorState? navigator = _navigatorKey.currentState; if (navigator == null) break; if (!mounted) return; final bool? retry = await showDialog( context: context, builder: (BuildContext dialogContext) { 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 _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 _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 filesToDelete = [ 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 _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 _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 _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 _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 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; }); // Keep the completion state visible briefly so it can be read. Future.delayed(const Duration(seconds: 1), () { 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('home-screen'), repository: repository, onOpenSettings: _openSettings, onRequestSync: _performSync, onVaultInvalid: _resetLocalVaultData, syncStatus: _syncStatus, syncProgress: _syncProgress, syncDetailMessage: _syncDetailMessage, syncErrorMessage: _syncErrorMessage, refreshToken: _homeRefreshToken, ) : SettingsScreen( key: const ValueKey('settings-screen'), onDeleteAllData: _resetLocalVaultData, onBackToHome: _openHome, onForceSync: () => _performSync(forceFull: true), currentSeedColor: _themeSeedColor, onThemeColorSelected: _setThemeSeedColor, currentThemeMode: _themeMode, onThemeModeSelected: _setThemeMode, ); return Shortcuts( shortcuts: { LogicalKeySet(LogicalKeyboardKey.f5): const PerformSyncIntent(), }, child: Actions( actions: >{ PerformSyncIntent: CallbackAction( 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 animation) { final Animation offsetAnimation = Tween( begin: const Offset(0.08, 0.0), end: Offset.zero, ).animate(animation); return FadeTransition( opacity: animation, child: SlideTransition( position: offsetAnimation, child: child, ), ); }, child: activeScreen, ), ), AnimatedSwitcher( duration: const Duration(milliseconds: 180), switchInCurve: Curves.easeOutCubic, switchOutCurve: Curves.easeInCubic, transitionBuilder: (Widget child, Animation animation) { final Animation slideAnimation = Tween( begin: const Offset(0.0, 0.35), end: Offset.zero, ).animate(animation); return FadeTransition( opacity: animation, child: SlideTransition( position: slideAnimation, child: child, ), ); }, child: _buildSyncBanner(context), ), ], ), ), ), ), ), ), ); } @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(_biometricGateSession), isBusy: _isUnlocking, onUnlockRequested: _unlockBiometricGate, ), ); break; case _AppPhase.notes: homeWidget = _buildLoadingScreen(); break; } } } return MaterialApp( navigatorKey: _navigatorKey, title: 'Mis Notas', debugShowCheckedModeBanner: false, scaffoldMessengerKey: _scaffoldMessengerKey, localizationsDelegates: const >[ GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, FlutterQuillLocalizations.delegate, ], theme: _lightTheme ?? AppTheme.theme( seedColor: _themeSeedColor, brightness: Brightness.light, ), darkTheme: _darkTheme ?? AppTheme.theme( seedColor: _themeSeedColor, brightness: Brightness.dark, ), themeMode: _themeMode, home: homeWidget, ); } }