diff --git a/mobile/lib/providers/backup/exp_backup.provider.dart b/mobile/lib/providers/backup/exp_backup.provider.dart index b07a606d45..fa56ad8363 100644 --- a/mobile/lib/providers/backup/exp_backup.provider.dart +++ b/mobile/lib/providers/backup/exp_backup.provider.dart @@ -2,35 +2,99 @@ import 'dart:convert'; import 'package:background_downloader/background_downloader.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/cupertino.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/constants.dart'; + import 'package:immich_mobile/domain/utils/background_sync.dart'; import 'package:immich_mobile/providers/background_sync.provider.dart'; - import 'package:immich_mobile/services/exp_backup.service.dart'; import 'package:immich_mobile/services/upload.service.dart'; +class ExpUploadStatus { + final String taskId; + final String filename; + final double progress; + ExpUploadStatus({ + required this.taskId, + required this.filename, + required this.progress, + }); + + ExpUploadStatus copyWith({ + String? taskId, + String? filename, + double? progress, + }) { + return ExpUploadStatus( + taskId: taskId ?? this.taskId, + filename: filename ?? this.filename, + progress: progress ?? this.progress, + ); + } + + Map toMap() { + return { + 'taskId': taskId, + 'filename': filename, + 'progress': progress, + }; + } + + factory ExpUploadStatus.fromMap(Map map) { + return ExpUploadStatus( + taskId: map['taskId'] as String, + filename: map['filename'] as String, + progress: map['progress'] as double, + ); + } + + String toJson() => json.encode(toMap()); + + factory ExpUploadStatus.fromJson(String source) => + ExpUploadStatus.fromMap(json.decode(source) as Map); + + @override + String toString() => + 'ExpUploadStatus(taskId: $taskId, filename: $filename, progress: $progress)'; + + @override + bool operator ==(covariant ExpUploadStatus other) { + if (identical(this, other)) return true; + + return other.taskId == taskId && + other.filename == filename && + other.progress == progress; + } + + @override + int get hashCode => taskId.hashCode ^ filename.hashCode ^ progress.hashCode; +} + class ExpBackupState { final int totalCount; final int backupCount; final int remainderCount; + final Map uploadItems; ExpBackupState({ required this.totalCount, required this.backupCount, required this.remainderCount, + required this.uploadItems, }); ExpBackupState copyWith({ int? totalCount, int? backupCount, int? remainderCount, + Map? uploadItems, }) { return ExpBackupState( totalCount: totalCount ?? this.totalCount, backupCount: backupCount ?? this.backupCount, remainderCount: remainderCount ?? this.remainderCount, + uploadItems: uploadItems ?? this.uploadItems, ); } @@ -39,6 +103,7 @@ class ExpBackupState { 'totalCount': totalCount, 'backupCount': backupCount, 'remainderCount': remainderCount, + 'uploadItems': uploadItems, }; } @@ -47,6 +112,14 @@ class ExpBackupState { totalCount: map['totalCount'] as int, backupCount: map['backupCount'] as int, remainderCount: map['remainderCount'] as int, + uploadItems: Map.from( + (map['uploadItems'] as Map).map( + (key, value) => MapEntry( + key, + ExpUploadStatus.fromMap(value as Map), + ), + ), + ), ); } @@ -56,21 +129,28 @@ class ExpBackupState { ExpBackupState.fromMap(json.decode(source) as Map); @override - String toString() => - 'ExpBackupState(totalCount: $totalCount, backupCount: $backupCount, remainderCount: $remainderCount)'; + String toString() { + return 'ExpBackupState(totalCount: $totalCount, backupCount: $backupCount, remainderCount: $remainderCount, uploadItems: $uploadItems)'; + } @override bool operator ==(covariant ExpBackupState other) { if (identical(this, other)) return true; + final mapEquals = const DeepCollectionEquality().equals; return other.totalCount == totalCount && other.backupCount == backupCount && - other.remainderCount == remainderCount; + other.remainderCount == remainderCount && + mapEquals(other.uploadItems, uploadItems); } @override - int get hashCode => - totalCount.hashCode ^ backupCount.hashCode ^ remainderCount.hashCode; + int get hashCode { + return totalCount.hashCode ^ + backupCount.hashCode ^ + remainderCount.hashCode ^ + uploadItems.hashCode; + } } final expBackupProvider = @@ -92,6 +172,7 @@ class ExpBackupNotifier extends StateNotifier { totalCount: 0, backupCount: 0, remainderCount: 0, + uploadItems: {}, ), ) { { @@ -120,8 +201,6 @@ class ExpBackupNotifier extends StateNotifier { remainderCount: state.remainderCount - 1, ); - // TODO: find a better place to call this. - _backgroundSyncManager.syncRemote(); break; default: @@ -130,10 +209,30 @@ class ExpBackupNotifier extends StateNotifier { } void _taskProgressCallback(TaskProgressUpdate update) { - debugPrint("[_taskProgressCallback] $update"); + final uploadStatus = ExpUploadStatus( + taskId: update.task.taskId, + filename: update.task.displayName, + progress: update.progress, + ); + + state = state.copyWith( + uploadItems: { + for (final entry in state.uploadItems.entries) + if (entry.key == update.task.taskId) + entry.key: uploadStatus + else + entry.key: entry.value, + if (!state.uploadItems.containsKey(update.task.taskId)) + update.task.taskId: uploadStatus, + }, + ); + + print(update.task.taskId); } Future getBackupStatus() async { + await _backgroundSyncManager.syncRemote(); + final [totalCount, backupCount, remainderCount] = await Future.wait([ _backupService.getTotalCount(), _backupService.getBackupCount(), diff --git a/mobile/lib/services/upload.service.dart b/mobile/lib/services/upload.service.dart index 02d39763b6..3427ad509f 100644 --- a/mobile/lib/services/upload.service.dart +++ b/mobile/lib/services/upload.service.dart @@ -108,7 +108,7 @@ class UploadService { return UploadTask( taskId: id, - displayName: filename, + displayName: originalFileName ?? filename, httpRequestMethod: 'POST', url: url, headers: headers, diff --git a/mobile/lib/widgets/backup/current_backup_asset_info_box.dart b/mobile/lib/widgets/backup/current_backup_asset_info_box.dart index b6d0edb200..3bd715c0f2 100644 --- a/mobile/lib/widgets/backup/current_backup_asset_info_box.dart +++ b/mobile/lib/widgets/backup/current_backup_asset_info_box.dart @@ -2,43 +2,53 @@ import 'dart:io'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/providers/backup/exp_backup.provider.dart'; import 'package:immich_mobile/widgets/backup/asset_info_table.dart'; import 'package:immich_mobile/widgets/backup/error_chip.dart'; import 'package:immich_mobile/widgets/backup/icloud_download_progress_bar.dart'; import 'package:immich_mobile/widgets/backup/upload_progress_bar.dart'; import 'package:immich_mobile/widgets/backup/upload_stats.dart'; -class CurrentUploadingAssetInfoBox extends StatelessWidget { +class CurrentUploadingAssetInfoBox extends ConsumerWidget { const CurrentUploadingAssetInfoBox({super.key}); @override - Widget build(BuildContext context) { - return ListTile( - isThreeLine: true, - leading: Icon( - Icons.image_outlined, - color: context.primaryColor, - size: 30, - ), - title: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "backup_controller_page_uploading_file_info", - style: context.textTheme.titleSmall, - ).tr(), - const BackupErrorChip(), - ], - ), - subtitle: Column( - children: [ - if (Platform.isIOS) const IcloudDownloadProgressBar(), - const BackupUploadProgressBar(), - const BackupUploadStats(), - const BackupAssetInfoTable(), - ], - ), + Widget build(BuildContext context, WidgetRef ref) { + final uploadItems = + ref.watch(expBackupProvider.select((state) => state.uploadItems)); + + return Column( + children: [ + if (uploadItems.isNotEmpty) + Container( + constraints: const BoxConstraints(maxHeight: 200), + child: ListView.builder( + shrinkWrap: true, + itemCount: uploadItems.length, + itemBuilder: (context, index) { + final uploadItem = uploadItems.values.elementAt(index); + return ListTile( + dense: true, + leading: CircularProgressIndicator( + value: uploadItem.progress, + strokeWidth: 2, + ), + title: Text( + uploadItem.filename, + style: context.textTheme.bodySmall, + overflow: TextOverflow.ellipsis, + ), + trailing: Text( + '${(uploadItem.progress * 100).toInt()}%', + style: context.textTheme.bodySmall, + ), + ); + }, + ), + ), + ], ); } } diff --git a/server/src/middleware/file-upload.interceptor.ts b/server/src/middleware/file-upload.interceptor.ts index 2323d40763..b6f37dbbd2 100644 --- a/server/src/middleware/file-upload.interceptor.ts +++ b/server/src/middleware/file-upload.interceptor.ts @@ -103,10 +103,6 @@ export class FileUploadInterceptor implements NestInterceptor { } private filename(request: AuthRequest, file: Express.Multer.File, callback: DiskStorageCallback) { - console.log( - 'FileUploadInterceptor.filename called with file:', - this.assetService.getUploadFilename(asRequest(request, file)), - ); return callbackify( () => this.assetService.getUploadFilename(asRequest(request, file)), callback as Callback, diff --git a/server/src/utils/asset.util.ts b/server/src/utils/asset.util.ts index 2835269ff7..9492937886 100644 --- a/server/src/utils/asset.util.ts +++ b/server/src/utils/asset.util.ts @@ -191,7 +191,9 @@ export function mapToUploadFile(file: ImmichFile): UploadFile { } export const asRequest = (request: AuthRequest, file: Express.Multer.File) => { - file.originalname = request.body['filename'] ?? file.filename; + if (request.body['filename'] != undefined) { + file.originalname = request.body['filename']; + } return { auth: request.user || null,