diff --git a/lib/app.dart b/lib/app.dart index 858688c..a19a17d 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -16,24 +16,15 @@ 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: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 _AppSection { home, settings } -enum _AppPhase { - loading, - access, - biometricChoice, - biometricGate, - notes, -} +enum _AppPhase { loading, access, biometricChoice, biometricGate, notes } class PerformSyncIntent extends Intent { const PerformSyncIntent(); @@ -71,7 +62,10 @@ class _NotesAppState extends State _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; @@ -150,7 +144,8 @@ class _NotesAppState extends State Future _bootstrapVault() async { try { final bool hasEncryptionKey = await _vaultService.hasEncryptionKey(); - _biometricGateEnabled = hasEncryptionKey && await _vaultService.isBiometricGateEnabled(); + _biometricGateEnabled = + hasEncryptionKey && await _vaultService.isBiometricGateEnabled(); if (!hasEncryptionKey) { _pendingEncryptionKey = null; @@ -163,10 +158,12 @@ class _NotesAppState extends State } final bool accessCompleted = await _vaultService.isVaultAccessCompleted(); - final bool biometricChoicePending = await _vaultService.isBiometricChoicePending(); + final bool biometricChoicePending = await _vaultService + .isBiometricChoicePending(); if (!accessCompleted) { - _pendingEncryptionKey = await _vaultService.readStoredEncryptionKeyRaw(); + _pendingEncryptionKey = await _vaultService + .readStoredEncryptionKeyRaw(); if (mounted) { setState(() { _phase = _AppPhase.access; @@ -176,7 +173,8 @@ class _NotesAppState extends State } if (biometricChoicePending) { - _pendingEncryptionKey = await _vaultService.readStoredEncryptionKeyRaw(); + _pendingEncryptionKey = await _vaultService + .readStoredEncryptionKeyRaw(); if (mounted) { setState(() { _phase = _AppPhase.biometricChoice; @@ -194,7 +192,8 @@ class _NotesAppState extends State return; } - final String? encryptionKey = await _vaultService.readStoredEncryptionKeyRaw(); + final String? encryptionKey = await _vaultService + .readStoredEncryptionKeyRaw(); if (encryptionKey != null) { await _openVault(encryptionKey); } else if (mounted) { @@ -226,7 +225,7 @@ class _NotesAppState extends State _repository = NoteRepository( database: database, authApi: AuthApi.instance, - masterKey: encryptionKey, + masterKey: encryptionKey, ); _phase = _AppPhase.notes; }); @@ -244,7 +243,11 @@ class _NotesAppState extends State if (mounted) { _scaffoldMessengerKey.currentState?.showSnackBar( - const SnackBar(content: Text('El vault local estaba corrupto y ha sido reiniciado.')), + const SnackBar( + content: Text( + 'El vault local estaba corrupto y ha sido reiniciado.', + ), + ), ); } } @@ -264,8 +267,10 @@ class _NotesAppState extends State _showAccountPlaceholder(actionLabel); } - final String? existingKey = await _vaultService.readStoredEncryptionKeyRaw(); - final String encryptionKey = existingKey ?? await _vaultService.createEncryptionKey(); + final String? existingKey = await _vaultService + .readStoredEncryptionKeyRaw(); + final String encryptionKey = + existingKey ?? await _vaultService.createEncryptionKey(); _pendingEncryptionKey = encryptionKey; await _vaultService.setVaultAccessCompleted(true); @@ -308,8 +313,8 @@ class _NotesAppState extends State try { if (isRegister) { final String encryptionKey = _vaultService.generateEncryptionKey(); - final String encryptedMasterKey = - await AuthApi.instance.encryptWithPassword(encryptionKey, password); + final String encryptedMasterKey = await AuthApi.instance + .encryptWithPassword(encryptionKey, password); final Map response = await AuthApi.instance.register( username, @@ -341,8 +346,10 @@ class _NotesAppState extends State throw StateError('La API no devolvió la clave de encriptación.'); } - final String encryptionKey = - await AuthApi.instance.decryptWithPassword(encryptedMasterKey, password); + final String encryptionKey = await AuthApi.instance.decryptWithPassword( + encryptedMasterKey, + password, + ); await _vaultService.storeEncryptionKey(encryptionKey); _pendingEncryptionKey = encryptionKey; @@ -360,7 +367,9 @@ class _NotesAppState extends State } } catch (error) { _scaffoldMessengerKey.currentState?.showSnackBar( - SnackBar(content: Text('No se pudo completar la autenticación: $error')), + SnackBar( + content: Text('No se pudo completar la autenticación: $error'), + ), ); } finally { if (mounted) { @@ -376,7 +385,9 @@ class _NotesAppState extends State return _beginInitialVaultFlow(); } - Future _completeBiometricChoice({required bool enableBiometrics}) async { + Future _completeBiometricChoice({ + required bool enableBiometrics, + }) async { if (_isUnlocking) { return; } @@ -386,7 +397,9 @@ class _NotesAppState extends State }); try { - final String? pendingKey = _pendingEncryptionKey ?? await _vaultService.readStoredEncryptionKeyRaw(); + final String? pendingKey = + _pendingEncryptionKey ?? + await _vaultService.readStoredEncryptionKeyRaw(); if (pendingKey == null) { throw StateError('No se encontró la llave local.'); @@ -409,10 +422,18 @@ class _NotesAppState extends State 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?'), + 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')), + TextButton( + onPressed: () => navigator.pop(false), + child: const Text('Entrar sin huella'), + ), + FilledButton( + onPressed: () => navigator.pop(true), + child: const Text('Reintentar'), + ), ], ), ); @@ -436,7 +457,11 @@ class _NotesAppState extends State } _scaffoldMessengerKey.currentState?.showSnackBar( - const SnackBar(content: Text('La biometría no está disponible en este dispositivo.')), + const SnackBar( + content: Text( + 'La biometría no está disponible en este dispositivo.', + ), + ), ); return; } @@ -449,7 +474,11 @@ class _NotesAppState extends State await _openVault(pendingKey); } catch (error) { _scaffoldMessengerKey.currentState?.showSnackBar( - SnackBar(content: Text('No se pudo finalizar la configuración del vault: $error')), + SnackBar( + content: Text( + 'No se pudo finalizar la configuración del vault: $error', + ), + ), ); } finally { if (mounted) { @@ -646,17 +675,48 @@ class _NotesAppState extends State return; } - if (_syncStatus == SyncStatus.syncing) { + 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.syncing; + _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); + final Map result = await _repository!.performSync( + forceFull: forceFull, + onProgress: updateSyncState, + ); if (!mounted) { return; @@ -666,19 +726,25 @@ class _NotesAppState extends State 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.delayed(const Duration(seconds: 3), () { - if (mounted) { + if (mounted && syncOperationId == _syncOperationId) { setState(() { _syncStatus = SyncStatus.idle; + _syncProgress = null; + _syncDetailMessage = null; }); } }); @@ -691,6 +757,8 @@ class _NotesAppState extends State setState(() { _syncStatus = SyncStatus.error; _syncErrorMessage = e.toString(); + _syncProgress = null; + _syncDetailMessage = null; }); } } @@ -745,6 +813,8 @@ class _NotesAppState extends State onRequestSync: _performSync, onVaultInvalid: _resetLocalVaultData, syncStatus: _syncStatus, + syncProgress: _syncProgress, + syncDetailMessage: _syncDetailMessage, syncErrorMessage: _syncErrorMessage, refreshToken: _homeRefreshToken, ) @@ -780,48 +850,53 @@ class _NotesAppState extends State 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, + 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, + ), + ), + ], + ), + ), + ), ), ), ), ), - ), - ), -), ); } @@ -913,8 +988,10 @@ class _NotesAppState extends State return _buildAppShell( home: BiometricChoiceScreen( isBusy: _isUnlocking, - onEnableBiometrics: () => _completeBiometricChoice(enableBiometrics: true), - onSkipBiometrics: () => _completeBiometricChoice(enableBiometrics: false), + onEnableBiometrics: () => + _completeBiometricChoice(enableBiometrics: true), + onSkipBiometrics: () => + _completeBiometricChoice(enableBiometrics: false), ), ); case _AppPhase.biometricGate: @@ -932,6 +1009,5 @@ class _NotesAppState extends State return _buildMainShell(repository); } - } -} \ No newline at end of file +} diff --git a/lib/data/note_repository.dart b/lib/data/note_repository.dart index 84b6b73..cdeb722 100644 --- a/lib/data/note_repository.dart +++ b/lib/data/note_repository.dart @@ -6,6 +6,7 @@ import 'package:notas/models/note.dart'; import 'package:notas/models/category.dart'; import 'package:notas/data/note_encryption.dart'; +import 'package:notas/widgets/sync_status.dart'; class NoteRepository { NoteRepository({ @@ -89,8 +90,17 @@ class NoteRepository { /// Sincroniza notas con el servidor. /// Requiere que el usuario esté autenticado (token válido). - Future> performSync({bool forceFull = false}) async { + Future> performSync({ + bool forceFull = false, + void Function(SyncStatus status, {double? progress, String? message})? + onProgress, + }) async { try { + onProgress?.call( + SyncStatus.preparing, + message: 'Preparando sincronización...', + ); + // Get last sync timestamp final DateTime? lastSync = await _authApi.getLastSyncAt(); final DateTime? lastSyncForRequest = forceFull @@ -113,10 +123,21 @@ class NoteRepository { ); } + final int totalNotesToEncrypt = unsyncedNotes.length; + // Build sync request (note: we send encrypted data, but locally we have plaintext) // Encrypt all notes before sending final List encryptedNotesPayload = []; - for (final dbNote in unsyncedNotes) { + if (totalNotesToEncrypt == 0) { + onProgress?.call( + SyncStatus.encrypting, + progress: 1.0, + message: 'No hay notas pendientes de encriptar.', + ); + } + + for (var index = 0; index < unsyncedNotes.length; index += 1) { + final DbNote dbNote = unsyncedNotes[index]; final note = _fromDbNote(dbNote); final encryptedTitle = await NoteEncryption.encryptNote( note.title, @@ -133,6 +154,13 @@ class NoteRepository { encryptedBody: encryptedBody, ), ); + + onProgress?.call( + SyncStatus.encrypting, + progress: (index + 1) / totalNotesToEncrypt, + message: + 'Encriptando notas para subir: ${index + 1} de $totalNotesToEncrypt', + ); } final List categoriesPayload = unsyncedCategories @@ -148,6 +176,10 @@ class NoteRepository { ); // Call sync API + onProgress?.call( + SyncStatus.uploading, + message: 'Subiendo datos al servidor...', + ); final Map syncResult = await _authApi.sync(syncRequest); if (syncResult['error'] == true) { @@ -157,7 +189,23 @@ class NoteRepository { final SyncResponse response = syncResult['data'] as SyncResponse; // Apply server changes to local database - await _applySyncResponse(response); + onProgress?.call( + SyncStatus.waitingResponse, + message: 'Esperando respuesta del servidor...', + ); + + await _applySyncResponse( + response, + onDecryptProgress: (int processed, int total) { + onProgress?.call( + SyncStatus.decrypting, + progress: total == 0 ? 1.0 : processed / total, + message: total == 0 + ? 'Desencriptando datos recibidos...' + : 'Desencriptando respuesta: $processed de $total', + ); + }, + ); // Update lastSyncAt await _authApi.setLastSyncAt(response.serverTimestamp); @@ -173,7 +221,10 @@ class NoteRepository { } } - Future _applySyncResponse(SyncResponse response) async { + Future _applySyncResponse( + SyncResponse response, { + void Function(int processed, int total)? onDecryptProgress, + }) async { // Apply categories from server for (final SyncCategoryResponse catResponse in response.changes.categories) { @@ -189,7 +240,9 @@ class NoteRepository { } // Apply notes from server - for (final SyncNoteResponse noteResponse in response.changes.notes) { + final int totalNotesToDecrypt = response.changes.notes.length; + for (var index = 0; index < response.changes.notes.length; index += 1) { + final SyncNoteResponse noteResponse = response.changes.notes[index]; final existingNote = await (_database.select( _database.notes, )..where((n) => n.uuid.equals(noteResponse.id))).getSingleOrNull(); @@ -245,6 +298,8 @@ class NoteRepository { ), ); } + + onDecryptProgress?.call(index + 1, totalNotesToDecrypt); } } diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index 9f3eb7e..e9b4ff4 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -10,6 +10,7 @@ import 'package:notas/screens/note_editor_screen.dart'; import 'package:notas/widgets/menu_drawer.dart'; import 'package:notas/widgets/note_card.dart'; import 'package:notas/widgets/search_app_bar.dart'; +import 'package:notas/widgets/sync_status.dart'; import 'package:notas/widgets/sync_status_indicator.dart'; class HomeScreen extends StatefulWidget { @@ -20,6 +21,8 @@ class HomeScreen extends StatefulWidget { required this.onRequestSync, this.onVaultInvalid, this.syncStatus = SyncStatus.idle, + this.syncProgress, + this.syncDetailMessage, this.syncErrorMessage, this.refreshToken = 0, }); @@ -29,6 +32,8 @@ class HomeScreen extends StatefulWidget { final Future Function() onRequestSync; final Future Function()? onVaultInvalid; final SyncStatus syncStatus; + final double? syncProgress; + final String? syncDetailMessage; final String? syncErrorMessage; final int refreshToken; @@ -272,7 +277,7 @@ class _HomeScreenState extends State { }); }, feedback: MouseRegion( - cursor: SystemMouseCursors.grabbing, + cursor: SystemMouseCursors.grabbing, child: Material( color: Colors.transparent, elevation: 8, @@ -280,7 +285,9 @@ class _HomeScreenState extends State { width: cellWidth, child: TweenAnimationBuilder( tween: Tween(begin: 0.97, end: 1.0), - duration: const Duration(milliseconds: 180), + duration: const Duration( + milliseconds: 180, + ), curve: Curves.easeOutCubic, builder: (context, scale, child) { return Transform.scale( @@ -302,14 +309,21 @@ class _HomeScreenState extends State { ), ), childWhenDragging: MouseRegion( - cursor: SystemMouseCursors.grabbing, + cursor: SystemMouseCursors.grabbing, child: Opacity( opacity: 0.3, child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: const Color.fromRGBO(24, 25, 26, 1), - borderRadius: BorderRadius.circular(12), + color: const Color.fromRGBO( + 24, + 25, + 26, + 1, + ), + borderRadius: BorderRadius.circular( + 12, + ), border: Border.all( color: Colors.white24, width: 1, @@ -388,7 +402,7 @@ class _HomeScreenState extends State { }); }, feedback: MouseRegion( - cursor: SystemMouseCursors.grabbing, + cursor: SystemMouseCursors.grabbing, child: Material( color: Colors.transparent, elevation: 8, @@ -396,7 +410,9 @@ class _HomeScreenState extends State { width: cellWidth, child: TweenAnimationBuilder( tween: Tween(begin: 0.97, end: 1.0), - duration: const Duration(milliseconds: 180), + duration: const Duration( + milliseconds: 180, + ), curve: Curves.easeOutCubic, builder: (context, scale, child) { return Transform.scale( @@ -418,14 +434,21 @@ class _HomeScreenState extends State { ), ), childWhenDragging: MouseRegion( - cursor: SystemMouseCursors.grabbing, + cursor: SystemMouseCursors.grabbing, child: Opacity( opacity: 0.3, child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - color: const Color.fromRGBO(24, 25, 26, 1), - borderRadius: BorderRadius.circular(12), + color: const Color.fromRGBO( + 24, + 25, + 26, + 1, + ), + borderRadius: BorderRadius.circular( + 12, + ), border: Border.all( color: Colors.white24, width: 1, @@ -525,6 +548,8 @@ class _HomeScreenState extends State { }, trailingWidget: SyncStatusIndicator( status: widget.syncStatus, + progress: widget.syncProgress, + detailMessage: widget.syncDetailMessage, errorMessage: widget.syncErrorMessage, onTap: widget.onRequestSync, ), diff --git a/lib/widgets/app_title_bar_stub.dart b/lib/widgets/app_title_bar_stub.dart index 206c99f..139473c 100644 --- a/lib/widgets/app_title_bar_stub.dart +++ b/lib/widgets/app_title_bar_stub.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:notas/widgets/sync_status_indicator.dart'; +import 'package:notas/widgets/sync_status.dart'; class AppTitleBar extends StatelessWidget { const AppTitleBar({ @@ -15,4 +15,4 @@ class AppTitleBar extends StatelessWidget { Widget build(BuildContext context) { return const SizedBox.shrink(); } -} \ No newline at end of file +} diff --git a/lib/widgets/sync_status.dart b/lib/widgets/sync_status.dart new file mode 100644 index 0000000..9e48bbf --- /dev/null +++ b/lib/widgets/sync_status.dart @@ -0,0 +1,11 @@ +enum SyncStatus { + idle, + preparing, + encrypting, + uploading, + waitingResponse, + decrypting, + syncing, + synced, + error, +} diff --git a/lib/widgets/sync_status_indicator.dart b/lib/widgets/sync_status_indicator.dart index c413fcb..d2205b4 100644 --- a/lib/widgets/sync_status_indicator.dart +++ b/lib/widgets/sync_status_indicator.dart @@ -1,21 +1,19 @@ import 'package:flutter/material.dart'; - -enum SyncStatus { - idle, - syncing, - synced, - error, -} +import 'package:notas/widgets/sync_status.dart'; class SyncStatusIndicator extends StatelessWidget { const SyncStatusIndicator({ required this.status, + this.progress, + this.detailMessage, this.errorMessage, this.onTap, super.key, }); final SyncStatus status; + final double? progress; + final String? detailMessage; final String? errorMessage; final VoidCallback? onTap; @@ -34,59 +32,154 @@ class SyncStatusIndicator extends StatelessWidget { ); } + String _messageForStatus() { + switch (status) { + case SyncStatus.idle: + return 'Sincronización en espera'; + case SyncStatus.preparing: + return detailMessage ?? 'Preparando sincronización...'; + case SyncStatus.encrypting: + return detailMessage ?? 'Encriptando datos para subir...'; + case SyncStatus.uploading: + return detailMessage ?? 'Subiendo datos al servidor...'; + case SyncStatus.waitingResponse: + return detailMessage ?? 'Esperando respuesta del servidor...'; + case SyncStatus.decrypting: + return detailMessage ?? 'Desencriptando datos recibidos...'; + case SyncStatus.syncing: + return detailMessage ?? 'Sincronizando...'; + case SyncStatus.synced: + return detailMessage ?? 'Sincronizado'; + case SyncStatus.error: + return errorMessage ?? detailMessage ?? 'Error al sincronizar'; + } + } + + Widget _buildStatusBadge({ + required IconData icon, + required Color color, + required bool determinate, + }) { + final double ringProgress = (progress ?? 0).clamp(0.0, 1.0); + + return SizedBox( + width: 18, + height: 18, + child: Stack( + alignment: Alignment.center, + children: [ + SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator( + strokeWidth: 2, + value: determinate ? ringProgress : null, + backgroundColor: color.withValues(alpha: 0.16), + valueColor: AlwaysStoppedAnimation(color), + ), + ), + Icon(icon, size: 10, color: color), + ], + ), + ); + } + @override Widget build(BuildContext context) { switch (status) { case SyncStatus.idle: return Tooltip( - message: 'Sincronización en espera', + message: _messageForStatus(), child: _buildIndicator( - const Icon( - Icons.cloud_outlined, - size: 16, - color: Colors.white38, + const Icon(Icons.cloud_outlined, size: 16, color: Colors.white38), + ), + ); + + case SyncStatus.preparing: + return Tooltip( + message: _messageForStatus(), + child: _buildIndicator( + _buildStatusBadge( + icon: Icons.sync, + color: const Color.fromARGB(255, 165, 165, 165), + determinate: false, + ), + ), + ); + + case SyncStatus.encrypting: + return Tooltip( + message: _messageForStatus(), + child: _buildIndicator( + _buildStatusBadge( + icon: Icons.cloud_upload_outlined, + color: const Color.fromARGB(255, 109, 191, 255), + determinate: true, + ), + ), + ); + + case SyncStatus.uploading: + return Tooltip( + message: _messageForStatus(), + child: _buildIndicator( + _buildStatusBadge( + icon: Icons.cloud_upload, + color: const Color.fromARGB(255, 98, 190, 255), + determinate: false, + ), + ), + ); + + case SyncStatus.waitingResponse: + return Tooltip( + message: _messageForStatus(), + child: _buildIndicator( + _buildStatusBadge( + icon: Icons.cloud_sync_outlined, + color: const Color.fromARGB(255, 150, 150, 150), + determinate: false, + ), + ), + ); + + case SyncStatus.decrypting: + return Tooltip( + message: _messageForStatus(), + child: _buildIndicator( + _buildStatusBadge( + icon: Icons.cloud_download_outlined, + color: const Color.fromARGB(255, 154, 194, 112), + determinate: false, ), ), ); case SyncStatus.syncing: return Tooltip( - message: 'Sincronizando...', + message: _messageForStatus(), child: _buildIndicator( - const SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation( - Color.fromARGB(255, 150, 150, 150), - ), - ), + _buildStatusBadge( + icon: Icons.sync, + color: const Color.fromARGB(255, 150, 150, 150), + determinate: false, ), ), ); case SyncStatus.synced: return Tooltip( - message: 'Sincronizado', + message: _messageForStatus(), child: _buildIndicator( - const Icon( - Icons.check_circle, - size: 16, - color: Colors.green, - ), + const Icon(Icons.check_circle, size: 16, color: Colors.green), ), ); case SyncStatus.error: return Tooltip( - message: errorMessage ?? 'Error al sincronizar', + message: _messageForStatus(), child: _buildIndicator( - const Icon( - Icons.error, - size: 16, - color: Colors.red, - ), + const Icon(Icons.error, size: 16, color: Colors.red), ), ); }