From 44b4f350191b8f8b969bea97c9e3e9173c4cef3b Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 27 Jan 2026 10:33:44 -0600 Subject: [PATCH] chore: expose upload errors to UI (#25566) --- i18n/en.json | 4 ++++ .../backup/backup_toggle_button.widget.dart | 10 ++++++++++ .../providers/backup/drift_backup.provider.dart | 2 ++ .../lib/services/background_upload.service.dart | 2 ++ .../lib/services/foreground_upload.service.dart | 17 ++++++++++++++++- 5 files changed, 34 insertions(+), 1 deletion(-) diff --git a/i18n/en.json b/i18n/en.json index ca843c7822..a435c5986e 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -572,6 +572,9 @@ "asset_list_layout_sub_title": "Layout", "asset_list_settings_subtitle": "Photo grid layout settings", "asset_list_settings_title": "Photo Grid", + "asset_not_found_on_device_android": "Asset not found on device", + "asset_not_found_on_device_ios": "Asset not found on device. If you are using iCloud, the asset may be inaccessible due to bad file stored on iCloud", + "asset_not_found_on_icloud": "Asset not found on iCloud. the asset may be inaccessible due to bad file stored on iCloud", "asset_offline": "Asset Offline", "asset_offline_description": "This external asset is no longer found on disk. Please contact your Immich administrator for help.", "asset_restored_successfully": "Asset restored successfully", @@ -2295,6 +2298,7 @@ "upload_details": "Upload Details", "upload_dialog_info": "Do you want to backup the selected Asset(s) to the server?", "upload_dialog_title": "Upload Asset", + "upload_error_with_count": "Upload error for {count, plural, one {# asset} other {# assets}}", "upload_errors": "Upload completed with {count, plural, one {# error} other {# errors}}, refresh the page to see new upload assets.", "upload_finished": "Upload finished", "upload_progress": "Remaining {remaining, number} - Processed {processed, number}/{total, number}", diff --git a/mobile/lib/presentation/widgets/backup/backup_toggle_button.widget.dart b/mobile/lib/presentation/widgets/backup/backup_toggle_button.widget.dart index 6361475f26..7c92dc01d8 100644 --- a/mobile/lib/presentation/widgets/backup/backup_toggle_button.widget.dart +++ b/mobile/lib/presentation/widgets/backup/backup_toggle_button.widget.dart @@ -62,6 +62,8 @@ class BackupToggleButtonState extends ConsumerState with Sin final iCloudProgress = ref.watch(driftBackupProvider.select((state) => state.iCloudDownloadProgress)); + final errorCount = ref.watch(driftBackupProvider.select((state) => state.errorCount)); + final isProcessing = uploadTasks.isNotEmpty || isSyncing || iCloudProgress.isNotEmpty; return AnimatedBuilder( @@ -149,6 +151,14 @@ class BackupToggleButtonState extends ConsumerState with Sin ), ], ), + if (errorCount > 0) + Padding( + padding: const EdgeInsets.only(top: 2), + child: Text( + "upload_error_with_count".t(context: context, args: {'count': '$errorCount'}), + style: context.textTheme.labelMedium?.copyWith(color: context.colorScheme.error), + ), + ), ], ), ), diff --git a/mobile/lib/providers/backup/drift_backup.provider.dart b/mobile/lib/providers/backup/drift_backup.provider.dart index e2d548595c..2f067fdf67 100644 --- a/mobile/lib/providers/backup/drift_backup.provider.dart +++ b/mobile/lib/providers/backup/drift_backup.provider.dart @@ -149,6 +149,8 @@ class DriftBackupState { ); } + int get errorCount => uploadItems.values.where((item) => item.isFailed == true).length; + @override String toString() { return 'DriftBackupState(totalCount: $totalCount, backupCount: $backupCount, remainderCount: $remainderCount, processingCount: $processingCount, isSyncing: $isSyncing, error: $error, uploadItems: $uploadItems, cancelToken: $cancelToken, iCloudDownloadProgress: $iCloudDownloadProgress)'; diff --git a/mobile/lib/services/background_upload.service.dart b/mobile/lib/services/background_upload.service.dart index 19192c9cff..4eece142d2 100644 --- a/mobile/lib/services/background_upload.service.dart +++ b/mobile/lib/services/background_upload.service.dart @@ -260,6 +260,7 @@ class BackgroundUploadService { Future getUploadTask(LocalAsset asset, {String group = kBackupGroup, int? priority}) async { final entity = await _storageRepository.getAssetEntityForAsset(asset); if (entity == null) { + _logger.warning("Asset entity not found for ${asset.id} - ${asset.name}"); return null; } @@ -282,6 +283,7 @@ class BackgroundUploadService { } if (file == null) { + _logger.warning("Failed to get file for asset ${asset.id} - ${asset.name}"); return null; } diff --git a/mobile/lib/services/foreground_upload.service.dart b/mobile/lib/services/foreground_upload.service.dart index 30cf4abcf6..cd28942bd2 100644 --- a/mobile/lib/services/foreground_upload.service.dart +++ b/mobile/lib/services/foreground_upload.service.dart @@ -10,6 +10,7 @@ import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/extensions/network_capability_extensions.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/infrastructure/repositories/backup.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart'; import 'package:immich_mobile/platform/connectivity_api.g.dart'; @@ -266,6 +267,10 @@ class ForegroundUploadService { try { final entity = await _storageRepository.getAssetEntityForAsset(asset); if (entity == null) { + callbacks.onError?.call( + asset.localId!, + CurrentPlatform.isAndroid ? "asset_not_found_on_device_android".t() : "asset_not_found_on_device_ios".t(), + ); return; } @@ -298,6 +303,11 @@ class ForegroundUploadService { // Get files locally file = await _storageRepository.getFileForAsset(asset.id); if (file == null) { + _logger.warning("Failed to get file ${asset.id} - ${asset.name}"); + callbacks.onError?.call( + asset.localId!, + CurrentPlatform.isAndroid ? "asset_not_found_on_device_android".t() : "asset_not_found_on_device_ios".t(), + ); return; } @@ -306,12 +316,17 @@ class ForegroundUploadService { livePhotoFile = await _storageRepository.getMotionFileForAsset(asset); if (livePhotoFile == null) { _logger.warning("Failed to obtain motion part of the livePhoto - ${asset.name}"); + callbacks.onError?.call( + asset.localId!, + CurrentPlatform.isAndroid ? "asset_not_found_on_device_android".t() : "asset_not_found_on_device_ios".t(), + ); } } } if (file == null) { - _logger.warning("Failed to obtain file for asset ${asset.id} - ${asset.name}"); + _logger.warning("Failed to obtain file from iCloud for asset ${asset.id} - ${asset.name}"); + callbacks.onError?.call(asset.localId!, "asset_not_found_on_icloud".t()); return; }