diff --git a/mobile/lib/domain/utils/background_sync.dart b/mobile/lib/domain/utils/background_sync.dart index b2f2fe54e1..38e249b9f1 100644 --- a/mobile/lib/domain/utils/background_sync.dart +++ b/mobile/lib/domain/utils/background_sync.dart @@ -6,11 +6,12 @@ import 'package:immich_mobile/utils/isolate.dart'; import 'package:worker_manager/worker_manager.dart'; typedef SyncCallback = void Function(); +typedef SyncCallbackWithResult = void Function(T result); typedef SyncErrorCallback = void Function(String error); class BackgroundSyncManager { final SyncCallback? onRemoteSyncStart; - final SyncCallback? onRemoteSyncComplete; + final SyncCallbackWithResult? onRemoteSyncComplete; final SyncErrorCallback? onRemoteSyncError; final SyncCallback? onLocalSyncStart; @@ -156,15 +157,18 @@ class BackgroundSyncManager { debugLabel: 'remote-sync', ); return _syncTask! - .then((result) => result ?? false) - .whenComplete(() { - onRemoteSyncComplete?.call(); - _syncTask = null; + .then((result) { + final success = result ?? false; + onRemoteSyncComplete?.call(success); + return success; }) .catchError((error) { onRemoteSyncError?.call(error.toString()); _syncTask = null; return false; + }) + .whenComplete(() { + _syncTask = null; }); } diff --git a/mobile/lib/pages/backup/drift_backup.page.dart b/mobile/lib/pages/backup/drift_backup.page.dart index 5a2cab8dd6..2e7c3e946c 100644 --- a/mobile/lib/pages/backup/drift_backup.page.dart +++ b/mobile/lib/pages/backup/drift_backup.page.dart @@ -49,9 +49,6 @@ class _DriftBackupPageState extends ConsumerState { ref.read(driftBackupProvider.notifier).updateSyncing(true); syncSuccess = await ref.read(backgroundSyncProvider).syncRemote(); - ref - .read(driftBackupProvider.notifier) - .updateError(syncSuccess == true ? BackupError.none : BackupError.syncFailed); ref.read(driftBackupProvider.notifier).updateSyncing(false); if (mounted) { @@ -94,7 +91,6 @@ class _DriftBackupPageState extends ConsumerState { if (syncSuccess == false) { Logger("DriftBackupPage").warning("Remote sync did not complete successfully, skipping backup"); - backupNotifier.updateError(BackupError.syncFailed); return; } await backupNotifier.startBackup(currentUser.id); diff --git a/mobile/lib/pages/backup/drift_backup_album_selection.page.dart b/mobile/lib/pages/backup/drift_backup_album_selection.page.dart index d49f71ce52..cae9f0a408 100644 --- a/mobile/lib/pages/backup/drift_backup_album_selection.page.dart +++ b/mobile/lib/pages/backup/drift_backup_album_selection.page.dart @@ -120,7 +120,6 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState { _resumeBackup(backupProvider), ]); } else { - backupProvider.updateError(BackupError.syncFailed); await backgroundManager.hashAssets(); } diff --git a/mobile/lib/providers/app_life_cycle.provider.dart b/mobile/lib/providers/app_life_cycle.provider.dart index 29de09fd33..3b51874ab5 100644 --- a/mobile/lib/providers/app_life_cycle.provider.dart +++ b/mobile/lib/providers/app_life_cycle.provider.dart @@ -161,7 +161,6 @@ class AppLifeCycleNotifier extends StateNotifier { _resumeBackup(), ]); } else { - _ref.read(driftBackupProvider.notifier).updateError(BackupError.syncFailed); await _safeRun(backgroundManager.hashAssets(), "hashAssets"); } diff --git a/mobile/lib/providers/background_sync.provider.dart b/mobile/lib/providers/background_sync.provider.dart index e6e83b64df..a61cd93022 100644 --- a/mobile/lib/providers/background_sync.provider.dart +++ b/mobile/lib/providers/background_sync.provider.dart @@ -1,12 +1,21 @@ import 'package:hooks_riverpod/hooks_riverpod.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'; final backgroundSyncProvider = Provider((ref) { final syncStatusNotifier = ref.read(syncStatusProvider.notifier); + final backupProvider = ref.read(driftBackupProvider.notifier); + final manager = BackgroundSyncManager( - onRemoteSyncStart: syncStatusNotifier.startRemoteSync, - onRemoteSyncComplete: syncStatusNotifier.completeRemoteSync, + onRemoteSyncStart: () { + syncStatusNotifier.startRemoteSync(); + backupProvider.updateError(BackupError.none); + }, + onRemoteSyncComplete: (isSuccess) { + syncStatusNotifier.completeRemoteSync(); + backupProvider.updateError(isSuccess == true ? BackupError.none : BackupError.syncFailed); + }, onRemoteSyncError: syncStatusNotifier.errorRemoteSync, onLocalSyncStart: syncStatusNotifier.startLocalSync, onLocalSyncComplete: syncStatusNotifier.completeLocalSync, diff --git a/mobile/lib/widgets/common/immich_sliver_app_bar.dart b/mobile/lib/widgets/common/immich_sliver_app_bar.dart index 23d64ecfcd..90c213599c 100644 --- a/mobile/lib/widgets/common/immich_sliver_app_bar.dart +++ b/mobile/lib/widgets/common/immich_sliver_app_bar.dart @@ -162,48 +162,32 @@ class _ProfileIndicator extends ConsumerWidget { } } +const double _kBadgeWidgetSize = 30.0; + class _BackupIndicator extends ConsumerWidget { const _BackupIndicator(); @override Widget build(BuildContext context, WidgetRef ref) { - const widgetSize = 30.0; - 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; + final indicatorIcon = _getBackupBadgeIcon(context, ref); return InkWell( onTap: () => context.pushRoute(const DriftBackupRoute()), borderRadius: const BorderRadius.all(Radius.circular(12)), child: Badge( - label: Container( - 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, - ), + label: indicatorIcon, backgroundColor: Colors.transparent, alignment: Alignment.bottomRight, isLabelVisible: indicatorIcon != null, 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) { final backupStateStream = ref.watch(settingsProvider).watch(Setting.enableBackup); + final hasError = ref.watch(driftBackupProvider.select((state) => state.error != BackupError.none)); final isDarkTheme = context.isDarkTheme; final iconColor = isDarkTheme ? Colors.white : Colors.black; final isUploading = ref.watch(driftBackupProvider.select((state) => state.uploadItems.isNotEmpty)); @@ -215,42 +199,76 @@ class _BackupIndicator extends ConsumerWidget { final backupEnabled = snapshot.data ?? false; if (!backupEnabled) { - return Icon( - Icons.cloud_off_rounded, - size: 9, - color: iconColor, - semanticLabel: 'backup_controller_page_backup'.tr(), + return _BadgeLabel( + Icon( + Icons.cloud_off_rounded, + size: 9, + 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) { - return Container( - padding: const EdgeInsets.all(3.5), - child: Theme( - data: context.themeData.copyWith( - progressIndicatorTheme: context.themeData.progressIndicatorTheme.copyWith(year2023: true), - ), - child: CircularProgressIndicator( - strokeWidth: 2, - strokeCap: StrokeCap.round, - valueColor: AlwaysStoppedAnimation(iconColor), - semanticsLabel: 'backup_controller_page_backup'.tr(), + return _BadgeLabel( + Container( + padding: const EdgeInsets.all(3.5), + child: Theme( + data: context.themeData.copyWith( + progressIndicatorTheme: context.themeData.progressIndicatorTheme.copyWith(year2023: true), + ), + child: CircularProgressIndicator( + strokeWidth: 2, + strokeCap: StrokeCap.round, + valueColor: AlwaysStoppedAnimation(iconColor), + semanticsLabel: 'backup_controller_page_backup'.tr(), + ), ), ), ); } - return Icon( - Icons.check_outlined, - size: 9, - color: iconColor, - semanticLabel: 'backup_controller_page_backup'.tr(), + return _BadgeLabel( + Icon(Icons.check_outlined, 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 { const _SyncStatusIndicator();