diff --git a/i18n/en.json b/i18n/en.json index 4f7613c9b7..ab3c7ad7b4 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1122,6 +1122,7 @@ "unable_to_update_workflow": "Unable to update workflow", "unable_to_upload_file": "Unable to upload file" }, + "errors_text": "Errors", "exclusion_pattern": "Exclusion pattern", "exif": "Exif", "exif_bottom_sheet_description": "Add Description...", diff --git a/mobile/lib/pages/backup/drift_upload_detail.page.dart b/mobile/lib/pages/backup/drift_upload_detail.page.dart index eaf7ef5d81..76f61963f3 100644 --- a/mobile/lib/pages/backup/drift_upload_detail.page.dart +++ b/mobile/lib/pages/backup/drift_upload_detail.page.dart @@ -28,17 +28,14 @@ class _DriftUploadDetailPageState extends ConsumerState { final uploadItems = ref.watch(driftBackupProvider.select((state) => state.uploadItems)); final iCloudProgress = ref.watch(driftBackupProvider.select((state) => state.iCloudDownloadProgress)); - // Track failed items to exclude them from completed list for (final item in uploadItems.values) { if (item.isFailed == true) { _failedTaskIds.add(item.taskId); } } - // Remove any items from completed list that have since failed _completedItems.removeWhere((item) => _failedTaskIds.contains(item.taskId)); - // Watch for items with progress >= 1.0 (completed successfully) before they get removed for (final item in uploadItems.values) { if (item.progress >= 1.0 && item.isFailed != true && !_failedTaskIds.contains(item.taskId)) { if (!_seenTaskIds.contains(item.taskId)) { @@ -51,10 +48,11 @@ class _DriftUploadDetailPageState extends ConsumerState { } } - // Get current uploading items (progress < 1.0 or failed) - final currentUploads = uploadItems.values.where((item) => item.progress < 1.0 || item.isFailed == true).toList(); + final uploadingItems = uploadItems.values.where((item) => item.progress < 1.0 && item.isFailed != true).toList(); + final failedItems = uploadItems.values.where((item) => item.isFailed == true).toList(); - final hasContent = currentUploads.isNotEmpty || iCloudProgress.isNotEmpty || _completedItems.isNotEmpty; + final hasContent = + uploadingItems.isNotEmpty || failedItems.isNotEmpty || iCloudProgress.isNotEmpty || _completedItems.isNotEmpty; return Scaffold( appBar: AppBar( @@ -79,7 +77,7 @@ class _DriftUploadDetailPageState extends ConsumerState { ), body: !hasContent ? _buildEmptyState(context) - : _buildTwoSectionLayout(context, currentUploads, iCloudProgress, _completedItems), + : _buildTwoSectionLayout(context, uploadingItems, failedItems, iCloudProgress, _completedItems), ); } @@ -101,7 +99,8 @@ class _DriftUploadDetailPageState extends ConsumerState { Widget _buildTwoSectionLayout( BuildContext context, - List currentUploads, + List uploadingItems, + List failedItems, Map iCloudProgress, List completedItems, ) { @@ -131,10 +130,12 @@ class _DriftUploadDetailPageState extends ConsumerState { ), ], + // Uploading Section SliverToBoxAdapter( child: _buildSectionHeader( context, title: "uploading".t(context: context), + count: uploadingItems.length, color: context.colorScheme.primary, ), ), @@ -142,8 +143,8 @@ class _DriftUploadDetailPageState extends ConsumerState { padding: const EdgeInsets.symmetric(horizontal: 16), sliver: SliverList( delegate: SliverChildBuilderDelegate((context, index) { - if (index < currentUploads.length) { - final item = currentUploads[index]; + if (index < uploadingItems.length) { + final item = uploadingItems[index]; return _buildCurrentUploadCard(context, item); } else { return _buildPlaceholderCard(context); @@ -152,6 +153,27 @@ class _DriftUploadDetailPageState extends ConsumerState { ), ), + // Errors Section + if (failedItems.isNotEmpty) ...[ + SliverToBoxAdapter( + child: _buildSectionHeader( + context, + title: "errors_text".t(context: context), + count: failedItems.length, + color: context.colorScheme.error, + ), + ), + SliverPadding( + padding: const EdgeInsets.symmetric(horizontal: 16), + sliver: SliverList( + delegate: SliverChildBuilderDelegate((context, index) { + final item = failedItems[index]; + return Padding(padding: const EdgeInsets.only(bottom: 8), child: _buildErrorCard(context, item)); + }, childCount: failedItems.length), + ), + ), + ], + // Completed Section if (completedItems.isNotEmpty) ...[ SliverToBoxAdapter( @@ -409,6 +431,53 @@ class _DriftUploadDetailPageState extends ConsumerState { ); } + Widget _buildErrorCard(BuildContext context, DriftUploadStatus item) { + return Card( + elevation: 0, + color: context.colorScheme.errorContainer, + shape: RoundedRectangleBorder( + borderRadius: const BorderRadius.all(Radius.circular(12)), + side: BorderSide(color: context.colorScheme.error.withValues(alpha: 0.3), width: 1), + ), + child: InkWell( + onTap: () => _showFileDetailDialog(context, item), + borderRadius: const BorderRadius.all(Radius.circular(12)), + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + _CurrentUploadThumbnail(taskId: item.taskId), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + path.basename(item.filename), + style: context.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + Text( + item.error ?? "Upload failed", + style: context.textTheme.bodySmall?.copyWith(color: context.colorScheme.error), + maxLines: 4, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + const SizedBox(width: 12), + Icon(Icons.error_rounded, color: context.colorScheme.error, size: 28), + ], + ), + ), + ), + ); + } + Widget _buildPlaceholderCard(BuildContext context) { return Card( elevation: 0, diff --git a/mobile/lib/providers/app_life_cycle.provider.dart b/mobile/lib/providers/app_life_cycle.provider.dart index 6dabd9a744..6ddd58012a 100644 --- a/mobile/lib/providers/app_life_cycle.provider.dart +++ b/mobile/lib/providers/app_life_cycle.provider.dart @@ -237,6 +237,9 @@ class AppLifeCycleNotifier extends StateNotifier { if (_ref.read(backupProvider.notifier).backupProgress != BackUpProgressEnum.manualInProgress) { _ref.read(backupProvider.notifier).cancelBackup(); } + } else { + // Cancel foreground upload when app goes to background + await _ref.read(driftBackupProvider.notifier).stopForegroundBackup(); } _ref.read(websocketProvider.notifier).disconnect();