From ee3c07d049688627d009dca22db51dc324563d04 Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Mon, 29 Sep 2025 20:40:00 +0530 Subject: [PATCH] fix: process upload only after successful remote sync (#22360) Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- i18n/en.json | 1 + .../services/background_worker.service.dart | 53 ++++++++----------- .../domain/services/sync_stream.service.dart | 3 +- mobile/lib/domain/utils/background_sync.dart | 8 +-- .../repositories/sync_api.repository.dart | 1 - .../lib/pages/backup/drift_backup.page.dart | 36 ++++++++++++- .../drift_backup_album_selection.page.dart | 14 ++++- .../backup/drift_backup_options.page.dart | 20 +++++-- .../lib/pages/common/splash_screen.page.dart | 20 +++++-- .../providers/app_life_cycle.provider.dart | 21 +++++--- .../backup/drift_backup.provider.dart | 23 ++++++-- mobile/lib/services/server_info.service.dart | 9 ---- mobile/lib/utils/isolate.dart | 6 +-- .../widgets/common/immich_sliver_app_bar.dart | 14 +++-- 14 files changed, 156 insertions(+), 73 deletions(-) diff --git a/i18n/en.json b/i18n/en.json index b142dc7fca..72e2c77e41 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -599,6 +599,7 @@ "backup_controller_page_turn_on": "Turn on foreground backup", "backup_controller_page_uploading_file_info": "Uploading file info", "backup_err_only_album": "Cannot remove the only album", + "backup_error_sync_failed": "Sync failed. Cannot start backup.", "backup_info_card_assets": "assets", "backup_manual_cancelled": "Cancelled", "backup_manual_in_progress": "Upload already in progress. Try after sometime", diff --git a/mobile/lib/domain/services/background_worker.service.dart b/mobile/lib/domain/services/background_worker.service.dart index 4e9291a1fc..0548a45bf7 100644 --- a/mobile/lib/domain/services/background_worker.service.dart +++ b/mobile/lib/domain/services/background_worker.service.dart @@ -28,7 +28,6 @@ import 'package:immich_mobile/repositories/file_media.repository.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/services/auth.service.dart'; import 'package:immich_mobile/services/localization.service.dart'; -import 'package:immich_mobile/services/server_info.service.dart'; import 'package:immich_mobile/services/upload.service.dart'; import 'package:immich_mobile/utils/bootstrap.dart'; import 'package:immich_mobile/utils/debug_print.dart'; @@ -130,30 +129,33 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { @override Future onAndroidUpload() async { + _logger.info('Android background processing started'); + final sw = Stopwatch()..start(); try { - _logger.info('Android background processing started'); - final sw = Stopwatch()..start(); - - await _syncAssets(hashTimeout: Duration(minutes: _isBackupEnabled ? 3 : 6)); + if (!await _syncAssets(hashTimeout: Duration(minutes: _isBackupEnabled ? 3 : 6))) { + _logger.warning("Remote sync did not complete successfully, skipping backup"); + return; + } await _handleBackup(); - - sw.stop(); - _logger.info("Android background processing completed in ${sw.elapsed.inSeconds}s"); } catch (error, stack) { _logger.severe("Failed to complete Android background processing", error, stack); } finally { + sw.stop(); + _logger.info("Android background processing completed in ${sw.elapsed.inSeconds}s"); await _cleanup(); } } @override Future onIosUpload(bool isRefresh, int? maxSeconds) async { + _logger.info('iOS background upload started with maxSeconds: ${maxSeconds}s'); + final sw = Stopwatch()..start(); try { - _logger.info('iOS background upload started with maxSeconds: ${maxSeconds}s'); - final sw = Stopwatch()..start(); - final timeout = isRefresh ? const Duration(seconds: 5) : Duration(minutes: _isBackupEnabled ? 3 : 6); - await _syncAssets(hashTimeout: timeout); + if (!await _syncAssets(hashTimeout: timeout)) { + _logger.warning("Remote sync did not complete successfully, skipping backup"); + return; + } final backupFuture = _handleBackup(); if (maxSeconds != null) { @@ -161,12 +163,11 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { } else { await backupFuture; } - - sw.stop(); - _logger.info("iOS background upload completed in ${sw.elapsed.inSeconds}s"); } catch (error, stack) { _logger.severe("Failed to complete iOS background upload", error, stack); } finally { + sw.stop(); + _logger.info("iOS background upload completed in ${sw.elapsed.inSeconds}s"); await _cleanup(); } } @@ -227,29 +228,20 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { } if (!_isBackupEnabled) { - _logger.info("[_handleBackup 1] Backup is disabled. Skipping backup routine"); + _logger.info("Backup is disabled. Skipping backup routine"); return; } - _logger.info("[_handleBackup 2] Enqueuing assets for backup from the background service"); - final currentUser = _ref?.read(currentUserProvider); if (currentUser == null) { - _logger.warning("[_handleBackup 3] No current user found. Skipping backup from background"); + _logger.warning("No current user found. Skipping backup from background"); return; } - _logger.info("[_handleBackup 4] Resume backup from background"); if (Platform.isIOS) { return _ref?.read(driftBackupProvider.notifier).handleBackupResume(currentUser.id); } - final canPing = await _ref?.read(serverInfoServiceProvider).ping() ?? false; - if (!canPing) { - _logger.warning("[_handleBackup 5] Server is not reachable. Skipping backup from background"); - return; - } - final networkCapabilities = await _ref?.read(connectivityApiProvider).getCapabilities() ?? []; return _ref ?.read(uploadServiceProvider) @@ -261,15 +253,15 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { ); } - Future _syncAssets({Duration? hashTimeout}) async { + Future _syncAssets({Duration? hashTimeout}) async { await _ref?.read(backgroundSyncProvider).syncLocal(); if (_isCleanedUp) { - return; + return false; } - await _ref?.read(backgroundSyncProvider).syncRemote(); + final isSuccess = await _ref?.read(backgroundSyncProvider).syncRemote() ?? false; if (_isCleanedUp) { - return; + return isSuccess; } var hashFuture = _ref?.read(backgroundSyncProvider).hashAssets(); @@ -283,6 +275,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi { } await hashFuture; + return isSuccess; } } diff --git a/mobile/lib/domain/services/sync_stream.service.dart b/mobile/lib/domain/services/sync_stream.service.dart index 6c8e444d50..bec7e6afda 100644 --- a/mobile/lib/domain/services/sync_stream.service.dart +++ b/mobile/lib/domain/services/sync_stream.service.dart @@ -23,7 +23,7 @@ class SyncStreamService { bool get isCancelled => _cancelChecker?.call() ?? false; - Future sync() async { + Future sync() async { _logger.info("Remote sync request for user"); // Start the sync stream and handle events bool shouldReset = false; @@ -32,6 +32,7 @@ class SyncStreamService { _logger.info("Resetting sync state as requested by server"); await _syncApiRepository.streamChanges(_handleEvents); } + return true; } Future _handleEvents(List events, Function() abort, Function() reset) async { diff --git a/mobile/lib/domain/utils/background_sync.dart b/mobile/lib/domain/utils/background_sync.dart index ffbb020345..b2f2fe54e1 100644 --- a/mobile/lib/domain/utils/background_sync.dart +++ b/mobile/lib/domain/utils/background_sync.dart @@ -21,7 +21,7 @@ class BackgroundSyncManager { final SyncCallback? onHashingComplete; final SyncErrorCallback? onHashingError; - Cancelable? _syncTask; + Cancelable? _syncTask; Cancelable? _syncWebsocketTask; Cancelable? _deviceAlbumSyncTask; Cancelable? _linkedAlbumSyncTask; @@ -144,9 +144,9 @@ class BackgroundSyncManager { }); } - Future syncRemote() { + Future syncRemote() { if (_syncTask != null) { - return _syncTask!.future; + return _syncTask!.future.then((result) => result ?? false).catchError((_) => false); } onRemoteSyncStart?.call(); @@ -156,6 +156,7 @@ class BackgroundSyncManager { debugLabel: 'remote-sync', ); return _syncTask! + .then((result) => result ?? false) .whenComplete(() { onRemoteSyncComplete?.call(); _syncTask = null; @@ -163,6 +164,7 @@ class BackgroundSyncManager { .catchError((error) { onRemoteSyncError?.call(error.toString()); _syncTask = null; + return false; }); } diff --git a/mobile/lib/infrastructure/repositories/sync_api.repository.dart b/mobile/lib/infrastructure/repositories/sync_api.repository.dart index 3969286d28..8bf2e80579 100644 --- a/mobile/lib/infrastructure/repositories/sync_api.repository.dart +++ b/mobile/lib/infrastructure/repositories/sync_api.repository.dart @@ -110,7 +110,6 @@ class SyncApiRepository { await onData(_parseLines(lines), abort, reset); } } catch (error, stack) { - _logger.severe("Error processing stream", error, stack); return Future.error(error, stack); } finally { client.close(); diff --git a/mobile/lib/pages/backup/drift_backup.page.dart b/mobile/lib/pages/backup/drift_backup.page.dart index 30782726e2..9b4a79c557 100644 --- a/mobile/lib/pages/backup/drift_backup.page.dart +++ b/mobile/lib/pages/backup/drift_backup.page.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -8,6 +10,7 @@ import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/generated/intl_keys.g.dart'; import 'package:immich_mobile/presentation/widgets/backup/backup_toggle_button.widget.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/backup/backup_album.provider.dart'; @@ -16,8 +19,7 @@ import 'package:immich_mobile/providers/sync_status.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/widgets/backup/backup_info_card.dart'; -import 'dart:async'; - +import 'package:logging/logging.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; @RoutePage() @@ -63,7 +65,10 @@ class _DriftBackupPageState extends ConsumerState { .where((album) => album.backupSelection == BackupSelection.selected) .toList(); + final error = ref.watch(driftBackupProvider.select((p) => p.error)); + final backupNotifier = ref.read(driftBackupProvider.notifier); + final backupSyncManager = ref.read(backgroundSyncProvider); Future startBackup() async { final currentUser = Store.tryGet(StoreKey.currentUser); @@ -71,7 +76,14 @@ class _DriftBackupPageState extends ConsumerState { return; } + final syncSuccess = await backupSyncManager.syncRemote(); await backupNotifier.getBackupStatus(currentUser.id); + + if (!syncSuccess) { + Logger("DriftBackupPage").warning("Remote sync did not complete successfully, skipping backup"); + await backupNotifier.updateError(BackupError.syncFailed); + return; + } await backupNotifier.startBackup(currentUser.id); } @@ -114,6 +126,26 @@ class _DriftBackupPageState extends ConsumerState { const _RemainderCard(), const Divider(), BackupToggleButton(onStart: () async => await startBackup(), onStop: () async => await stopBackup()), + switch (error) { + BackupError.none => const SizedBox.shrink(), + BackupError.syncFailed => Padding( + padding: const EdgeInsets.only(top: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + children: [ + Icon(Icons.warning_rounded, color: context.colorScheme.error, fill: 1), + const SizedBox(width: 8), + Text( + IntlKeys.backup_error_sync_failed.t(), + style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.error), + textAlign: TextAlign.center, + ), + ], + ), + ), + }, TextButton.icon( icon: const Icon(Icons.info_outline_rounded), onPressed: () => context.pushRoute(const DriftUploadDetailRoute()), 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 368341f24a..d49f71ce52 100644 --- a/mobile/lib/pages/backup/drift_backup_album_selection.page.dart +++ b/mobile/lib/pages/backup/drift_backup_album_selection.page.dart @@ -18,6 +18,7 @@ import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/widgets/backup/drift_album_info_list_tile.dart'; import 'package:immich_mobile/widgets/common/search_field.dart'; +import 'package:logging/logging.dart'; @RoutePage() class DriftBackupAlbumSelectionPage extends ConsumerStatefulWidget { @@ -112,7 +113,18 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState backgroundSync.hashAssets())); if (isBackupEnabled) { - unawaited(backupNotifier.cancel().whenComplete(() => backupNotifier.startBackup(user.id))); + unawaited( + backupNotifier.cancel().whenComplete( + () => backgroundSync.syncRemote().then((success) { + if (success) { + return backupNotifier.startBackup(user.id); + } else { + Logger('DriftBackupAlbumSelectionPage').warning('Background sync failed, not starting backup'); + backupNotifier.updateError(BackupError.syncFailed); + } + }), + ), + ); } } diff --git a/mobile/lib/pages/backup/drift_backup_options.page.dart b/mobile/lib/pages/backup/drift_backup_options.page.dart index 92f911ae1e..f18dc48dca 100644 --- a/mobile/lib/pages/backup/drift_backup_options.page.dart +++ b/mobile/lib/pages/backup/drift_backup_options.page.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -5,10 +7,12 @@ import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; +import 'package:immich_mobile/providers/background_sync.provider.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/widgets/settings/backup_settings/drift_backup_settings.dart'; +import 'package:logging/logging.dart'; @RoutePage() class DriftBackupOptionsPage extends ConsumerWidget { @@ -54,9 +58,19 @@ class DriftBackupOptionsPage extends ConsumerWidget { ); final backupNotifier = ref.read(driftBackupProvider.notifier); - backupNotifier.cancel().then((_) { - backupNotifier.startBackup(currentUser.id); - }); + final backgroundSync = ref.read(backgroundSyncProvider); + unawaited( + backupNotifier.cancel().whenComplete( + () => backgroundSync.syncRemote().then((success) { + if (success) { + return backupNotifier.startBackup(currentUser.id); + } else { + Logger('DriftBackupOptionsPage').warning('Background sync failed, not starting backup'); + backupNotifier.updateError(BackupError.syncFailed); + } + }), + ), + ); } }, child: Scaffold( diff --git a/mobile/lib/pages/common/splash_screen.page.dart b/mobile/lib/pages/common/splash_screen.page.dart index aa4d142381..3b81368cd4 100644 --- a/mobile/lib/pages/common/splash_screen.page.dart +++ b/mobile/lib/pages/common/splash_screen.page.dart @@ -62,14 +62,24 @@ class SplashScreenPageState extends ConsumerState { infoProvider.getServerInfo(); if (Store.isBetaTimelineEnabled) { - await Future.wait([backgroundManager.syncLocal(), backgroundManager.syncRemote()]); + bool syncSuccess = false; await Future.wait([ - backgroundManager.hashAssets().then((_) { - _resumeBackup(backupProvider); - }), - _resumeBackup(backupProvider), + backgroundManager.syncLocal(), + backgroundManager.syncRemote().then((success) => syncSuccess = success), ]); + if (syncSuccess) { + await Future.wait([ + backgroundManager.hashAssets().then((_) { + _resumeBackup(backupProvider); + }), + _resumeBackup(backupProvider), + ]); + } else { + backupProvider.updateError(BackupError.syncFailed); + await backgroundManager.hashAssets(); + } + if (Store.get(StoreKey.syncAlbums, false)) { await backgroundManager.syncLinkedAlbum(); } diff --git a/mobile/lib/providers/app_life_cycle.provider.dart b/mobile/lib/providers/app_life_cycle.provider.dart index ec6495440a..29de09fd33 100644 --- a/mobile/lib/providers/app_life_cycle.provider.dart +++ b/mobile/lib/providers/app_life_cycle.provider.dart @@ -148,17 +148,22 @@ class AppLifeCycleNotifier extends StateNotifier { final isAlbumLinkedSyncEnable = _ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.syncAlbums); try { + bool syncSuccess = false; await Future.wait([ _safeRun(backgroundManager.syncLocal(), "syncLocal"), - _safeRun(backgroundManager.syncRemote(), "syncRemote"), - ]); - - await Future.wait([ - _safeRun(backgroundManager.hashAssets(), "hashAssets").then((_) { - _resumeBackup(); - }), - _resumeBackup(), + _safeRun(backgroundManager.syncRemote().then((success) => syncSuccess = success), "syncRemote"), ]); + if (syncSuccess) { + await Future.wait([ + _safeRun(backgroundManager.hashAssets(), "hashAssets").then((_) { + _resumeBackup(); + }), + _resumeBackup(), + ]); + } else { + _ref.read(driftBackupProvider.notifier).updateError(BackupError.syncFailed); + await _safeRun(backgroundManager.hashAssets(), "hashAssets"); + } if (isAlbumLinkedSyncEnable) { await _safeRun(backgroundManager.syncLinkedAlbum(), "syncLinkedAlbum"); diff --git a/mobile/lib/providers/backup/drift_backup.provider.dart b/mobile/lib/providers/backup/drift_backup.provider.dart index fb6a94b0cb..37d5ce4e2b 100644 --- a/mobile/lib/providers/backup/drift_backup.provider.dart +++ b/mobile/lib/providers/backup/drift_backup.provider.dart @@ -91,6 +91,8 @@ class DriftUploadStatus { } } +enum BackupError { none, syncFailed } + class DriftBackupState { final int totalCount; final int backupCount; @@ -101,6 +103,7 @@ class DriftBackupState { final int enqueueTotalCount; final bool isCanceling; + final BackupError error; final Map uploadItems; @@ -113,6 +116,7 @@ class DriftBackupState { required this.enqueueTotalCount, required this.isCanceling, required this.uploadItems, + this.error = BackupError.none, }); DriftBackupState copyWith({ @@ -124,6 +128,7 @@ class DriftBackupState { int? enqueueTotalCount, bool? isCanceling, Map? uploadItems, + BackupError? error, }) { return DriftBackupState( totalCount: totalCount ?? this.totalCount, @@ -134,12 +139,13 @@ class DriftBackupState { enqueueTotalCount: enqueueTotalCount ?? this.enqueueTotalCount, isCanceling: isCanceling ?? this.isCanceling, uploadItems: uploadItems ?? this.uploadItems, + error: error ?? this.error, ); } @override String toString() { - return 'DriftBackupState(totalCount: $totalCount, backupCount: $backupCount, remainderCount: $remainderCount, processingCount: $processingCount, enqueueCount: $enqueueCount, enqueueTotalCount: $enqueueTotalCount, isCanceling: $isCanceling, uploadItems: $uploadItems)'; + return 'DriftBackupState(totalCount: $totalCount, backupCount: $backupCount, remainderCount: $remainderCount, processingCount: $processingCount, enqueueCount: $enqueueCount, enqueueTotalCount: $enqueueTotalCount, isCanceling: $isCanceling, uploadItems: $uploadItems, error: $error)'; } @override @@ -154,7 +160,8 @@ class DriftBackupState { other.enqueueCount == enqueueCount && other.enqueueTotalCount == enqueueTotalCount && other.isCanceling == isCanceling && - mapEquals(other.uploadItems, uploadItems); + mapEquals(other.uploadItems, uploadItems) && + other.error == error; } @override @@ -166,7 +173,8 @@ class DriftBackupState { enqueueCount.hashCode ^ enqueueTotalCount.hashCode ^ isCanceling.hashCode ^ - uploadItems.hashCode; + uploadItems.hashCode ^ + error.hashCode; } } @@ -186,6 +194,7 @@ class DriftBackupNotifier extends StateNotifier { enqueueTotalCount: 0, isCanceling: false, uploadItems: {}, + error: BackupError.none, ), ) { { @@ -303,7 +312,12 @@ class DriftBackupNotifier extends StateNotifier { ); } + Future updateError(BackupError error) async { + state = state.copyWith(error: error); + } + Future startBackup(String userId) { + state = state.copyWith(error: BackupError.none); return _uploadService.startBackup(userId, _updateEnqueueCount); } @@ -313,7 +327,7 @@ class DriftBackupNotifier extends StateNotifier { Future cancel() async { dPrint(() => "Canceling backup tasks..."); - state = state.copyWith(enqueueCount: 0, enqueueTotalCount: 0, isCanceling: true); + state = state.copyWith(enqueueCount: 0, enqueueTotalCount: 0, isCanceling: true, error: BackupError.none); final activeTaskCount = await _uploadService.cancelBackup(); @@ -329,6 +343,7 @@ class DriftBackupNotifier extends StateNotifier { Future handleBackupResume(String userId) async { _logger.info("Resuming backup tasks..."); + state = state.copyWith(error: BackupError.none); final tasks = await _uploadService.getActiveTasks(kBackupGroup); _logger.info("Found ${tasks.length} tasks"); diff --git a/mobile/lib/services/server_info.service.dart b/mobile/lib/services/server_info.service.dart index 0bce9366d2..460e135421 100644 --- a/mobile/lib/services/server_info.service.dart +++ b/mobile/lib/services/server_info.service.dart @@ -14,15 +14,6 @@ class ServerInfoService { const ServerInfoService(this._apiService); - Future ping() async { - try { - await _apiService.serverInfoApi.pingServer().timeout(const Duration(seconds: 5)); - return true; - } catch (e) { - return false; - } - } - Future getDiskInfo() async { try { final dto = await _apiService.serverInfoApi.getStorage(); diff --git a/mobile/lib/utils/isolate.dart b/mobile/lib/utils/isolate.dart index e8b7d410f4..1ccf00d58b 100644 --- a/mobile/lib/utils/isolate.dart +++ b/mobile/lib/utils/isolate.dart @@ -32,6 +32,7 @@ Cancelable runInIsolateGentle({ } return workerManager.executeGentle((cancelledChecker) async { + T? result; await runZonedGuarded( () async { BackgroundIsolateBinaryMessenger.ensureInitialized(token); @@ -53,7 +54,7 @@ Cancelable runInIsolateGentle({ try { HttpSSLOptions.apply(applyNative: false); - return await computation(ref); + result = await computation(ref); } on CanceledError { log.warning("Computation cancelled ${debugLabel == null ? '' : ' for $debugLabel'}"); } catch (error, stack) { @@ -83,12 +84,11 @@ Cancelable runInIsolateGentle({ await Future.delayed(const Duration(seconds: 2)); } } - return null; }, (error, stack) { dPrint(() => "Error in isolate $debugLabel zone: $error, $stack"); }, ); - return null; + return result; }); } diff --git a/mobile/lib/widgets/common/immich_sliver_app_bar.dart b/mobile/lib/widgets/common/immich_sliver_app_bar.dart index 09c0518a23..23d64ecfcd 100644 --- a/mobile/lib/widgets/common/immich_sliver_app_bar.dart +++ b/mobile/lib/widgets/common/immich_sliver_app_bar.dart @@ -9,8 +9,8 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/models/server_info/server_info.model.dart'; import 'package:immich_mobile/providers/backup/drift_backup.provider.dart'; import 'package:immich_mobile/providers/cast.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/setting.provider.dart'; import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/setting.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/sync_status.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; @@ -168,8 +168,16 @@ class _BackupIndicator extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { const widgetSize = 30.0; - final indicatorIcon = _getBackupBadgeIcon(context, ref); - final badgeBackground = context.colorScheme.surfaceContainer; + 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( onTap: () => context.pushRoute(const DriftBackupRoute()),