fix: improve sync backup error indicator (#22527)

* fix: improve sync indicator error

* prefer backup disabled icon before error

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
shenlong 2025-10-03 19:06:44 +05:30 committed by GitHub
parent 3c5a125762
commit 212649edf9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 81 additions and 58 deletions

View file

@ -6,11 +6,12 @@ import 'package:immich_mobile/utils/isolate.dart';
import 'package:worker_manager/worker_manager.dart'; import 'package:worker_manager/worker_manager.dart';
typedef SyncCallback = void Function(); typedef SyncCallback = void Function();
typedef SyncCallbackWithResult<T> = void Function(T result);
typedef SyncErrorCallback = void Function(String error); typedef SyncErrorCallback = void Function(String error);
class BackgroundSyncManager { class BackgroundSyncManager {
final SyncCallback? onRemoteSyncStart; final SyncCallback? onRemoteSyncStart;
final SyncCallback? onRemoteSyncComplete; final SyncCallbackWithResult<bool?>? onRemoteSyncComplete;
final SyncErrorCallback? onRemoteSyncError; final SyncErrorCallback? onRemoteSyncError;
final SyncCallback? onLocalSyncStart; final SyncCallback? onLocalSyncStart;
@ -156,15 +157,18 @@ class BackgroundSyncManager {
debugLabel: 'remote-sync', debugLabel: 'remote-sync',
); );
return _syncTask! return _syncTask!
.then((result) => result ?? false) .then((result) {
.whenComplete(() { final success = result ?? false;
onRemoteSyncComplete?.call(); onRemoteSyncComplete?.call(success);
_syncTask = null; return success;
}) })
.catchError((error) { .catchError((error) {
onRemoteSyncError?.call(error.toString()); onRemoteSyncError?.call(error.toString());
_syncTask = null; _syncTask = null;
return false; return false;
})
.whenComplete(() {
_syncTask = null;
}); });
} }

View file

@ -49,9 +49,6 @@ class _DriftBackupPageState extends ConsumerState<DriftBackupPage> {
ref.read(driftBackupProvider.notifier).updateSyncing(true); ref.read(driftBackupProvider.notifier).updateSyncing(true);
syncSuccess = await ref.read(backgroundSyncProvider).syncRemote(); syncSuccess = await ref.read(backgroundSyncProvider).syncRemote();
ref
.read(driftBackupProvider.notifier)
.updateError(syncSuccess == true ? BackupError.none : BackupError.syncFailed);
ref.read(driftBackupProvider.notifier).updateSyncing(false); ref.read(driftBackupProvider.notifier).updateSyncing(false);
if (mounted) { if (mounted) {
@ -94,7 +91,6 @@ class _DriftBackupPageState extends ConsumerState<DriftBackupPage> {
if (syncSuccess == false) { if (syncSuccess == false) {
Logger("DriftBackupPage").warning("Remote sync did not complete successfully, skipping backup"); Logger("DriftBackupPage").warning("Remote sync did not complete successfully, skipping backup");
backupNotifier.updateError(BackupError.syncFailed);
return; return;
} }
await backupNotifier.startBackup(currentUser.id); await backupNotifier.startBackup(currentUser.id);

View file

@ -120,7 +120,6 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState<DriftBackupAlbum
return backupNotifier.startBackup(user.id); return backupNotifier.startBackup(user.id);
} else { } else {
Logger('DriftBackupAlbumSelectionPage').warning('Background sync failed, not starting backup'); Logger('DriftBackupAlbumSelectionPage').warning('Background sync failed, not starting backup');
backupNotifier.updateError(BackupError.syncFailed);
} }
}), }),
), ),

View file

@ -66,7 +66,6 @@ class DriftBackupOptionsPage extends ConsumerWidget {
return backupNotifier.startBackup(currentUser.id); return backupNotifier.startBackup(currentUser.id);
} else { } else {
Logger('DriftBackupOptionsPage').warning('Background sync failed, not starting backup'); Logger('DriftBackupOptionsPage').warning('Background sync failed, not starting backup');
backupNotifier.updateError(BackupError.syncFailed);
} }
}), }),
), ),

View file

@ -76,7 +76,6 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
_resumeBackup(backupProvider), _resumeBackup(backupProvider),
]); ]);
} else { } else {
backupProvider.updateError(BackupError.syncFailed);
await backgroundManager.hashAssets(); await backgroundManager.hashAssets();
} }

View file

@ -161,7 +161,6 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
_resumeBackup(), _resumeBackup(),
]); ]);
} else { } else {
_ref.read(driftBackupProvider.notifier).updateError(BackupError.syncFailed);
await _safeRun(backgroundManager.hashAssets(), "hashAssets"); await _safeRun(backgroundManager.hashAssets(), "hashAssets");
} }

View file

@ -1,12 +1,21 @@
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/utils/background_sync.dart'; import 'package:immich_mobile/domain/utils/background_sync.dart';
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
import 'package:immich_mobile/providers/sync_status.provider.dart'; import 'package:immich_mobile/providers/sync_status.provider.dart';
final backgroundSyncProvider = Provider<BackgroundSyncManager>((ref) { final backgroundSyncProvider = Provider<BackgroundSyncManager>((ref) {
final syncStatusNotifier = ref.read(syncStatusProvider.notifier); final syncStatusNotifier = ref.read(syncStatusProvider.notifier);
final backupProvider = ref.read(driftBackupProvider.notifier);
final manager = BackgroundSyncManager( final manager = BackgroundSyncManager(
onRemoteSyncStart: syncStatusNotifier.startRemoteSync, onRemoteSyncStart: () {
onRemoteSyncComplete: syncStatusNotifier.completeRemoteSync, syncStatusNotifier.startRemoteSync();
backupProvider.updateError(BackupError.none);
},
onRemoteSyncComplete: (isSuccess) {
syncStatusNotifier.completeRemoteSync();
backupProvider.updateError(isSuccess == true ? BackupError.none : BackupError.syncFailed);
},
onRemoteSyncError: syncStatusNotifier.errorRemoteSync, onRemoteSyncError: syncStatusNotifier.errorRemoteSync,
onLocalSyncStart: syncStatusNotifier.startLocalSync, onLocalSyncStart: syncStatusNotifier.startLocalSync,
onLocalSyncComplete: syncStatusNotifier.completeLocalSync, onLocalSyncComplete: syncStatusNotifier.completeLocalSync,

View file

@ -162,48 +162,32 @@ class _ProfileIndicator extends ConsumerWidget {
} }
} }
const double _kBadgeWidgetSize = 30.0;
class _BackupIndicator extends ConsumerWidget { class _BackupIndicator extends ConsumerWidget {
const _BackupIndicator(); const _BackupIndicator();
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
const widgetSize = 30.0; final indicatorIcon = _getBackupBadgeIcon(context, ref);
final hasError = ref.watch(driftBackupProvider.select((state) => state.error != BackupError.none));
final indicatorIcon = hasError
? Icon(
Icons.warning_rounded,
size: 12,
color: context.colorScheme.error,
semanticLabel: 'backup_controller_page_backup'.tr(),
)
: _getBackupBadgeIcon(context, ref);
final badgeBackground = hasError ? context.colorScheme.errorContainer : context.colorScheme.surfaceContainer;
return InkWell( return InkWell(
onTap: () => context.pushRoute(const DriftBackupRoute()), onTap: () => context.pushRoute(const DriftBackupRoute()),
borderRadius: const BorderRadius.all(Radius.circular(12)), borderRadius: const BorderRadius.all(Radius.circular(12)),
child: Badge( child: Badge(
label: Container( label: indicatorIcon,
width: widgetSize / 2,
height: widgetSize / 2,
decoration: BoxDecoration(
color: badgeBackground,
border: Border.all(color: context.colorScheme.outline.withValues(alpha: .3)),
borderRadius: BorderRadius.circular(widgetSize / 2),
),
child: indicatorIcon,
),
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
alignment: Alignment.bottomRight, alignment: Alignment.bottomRight,
isLabelVisible: indicatorIcon != null, isLabelVisible: indicatorIcon != null,
offset: const Offset(-2, -12), offset: const Offset(-2, -12),
child: Icon(Icons.backup_rounded, size: widgetSize, color: context.primaryColor), child: Icon(Icons.backup_rounded, size: _kBadgeWidgetSize, color: context.primaryColor),
), ),
); );
} }
Widget? _getBackupBadgeIcon(BuildContext context, WidgetRef ref) { Widget? _getBackupBadgeIcon(BuildContext context, WidgetRef ref) {
final backupStateStream = ref.watch(settingsProvider).watch(Setting.enableBackup); final backupStateStream = ref.watch(settingsProvider).watch(Setting.enableBackup);
final hasError = ref.watch(driftBackupProvider.select((state) => state.error != BackupError.none));
final isDarkTheme = context.isDarkTheme; final isDarkTheme = context.isDarkTheme;
final iconColor = isDarkTheme ? Colors.white : Colors.black; final iconColor = isDarkTheme ? Colors.white : Colors.black;
final isUploading = ref.watch(driftBackupProvider.select((state) => state.uploadItems.isNotEmpty)); final isUploading = ref.watch(driftBackupProvider.select((state) => state.uploadItems.isNotEmpty));
@ -215,42 +199,76 @@ class _BackupIndicator extends ConsumerWidget {
final backupEnabled = snapshot.data ?? false; final backupEnabled = snapshot.data ?? false;
if (!backupEnabled) { if (!backupEnabled) {
return Icon( return _BadgeLabel(
Icons.cloud_off_rounded, Icon(
size: 9, Icons.cloud_off_rounded,
color: iconColor, size: 9,
semanticLabel: 'backup_controller_page_backup'.tr(), color: iconColor,
semanticLabel: 'backup_controller_page_backup'.tr(),
),
);
}
if (hasError) {
return _BadgeLabel(
Icon(
Icons.warning_rounded,
size: 12,
color: context.colorScheme.error,
semanticLabel: 'backup_controller_page_backup'.tr(),
),
backgroundColor: context.colorScheme.errorContainer,
); );
} }
if (isUploading) { if (isUploading) {
return Container( return _BadgeLabel(
padding: const EdgeInsets.all(3.5), Container(
child: Theme( padding: const EdgeInsets.all(3.5),
data: context.themeData.copyWith( child: Theme(
progressIndicatorTheme: context.themeData.progressIndicatorTheme.copyWith(year2023: true), data: context.themeData.copyWith(
), progressIndicatorTheme: context.themeData.progressIndicatorTheme.copyWith(year2023: true),
child: CircularProgressIndicator( ),
strokeWidth: 2, child: CircularProgressIndicator(
strokeCap: StrokeCap.round, strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(iconColor), strokeCap: StrokeCap.round,
semanticsLabel: 'backup_controller_page_backup'.tr(), valueColor: AlwaysStoppedAnimation<Color>(iconColor),
semanticsLabel: 'backup_controller_page_backup'.tr(),
),
), ),
), ),
); );
} }
return Icon( return _BadgeLabel(
Icons.check_outlined, Icon(Icons.check_outlined, size: 9, color: iconColor, semanticLabel: 'backup_controller_page_backup'.tr()),
size: 9,
color: iconColor,
semanticLabel: 'backup_controller_page_backup'.tr(),
); );
}, },
); );
} }
} }
class _BadgeLabel extends StatelessWidget {
final Widget indicator;
final Color? backgroundColor;
const _BadgeLabel(this.indicator, {this.backgroundColor});
@override
Widget build(BuildContext context) {
return Container(
width: _kBadgeWidgetSize / 2,
height: _kBadgeWidgetSize / 2,
decoration: BoxDecoration(
color: backgroundColor ?? context.colorScheme.surfaceContainer,
border: Border.all(color: context.colorScheme.outline.withValues(alpha: .3)),
borderRadius: BorderRadius.circular(_kBadgeWidgetSize / 2),
),
child: indicator,
);
}
}
class _SyncStatusIndicator extends ConsumerStatefulWidget { class _SyncStatusIndicator extends ConsumerStatefulWidget {
const _SyncStatusIndicator(); const _SyncStatusIndicator();