diff --git a/mobile/lib/repositories/upload.repository.dart b/mobile/lib/repositories/upload.repository.dart index d868fb4423..9ae39a85f4 100644 --- a/mobile/lib/repositories/upload.repository.dart +++ b/mobile/lib/repositories/upload.repository.dart @@ -94,14 +94,15 @@ class UploadRepository { required Map fields, required Client httpClient, required Completer cancelToken, - required void Function(int bytes, int totalBytes) onProgress, // TODO: use onProgress + void Function(int bytes, int totalBytes)? onProgress, required String logContext, }) async { final String savedEndpoint = Store.get(StoreKey.serverEndpoint); - final baseRequest = AbortableMultipartRequest( + final baseRequest = ProgressMultipartRequest( 'POST', Uri.parse('$savedEndpoint/assets'), abortTrigger: cancelToken.future, + onProgress: onProgress, ); try { @@ -151,6 +152,34 @@ class UploadRepository { } } +class ProgressMultipartRequest extends MultipartRequest with Abortable { + ProgressMultipartRequest(super.method, super.url, {this.abortTrigger, this.onProgress}); + + @override + final Future? abortTrigger; + + final void Function(int bytes, int totalBytes)? onProgress; + + @override + ByteStream finalize() { + final byteStream = super.finalize(); + if (onProgress == null) return byteStream; + + final total = contentLength; + var bytes = 0; + final stream = byteStream.transform( + StreamTransformer.fromHandlers( + handleData: (List data, EventSink> sink) { + bytes += data.length; + onProgress!(bytes, total); + sink.add(data); + }, + ), + ); + return ByteStream(stream); + } +} + class UploadResult { final bool isSuccess; final bool isCancelled; diff --git a/mobile/lib/services/backup.service.dart b/mobile/lib/services/backup.service.dart index ea1c9d5da2..5b8b5540ca 100644 --- a/mobile/lib/services/backup.service.dart +++ b/mobile/lib/services/backup.service.dart @@ -11,6 +11,7 @@ import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/backup_album.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/network.repository.dart'; +import 'package:immich_mobile/repositories/upload.repository.dart'; import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; import 'package:immich_mobile/models/backup/error_upload_asset.model.dart'; @@ -237,7 +238,7 @@ class BackupService { bool isBackground = false, PMProgressHandler? pmProgressHandler, required void Function(SuccessUploadAsset result) onSuccess, - required void Function(int bytes, int totalBytes) onProgress, // TODO: use onProgress + required void Function(int bytes, int totalBytes) onProgress, required void Function(CurrentUploadAsset asset) onCurrentAsset, required void Function(ErrorUploadAsset error) onError, }) async { @@ -313,10 +314,11 @@ class BackupService { filename: originalFileName, ); - final baseRequest = AbortableMultipartRequest( + final baseRequest = ProgressMultipartRequest( 'POST', Uri.parse('$savedEndpoint/assets'), abortTrigger: cancelToken.future, + onProgress: ((bytes, totalBytes) => onProgress(bytes, totalBytes)), ); baseRequest.headers.addAll(ApiService.getRequestHeaders()); @@ -442,10 +444,9 @@ class BackupService { livePhotoVideoFile.lengthSync(), filename: livePhotoTitle, ); - final livePhotoReq = - AbortableMultipartRequest(baseRequest.method, baseRequest.url, abortTrigger: cancelToken.future) - ..headers.addAll(baseRequest.headers) - ..fields.addAll(baseRequest.fields); + final livePhotoReq = ProgressMultipartRequest(baseRequest.method, baseRequest.url, abortTrigger: cancelToken.future) + ..headers.addAll(baseRequest.headers) + ..fields.addAll(baseRequest.fields); livePhotoReq.files.add(livePhotoRawUploadData); diff --git a/mobile/lib/services/foreground_upload.service.dart b/mobile/lib/services/foreground_upload.service.dart index 34f509413a..d0670037b0 100644 --- a/mobile/lib/services/foreground_upload.service.dart +++ b/mobile/lib/services/foreground_upload.service.dart @@ -340,6 +340,7 @@ class ForegroundUploadService { if (entity.isLivePhoto && livePhotoFile != null) { final livePhotoTitle = p.setExtension(originalFileName, p.extension(livePhotoFile.path)); + final onProgress = callbacks.onProgress; final livePhotoResult = await _uploadRepository.uploadFile( file: livePhotoFile, originalFileName: livePhotoTitle, @@ -347,8 +348,9 @@ class ForegroundUploadService { fields: fields, httpClient: NetworkRepository.client, cancelToken: cancelToken, - onProgress: (bytes, totalBytes) => - callbacks.onProgress?.call(asset.localId!, livePhotoTitle, bytes, totalBytes), + onProgress: onProgress != null + ? (bytes, totalBytes) => onProgress(asset.localId!, livePhotoTitle, bytes, totalBytes) + : null, logContext: 'livePhotoVideo[${asset.localId}]', ); @@ -377,6 +379,7 @@ class ForegroundUploadService { ]); } + final onProgress = callbacks.onProgress; final result = await _uploadRepository.uploadFile( file: file, originalFileName: originalFileName, @@ -384,8 +387,9 @@ class ForegroundUploadService { fields: fields, httpClient: NetworkRepository.client, cancelToken: cancelToken, - onProgress: (bytes, totalBytes) => - callbacks.onProgress?.call(asset.localId!, originalFileName, bytes, totalBytes), + onProgress: onProgress != null + ? (bytes, totalBytes) => onProgress(asset.localId!, originalFileName, bytes, totalBytes) + : null, logContext: 'asset[${asset.localId}]', ); @@ -453,7 +457,7 @@ class ForegroundUploadService { fields: fields, httpClient: httpClient, cancelToken: cancelToken, - onProgress: onProgress ?? (_, __) {}, + onProgress: onProgress, logContext: 'shareIntent[$deviceAssetId]', ); } catch (e) {