feat: Refactor sync status handling and improve synchronization feedback in the app
This commit is contained in:
+153
-77
@@ -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<NotesApp>
|
||||
_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<NotesApp>
|
||||
Future<void> _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<NotesApp>
|
||||
}
|
||||
|
||||
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<NotesApp>
|
||||
}
|
||||
|
||||
if (biometricChoicePending) {
|
||||
_pendingEncryptionKey = await _vaultService.readStoredEncryptionKeyRaw();
|
||||
_pendingEncryptionKey = await _vaultService
|
||||
.readStoredEncryptionKeyRaw();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_phase = _AppPhase.biometricChoice;
|
||||
@@ -194,7 +192,8 @@ class _NotesAppState extends State<NotesApp>
|
||||
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<NotesApp>
|
||||
_repository = NoteRepository(
|
||||
database: database,
|
||||
authApi: AuthApi.instance,
|
||||
masterKey: encryptionKey,
|
||||
masterKey: encryptionKey,
|
||||
);
|
||||
_phase = _AppPhase.notes;
|
||||
});
|
||||
@@ -244,7 +243,11 @@ class _NotesAppState extends State<NotesApp>
|
||||
|
||||
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<NotesApp>
|
||||
_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<NotesApp>
|
||||
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<String, dynamic> response = await AuthApi.instance.register(
|
||||
username,
|
||||
@@ -341,8 +346,10 @@ class _NotesAppState extends State<NotesApp>
|
||||
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<NotesApp>
|
||||
}
|
||||
} 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<NotesApp>
|
||||
return _beginInitialVaultFlow();
|
||||
}
|
||||
|
||||
Future<void> _completeBiometricChoice({required bool enableBiometrics}) async {
|
||||
Future<void> _completeBiometricChoice({
|
||||
required bool enableBiometrics,
|
||||
}) async {
|
||||
if (_isUnlocking) {
|
||||
return;
|
||||
}
|
||||
@@ -386,7 +397,9 @@ class _NotesAppState extends State<NotesApp>
|
||||
});
|
||||
|
||||
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<NotesApp>
|
||||
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<NotesApp>
|
||||
}
|
||||
|
||||
_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<NotesApp>
|
||||
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<NotesApp>
|
||||
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<String, dynamic> result = await _repository!.performSync(forceFull: forceFull);
|
||||
final Map<String, dynamic> result = await _repository!.performSync(
|
||||
forceFull: forceFull,
|
||||
onProgress: updateSyncState,
|
||||
);
|
||||
|
||||
if (!mounted) {
|
||||
return;
|
||||
@@ -666,19 +726,25 @@ class _NotesAppState extends State<NotesApp>
|
||||
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) {
|
||||
if (mounted && syncOperationId == _syncOperationId) {
|
||||
setState(() {
|
||||
_syncStatus = SyncStatus.idle;
|
||||
_syncProgress = null;
|
||||
_syncDetailMessage = null;
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -691,6 +757,8 @@ class _NotesAppState extends State<NotesApp>
|
||||
setState(() {
|
||||
_syncStatus = SyncStatus.error;
|
||||
_syncErrorMessage = e.toString();
|
||||
_syncProgress = null;
|
||||
_syncDetailMessage = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -745,6 +813,8 @@ class _NotesAppState extends State<NotesApp>
|
||||
onRequestSync: _performSync,
|
||||
onVaultInvalid: _resetLocalVaultData,
|
||||
syncStatus: _syncStatus,
|
||||
syncProgress: _syncProgress,
|
||||
syncDetailMessage: _syncDetailMessage,
|
||||
syncErrorMessage: _syncErrorMessage,
|
||||
refreshToken: _homeRefreshToken,
|
||||
)
|
||||
@@ -780,48 +850,53 @@ class _NotesAppState extends State<NotesApp>
|
||||
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<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,
|
||||
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<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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -913,8 +988,10 @@ class _NotesAppState extends State<NotesApp>
|
||||
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<NotesApp>
|
||||
|
||||
return _buildMainShell(repository);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user