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_theme.dart'; import 'package:notas/widgets/app_title_bar.dart'; import 'package:notas/widgets/sync_status_indicator.dart'; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.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); 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? _syncTimer; bool _isHandlingWindowClose = false; _AppPhase _phase = _AppPhase.loading; _AppSection _currentSection = _AppSection.home; SyncStatus _syncStatus = SyncStatus.idle; String? _syncErrorMessage; @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); if (isDesktop) { windowManager.addListener(this); windowManager.setPreventClose(true); } _bootstrapVault(); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); if (isDesktop) { windowManager.removeListener(this); windowManager.setPreventClose(false); } _biometricLockTimer?.cancel(); _syncTimer?.cancel(); _database?.close(); super.dispose(); } @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 full sync immediately to pull server changes unawaited(_performSync(forceFull: true)); } 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 final BuildContext? dialogCtx = _navigatorKey.currentContext; if (dialogCtx == null) { break; } final NavigatorState navigator = Navigator.of(dialogCtx); final bool? retry = await showDialog( context: dialogCtx, builder: (BuildContext context) => AlertDialog( 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(); 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 _startPeriodicSync() { _syncTimer?.cancel(); _syncTimer = Timer.periodic(_syncInterval, (_) { _performSync(); }); } Future _performSync({bool forceFull = false}) async { if (_repository == null) { return; } if (!mounted) { return; } setState(() { _syncStatus = SyncStatus.syncing; _syncErrorMessage = null; }); try { final Map result = await _repository!.performSync(forceFull: forceFull); if (!mounted) { return; } if (result['error'] == true) { setState(() { _syncStatus = SyncStatus.error; _syncErrorMessage = result['message'] as String?; }); } else { setState(() { _syncStatus = SyncStatus.synced; _syncErrorMessage = null; }); // Reset to idle after 3 seconds Future.delayed(const Duration(seconds: 3), () { if (mounted) { setState(() { _syncStatus = SyncStatus.idle; }); } }); } } catch (e) { if (!mounted) { return; } setState(() { _syncStatus = SyncStatus.error; _syncErrorMessage = e.toString(); }); } } Widget _buildLoadingScreen() { return MaterialApp( navigatorKey: _navigatorKey, title: 'Mis Notas', debugShowCheckedModeBanner: false, theme: AppTheme.theme, home: const Scaffold( body: SafeArea( child: Column( children: [ AppTitleBar(), Expanded( child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ CircularProgressIndicator(), SizedBox(height: 16), Text('Preparando el vault local...'), ], ), ), ), ], ), ), ), ); } Widget _buildAppShell({required Widget home}) { return MaterialApp( navigatorKey: _navigatorKey, title: 'Mis Notas', debugShowCheckedModeBanner: false, scaffoldMessengerKey: _scaffoldMessengerKey, theme: AppTheme.theme, home: home, ); } Widget _buildMainShell(NoteRepository repository) { final Widget activeScreen = _currentSection == _AppSection.home ? HomeScreen( key: const ValueKey('home-screen'), repository: repository, onOpenSettings: _openSettings, onVaultInvalid: _resetLocalVaultData, syncStatus: _syncStatus, syncErrorMessage: _syncErrorMessage, ) : SettingsScreen( key: const ValueKey('settings-screen'), onDeleteAllData: _resetLocalVaultData, onBackToHome: _openHome, ); return MaterialApp( navigatorKey: _navigatorKey, title: 'Mis Notas', debugShowCheckedModeBanner: false, scaffoldMessengerKey: _scaffoldMessengerKey, theme: AppTheme.theme, home: 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: const BoxDecoration( gradient: LinearGradient( colors: [ Color(0xFF191A1D), Color(0xFF222326), Color(0xFF101114), ], begin: Alignment.topLeft, end: Alignment.bottomRight, ), ), child: SafeArea( child: Column( children: [ const AppTitleBar(), 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, ), ), ], ), ), ), ), ), ), ), ); } @override void onWindowResize() { _saveWindowSize(); } @override void onWindowResized() { _saveWindowSize(); } @override void onWindowFocus() { _cancelBiometricLockTimer(); } @override void onWindowBlur() { _scheduleBiometricLockTimer(); } @override void onWindowClose() { if (_isHandlingWindowClose) { return; } 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) { if (_isBootstrapping) { return _buildLoadingScreen(); } final NoteRepository? repository = _repository; if (repository != null) { return _buildMainShell(repository); } switch (_phase) { case _AppPhase.loading: return _buildLoadingScreen(); case _AppPhase.access: return _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, ), ); case _AppPhase.biometricChoice: return _buildAppShell( home: BiometricChoiceScreen( isBusy: _isUnlocking, onEnableBiometrics: () => _completeBiometricChoice(enableBiometrics: true), onSkipBiometrics: () => _completeBiometricChoice(enableBiometrics: false), ), ); case _AppPhase.biometricGate: return _buildAppShell( home: BiometricGateScreen( key: ValueKey(_biometricGateSession), isBusy: _isUnlocking, onUnlockRequested: _unlockBiometricGate, ), ); case _AppPhase.notes: if (repository == null) { return _buildLoadingScreen(); } return _buildMainShell(repository); } } }