From dd5eb154cf92be6a4820421d79fef12515a4d6bf Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 21 Aug 2024 11:21:45 -0500 Subject: [PATCH 01/34] curating assets with albums to upload --- .../models/backup/backup_candidate.model.dart | 19 ++++++ .../lib/models/backup/backup_state.model.dart | 6 +- .../lib/providers/backup/backup.provider.dart | 60 ++++++++++++++----- .../backup/manual_upload.provider.dart | 19 +++--- mobile/lib/services/background.service.dart | 34 +++++------ mobile/lib/services/backup.service.dart | 20 +++++-- 6 files changed, 109 insertions(+), 49 deletions(-) create mode 100644 mobile/lib/models/backup/backup_candidate.model.dart diff --git a/mobile/lib/models/backup/backup_candidate.model.dart b/mobile/lib/models/backup/backup_candidate.model.dart new file mode 100644 index 0000000000000..986ad795f92a2 --- /dev/null +++ b/mobile/lib/models/backup/backup_candidate.model.dart @@ -0,0 +1,19 @@ +import 'package:photo_manager/photo_manager.dart'; + +class BackupCandidate { + BackupCandidate({required this.asset, required this.albums}); + + AssetEntity asset; + List albums; + + @override + int get hashCode => asset.hashCode; + + @override + bool operator ==(Object other) { + if (other is! BackupCandidate) { + return false; + } + return asset == other.asset; + } +} diff --git a/mobile/lib/models/backup/backup_state.model.dart b/mobile/lib/models/backup/backup_state.model.dart index bb693a5b75f7a..d829f411fc355 100644 --- a/mobile/lib/models/backup/backup_state.model.dart +++ b/mobile/lib/models/backup/backup_state.model.dart @@ -2,7 +2,7 @@ import 'package:cancellation_token_http/http.dart'; import 'package:collection/collection.dart'; -import 'package:photo_manager/photo_manager.dart'; +import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; import 'package:immich_mobile/models/backup/available_album.model.dart'; import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; @@ -41,7 +41,7 @@ class BackUpState { final Set excludedBackupAlbums; /// Assets that are not overlapping in selected backup albums and excluded backup albums - final Set allUniqueAssets; + final Set allUniqueAssets; /// All assets from the selected albums that have been backup final Set selectedAlbumsBackupAssetsIds; @@ -94,7 +94,7 @@ class BackUpState { List? availableAlbums, Set? selectedBackupAlbums, Set? excludedBackupAlbums, - Set? allUniqueAssets, + Set? allUniqueAssets, Set? selectedAlbumsBackupAssetsIds, CurrentUploadAsset? currentUploadAsset, }) { diff --git a/mobile/lib/providers/backup/backup.provider.dart b/mobile/lib/providers/backup/backup.provider.dart index 58027e3b941e0..18c955cd5d67c 100644 --- a/mobile/lib/providers/backup/backup.provider.dart +++ b/mobile/lib/providers/backup/backup.provider.dart @@ -6,6 +6,7 @@ import 'package:flutter/widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/models/backup/available_album.model.dart'; import 'package:immich_mobile/entities/backup_album.entity.dart'; +import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; import 'package:immich_mobile/models/backup/backup_state.model.dart'; import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; import 'package:immich_mobile/models/backup/error_upload_asset.model.dart'; @@ -290,9 +291,10 @@ class BackupNotifier extends StateNotifier { /// Future _updateBackupAssetCount() async { final duplicatedAssetIds = await _backupService.getDuplicatedAssetIds(); - final Set assetsFromSelectedAlbums = {}; - final Set assetsFromExcludedAlbums = {}; + final Set assetsFromSelectedAlbums = {}; + final Set assetsFromExcludedAlbums = {}; + debugPrint("Selected Albums: ${state.selectedBackupAlbums}"); for (final album in state.selectedBackupAlbums) { final assetCount = await album.albumEntity.assetCountAsync; @@ -304,7 +306,30 @@ class BackupNotifier extends StateNotifier { start: 0, end: assetCount, ); - assetsFromSelectedAlbums.addAll(assets); + + for (final asset in assets) { + List existingAlbums = [album.name]; + + final inOtherAlbum = assetsFromSelectedAlbums.firstWhereOrNull( + (a) => a.asset.id == asset.id, + ); + + if (inOtherAlbum != null) { + existingAlbums.addAll(inOtherAlbum.albums); + assetsFromSelectedAlbums.remove(inOtherAlbum); + } + + assetsFromSelectedAlbums.add( + BackupCandidate( + asset: asset, + albums: existingAlbums, + ), + ); + } + } + + for (final bc in assetsFromSelectedAlbums) { + debugPrint("Asset: ${bc.asset.id} | Albums: ${bc.albums}"); } for (final album in state.excludedBackupAlbums) { @@ -318,11 +343,17 @@ class BackupNotifier extends StateNotifier { start: 0, end: assetCount, ); - assetsFromExcludedAlbums.addAll(assets); + + assets.forEach((asset) { + assetsFromExcludedAlbums.add( + BackupCandidate(asset: asset, albums: [album.name]), + ); + }); } - final Set allUniqueAssets = + final Set allUniqueAssets = assetsFromSelectedAlbums.difference(assetsFromExcludedAlbums); + final allAssetsInDatabase = await _backupService.getDeviceBackupAsset(); if (allAssetsInDatabase == null) { @@ -331,14 +362,14 @@ class BackupNotifier extends StateNotifier { // Find asset that were backup from selected albums final Set selectedAlbumsBackupAssets = - Set.from(allUniqueAssets.map((e) => e.id)); + Set.from(allUniqueAssets.map((e) => e.asset.id)); selectedAlbumsBackupAssets .removeWhere((assetId) => !allAssetsInDatabase.contains(assetId)); // Remove duplicated asset from all unique assets allUniqueAssets.removeWhere( - (asset) => duplicatedAssetIds.contains(asset.id), + (candidate) => duplicatedAssetIds.contains(candidate.asset.id), ); if (allUniqueAssets.isEmpty) { @@ -433,10 +464,10 @@ class BackupNotifier extends StateNotifier { return; } - Set assetsWillBeBackup = Set.from(state.allUniqueAssets); + Set assetsWillBeBackup = Set.from(state.allUniqueAssets); // Remove item that has already been backed up for (final assetId in state.allAssetsInDatabase) { - assetsWillBeBackup.removeWhere((e) => e.id == assetId); + assetsWillBeBackup.removeWhere((e) => e.asset.id == assetId); } if (assetsWillBeBackup.isEmpty) { @@ -505,7 +536,7 @@ class BackupNotifier extends StateNotifier { if (isDuplicated) { state = state.copyWith( allUniqueAssets: state.allUniqueAssets - .where((asset) => asset.id != deviceAssetId) + .where((candidate) => candidate.asset.id != deviceAssetId) .toSet(), ); } else { @@ -521,10 +552,11 @@ class BackupNotifier extends StateNotifier { if (state.allUniqueAssets.length - state.selectedAlbumsBackupAssetsIds.length == 0) { - final latestAssetBackup = - state.allUniqueAssets.map((e) => e.modifiedDateTime).reduce( - (v, e) => e.isAfter(v) ? e : v, - ); + final latestAssetBackup = state.allUniqueAssets + .map((candidate) => candidate.asset.modifiedDateTime) + .reduce( + (v, e) => e.isAfter(v) ? e : v, + ); state = state.copyWith( selectedBackupAlbums: state.selectedBackupAlbums .map((e) => e.copyWith(lastBackup: latestAssetBackup)) diff --git a/mobile/lib/providers/backup/manual_upload.provider.dart b/mobile/lib/providers/backup/manual_upload.provider.dart index b446711226324..cb916c91688c1 100644 --- a/mobile/lib/providers/backup/manual_upload.provider.dart +++ b/mobile/lib/providers/backup/manual_upload.provider.dart @@ -249,15 +249,16 @@ class ManualUploadNotifier extends StateNotifier { state.copyWith(showDetailedNotification: showDetailedNotification); final pmProgressHandler = Platform.isIOS ? PMProgressHandler() : null; - final bool ok = await ref.read(backupServiceProvider).backupAsset( - allUploadAssets, - state.cancelToken, - pmProgressHandler, - _onAssetUploaded, - _onProgress, - _onSetCurrentBackupAsset, - _onAssetUploadError, - ); + // final bool ok = await ref.read(backupServiceProvider).backupAsset( + // allUploadAssets, + // state.cancelToken, + // pmProgressHandler, + // _onAssetUploaded, + // _onProgress, + // _onSetCurrentBackupAsset, + // _onAssetUploadError, + // ); + final ok = true; // Close detailed notification await _localNotificationService.closeNotification( diff --git a/mobile/lib/services/background.service.dart b/mobile/lib/services/background.service.dart index ba8f5c01ed963..27a0b86fef326 100644 --- a/mobile/lib/services/background.service.dart +++ b/mobile/lib/services/background.service.dart @@ -457,23 +457,23 @@ class BackgroundService { _cancellationToken = CancellationToken(); final pmProgressHandler = Platform.isIOS ? PMProgressHandler() : null; - final bool ok = await backupService.backupAsset( - toUpload, - _cancellationToken!, - pmProgressHandler, - notifyTotalProgress ? _onAssetUploaded : (assetId, deviceId, isDup) {}, - notifySingleProgress ? _onProgress : (sent, total) {}, - notifySingleProgress ? _onSetCurrentBackupAsset : (asset) {}, - _onBackupError, - sortAssets: true, - ); - if (!ok && !_cancellationToken!.isCancelled) { - _showErrorNotification( - title: "backup_background_service_error_title".tr(), - content: "backup_background_service_backup_failed_message".tr(), - ); - } - return ok; + // final bool ok = await backupService.backupAsset( + // toUpload, + // _cancellationToken!, + // pmProgressHandler, + // notifyTotalProgress ? _onAssetUploaded : (assetId, deviceId, isDup) {}, + // notifySingleProgress ? _onProgress : (sent, total) {}, + // notifySingleProgress ? _onSetCurrentBackupAsset : (asset) {}, + // _onBackupError, + // sortAssets: true, + // ); + // if (!ok && !_cancellationToken!.isCancelled) { + // _showErrorNotification( + // title: "backup_background_service_error_title".tr(), + // content: "backup_background_service_backup_failed_message".tr(), + // ); + // } + return true; } void _onAssetUploaded(String deviceAssetId, String deviceId, bool isDup) { diff --git a/mobile/lib/services/backup.service.dart b/mobile/lib/services/backup.service.dart index 64d683dc2ae83..49e95d6f95fc0 100644 --- a/mobile/lib/services/backup.service.dart +++ b/mobile/lib/services/backup.service.dart @@ -9,6 +9,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/entities/backup_album.entity.dart'; import 'package:immich_mobile/entities/duplicated_asset.entity.dart'; import 'package:immich_mobile/entities/store.entity.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'; import 'package:immich_mobile/providers/api.provider.dart'; @@ -200,7 +201,7 @@ class BackupService { } Future backupAsset( - Iterable assetList, + Iterable assetList, http.CancellationToken cancelToken, PMProgressHandler? pmProgressHandler, Function(String, String, bool) uploadSuccessCb, @@ -230,19 +231,26 @@ class BackupService { await PhotoManager.requestPermissionExtend(); } - List assetsToUpload = sortAssets + List assetsToUpload = sortAssets // Upload images before video assets // these are further sorted by using their creation date ? assetList.sorted( (a, b) { - final cmp = a.typeInt - b.typeInt; + final cmp = a.asset.typeInt - b.asset.typeInt; if (cmp != 0) return cmp; - return a.createDateTime.compareTo(b.createDateTime); + return a.asset.createDateTime.compareTo(b.asset.createDateTime); }, ) : assetList.toList(); - - for (var entity in assetsToUpload) { + var count = 0; + for (var candidate in assetsToUpload) { + final AssetEntity entity = candidate.asset; + final List albums = candidate.albums; + count++; + print( + "[$count] Uploading asset ${entity.id} | ${albums} | ${entity.createDateTime}", + ); + continue; File? file; File? livePhotoFile; From 5942732823e75c6bf4639a8849a9999831d29d69 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 21 Aug 2024 12:53:34 -0500 Subject: [PATCH 02/34] sorting for background backup --- .../lib/providers/backup/backup.provider.dart | 18 +-- .../backup/manual_upload.provider.dart | 24 ++-- mobile/lib/services/background.service.dart | 37 +++--- mobile/lib/services/backup.service.dart | 122 +++++++++++------- 4 files changed, 113 insertions(+), 88 deletions(-) diff --git a/mobile/lib/providers/backup/backup.provider.dart b/mobile/lib/providers/backup/backup.provider.dart index 18c955cd5d67c..015a120bd754c 100644 --- a/mobile/lib/providers/backup/backup.provider.dart +++ b/mobile/lib/providers/backup/backup.provider.dart @@ -294,7 +294,6 @@ class BackupNotifier extends StateNotifier { final Set assetsFromSelectedAlbums = {}; final Set assetsFromExcludedAlbums = {}; - debugPrint("Selected Albums: ${state.selectedBackupAlbums}"); for (final album in state.selectedBackupAlbums) { final assetCount = await album.albumEntity.assetCountAsync; @@ -307,31 +306,28 @@ class BackupNotifier extends StateNotifier { end: assetCount, ); + // Add album's name to the asset info for (final asset in assets) { - List existingAlbums = [album.name]; + List albums = [album.name]; - final inOtherAlbum = assetsFromSelectedAlbums.firstWhereOrNull( + final existingAsset = assetsFromSelectedAlbums.firstWhereOrNull( (a) => a.asset.id == asset.id, ); - if (inOtherAlbum != null) { - existingAlbums.addAll(inOtherAlbum.albums); - assetsFromSelectedAlbums.remove(inOtherAlbum); + if (existingAsset != null) { + albums.addAll(existingAsset.albums); + assetsFromSelectedAlbums.remove(existingAsset); } assetsFromSelectedAlbums.add( BackupCandidate( asset: asset, - albums: existingAlbums, + albums: albums, ), ); } } - for (final bc in assetsFromSelectedAlbums) { - debugPrint("Asset: ${bc.asset.id} | Albums: ${bc.albums}"); - } - for (final album in state.excludedBackupAlbums) { final assetCount = await album.albumEntity.assetCountAsync; diff --git a/mobile/lib/providers/backup/manual_upload.provider.dart b/mobile/lib/providers/backup/manual_upload.provider.dart index cb916c91688c1..b50b8d198d099 100644 --- a/mobile/lib/providers/backup/manual_upload.provider.dart +++ b/mobile/lib/providers/backup/manual_upload.provider.dart @@ -6,6 +6,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/widgets.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; import 'package:immich_mobile/services/background.service.dart'; import 'package:immich_mobile/models/backup/backup_state.model.dart'; import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; @@ -209,7 +210,9 @@ class ManualUploadNotifier extends StateNotifier { ); } - Set allUploadAssets = allAssetsFromDevice.nonNulls.toSet(); + Set allUploadAssets = allAssetsFromDevice.nonNulls + .map((a) => BackupCandidate(asset: a, albums: [])) + .toSet(); if (allUploadAssets.isEmpty) { debugPrint("[_startUpload] No Assets to upload - Abort Process"); @@ -249,16 +252,15 @@ class ManualUploadNotifier extends StateNotifier { state.copyWith(showDetailedNotification: showDetailedNotification); final pmProgressHandler = Platform.isIOS ? PMProgressHandler() : null; - // final bool ok = await ref.read(backupServiceProvider).backupAsset( - // allUploadAssets, - // state.cancelToken, - // pmProgressHandler, - // _onAssetUploaded, - // _onProgress, - // _onSetCurrentBackupAsset, - // _onAssetUploadError, - // ); - final ok = true; + final bool ok = await ref.read(backupServiceProvider).backupAsset( + allUploadAssets, + state.cancelToken, + pmProgressHandler, + _onAssetUploaded, + _onProgress, + _onSetCurrentBackupAsset, + _onAssetUploadError, + ); // Close detailed notification await _localNotificationService.closeNotification( diff --git a/mobile/lib/services/background.service.dart b/mobile/lib/services/background.service.dart index 27a0b86fef326..585b8f861b78c 100644 --- a/mobile/lib/services/background.service.dart +++ b/mobile/lib/services/background.service.dart @@ -10,6 +10,7 @@ import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/main.dart'; +import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; import 'package:immich_mobile/services/localization.service.dart'; import 'package:immich_mobile/entities/backup_album.entity.dart'; import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; @@ -416,7 +417,7 @@ class BackgroundService { return false; } - List toUpload = await backupService.buildUploadCandidates( + Set toUpload = await backupService.buildUploadCandidates( selectedAlbums, excludedAlbums, ); @@ -457,23 +458,23 @@ class BackgroundService { _cancellationToken = CancellationToken(); final pmProgressHandler = Platform.isIOS ? PMProgressHandler() : null; - // final bool ok = await backupService.backupAsset( - // toUpload, - // _cancellationToken!, - // pmProgressHandler, - // notifyTotalProgress ? _onAssetUploaded : (assetId, deviceId, isDup) {}, - // notifySingleProgress ? _onProgress : (sent, total) {}, - // notifySingleProgress ? _onSetCurrentBackupAsset : (asset) {}, - // _onBackupError, - // sortAssets: true, - // ); - // if (!ok && !_cancellationToken!.isCancelled) { - // _showErrorNotification( - // title: "backup_background_service_error_title".tr(), - // content: "backup_background_service_backup_failed_message".tr(), - // ); - // } - return true; + final bool ok = await backupService.backupAsset( + toUpload, + _cancellationToken!, + pmProgressHandler, + notifyTotalProgress ? _onAssetUploaded : (assetId, deviceId, isDup) {}, + notifySingleProgress ? _onProgress : (sent, total) {}, + notifySingleProgress ? _onSetCurrentBackupAsset : (asset) {}, + _onBackupError, + sortAssets: true, + ); + if (!ok && !_cancellationToken!.isCancelled) { + _showErrorNotification( + title: "backup_background_service_error_title".tr(), + content: "backup_background_service_backup_failed_message".tr(), + ); + } + return ok; } void _onAssetUploaded(String deviceAssetId, String deviceId, bool isDup) { diff --git a/mobile/lib/services/backup.service.dart b/mobile/lib/services/backup.service.dart index 49e95d6f95fc0..89af9dc782cb6 100644 --- a/mobile/lib/services/backup.service.dart +++ b/mobile/lib/services/backup.service.dart @@ -71,7 +71,7 @@ class BackupService { _db.backupAlbums.filter().selectionEqualTo(BackupSelection.exclude); /// Returns all assets newer than the last successful backup per album - Future> buildUploadCandidates( + Future> buildUploadCandidates( List selectedBackupAlbums, List excludedBackupAlbums, ) async { @@ -82,34 +82,30 @@ class BackupService { imageOption: const FilterOption(needTitle: true), videoOption: const FilterOption(needTitle: true), ); + final now = DateTime.now(); final List selectedAlbums = await _loadAlbumsWithTimeFilter(selectedBackupAlbums, filter, now); if (selectedAlbums.every((e) => e == null)) { - return []; - } - final int allIdx = selectedAlbums.indexWhere((e) => e != null && e.isAll); - if (allIdx != -1) { - final List excludedAlbums = - await _loadAlbumsWithTimeFilter(excludedBackupAlbums, filter, now); - final List toAdd = await _fetchAssetsAndUpdateLastBackup( - selectedAlbums.slice(allIdx, allIdx + 1), - selectedBackupAlbums.slice(allIdx, allIdx + 1), - now, - ); - final List toRemove = await _fetchAssetsAndUpdateLastBackup( - excludedAlbums, - excludedBackupAlbums, - now, - ); - return toAdd.toSet().difference(toRemove.toSet()).toList(); - } else { - return await _fetchAssetsAndUpdateLastBackup( - selectedAlbums, - selectedBackupAlbums, - now, - ); + return {}; } + + final List excludedAlbums = + await _loadAlbumsWithTimeFilter(excludedBackupAlbums, filter, now); + + final Set toAdd = await _fetchAssetsAndUpdateLastBackup( + selectedAlbums, + selectedBackupAlbums, + now, + ); + + final Set toRemove = await _fetchAssetsAndUpdateLastBackup( + excludedAlbums, + excludedBackupAlbums, + now, + ); + + return toAdd.difference(toRemove); } Future> _loadAlbumsWithTimeFilter( @@ -140,48 +136,75 @@ class BackupService { return result; } - Future> _fetchAssetsAndUpdateLastBackup( + Future> _fetchAssetsAndUpdateLastBackup( List albums, List backupAlbums, DateTime now, ) async { - List result = []; - for (int i = 0; i < albums.length; i++) { - final AssetPathEntity? a = albums[i]; - if (a != null && - a.lastModified?.isBefore(backupAlbums[i].lastBackup) != true) { - result.addAll( - await a.getAssetListRange(start: 0, end: await a.assetCountAsync), + debugPrint("_fetchAssetsAndUpdateLastBackup"); + Set candidate = {}; + + for (final album in albums) { + if (album == null) { + continue; + } + final assets = await album.getAssetListRange( + start: 0, + end: await album.assetCountAsync, + ); + + // Add album's name to the asset info + for (final asset in assets) { + List albums = [album.name]; + + final existingAsset = candidate.firstWhereOrNull( + (a) => a.asset.id == asset.id, + ); + + if (existingAsset != null) { + albums.addAll(existingAsset.albums); + candidate.remove(existingAsset); + } + + candidate.add( + BackupCandidate( + asset: asset, + albums: albums, + ), ); - backupAlbums[i].lastBackup = now; } + + final idx = albums.indexOf(album); + backupAlbums[idx].lastBackup = now; } - return result; + + return candidate; } /// Returns a new list of assets not yet uploaded - Future> removeAlreadyUploadedAssets( - List candidates, + Future> removeAlreadyUploadedAssets( + Set candidates, ) async { if (candidates.isEmpty) { return candidates; } + final Set duplicatedAssetIds = await getDuplicatedAssetIds(); - candidates = duplicatedAssetIds.isEmpty - ? candidates - : candidates - .whereNot((asset) => duplicatedAssetIds.contains(asset.id)) - .toList(); + candidates.removeWhere( + (candidate) => duplicatedAssetIds.contains(candidate.asset.id), + ); + if (candidates.isEmpty) { return candidates; } + final Set existing = {}; try { final String deviceId = Store.get(StoreKey.deviceId); final CheckExistingAssetsResponseDto? duplicates = await _apiService.assetsApi.checkExistingAssets( CheckExistingAssetsDto( - deviceAssetIds: candidates.map((e) => e.id).toList(), + deviceAssetIds: candidates.map((c) => c.asset.id).toList(), deviceId: deviceId, ), ); @@ -195,9 +218,12 @@ class BackupService { existing.addAll(allAssetsInDatabase); } } - return existing.isEmpty - ? candidates - : candidates.whereNot((e) => existing.contains(e.id)).toList(); + + if (existing.isNotEmpty) { + candidates.removeWhere((c) => !existing.contains(c.asset.id)); + } + + return candidates; } Future backupAsset( @@ -243,12 +269,12 @@ class BackupService { ) : assetList.toList(); var count = 0; - for (var candidate in assetsToUpload) { + for (final candidate in assetsToUpload) { final AssetEntity entity = candidate.asset; final List albums = candidate.albums; count++; - print( - "[$count] Uploading asset ${entity.id} | ${albums} | ${entity.createDateTime}", + debugPrint( + "[$count] -Uploading asset ${entity.id} | ${albums} | ${entity.createDateTime}", ); continue; File? file; From 00991f743f70cedd658dcc1df3af9e33a20f29c4 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 21 Aug 2024 14:36:50 -0500 Subject: [PATCH 03/34] background upload works --- mobile/lib/services/backup.service.dart | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/mobile/lib/services/backup.service.dart b/mobile/lib/services/backup.service.dart index 89af9dc782cb6..5a198fdf303fc 100644 --- a/mobile/lib/services/backup.service.dart +++ b/mobile/lib/services/backup.service.dart @@ -141,13 +141,13 @@ class BackupService { List backupAlbums, DateTime now, ) async { - debugPrint("_fetchAssetsAndUpdateLastBackup"); Set candidate = {}; for (final album in albums) { if (album == null) { continue; } + final assets = await album.getAssetListRange( start: 0, end: await album.assetCountAsync, @@ -220,7 +220,7 @@ class BackupService { } if (existing.isNotEmpty) { - candidates.removeWhere((c) => !existing.contains(c.asset.id)); + candidates.removeWhere((c) => existing.contains(c.asset.id)); } return candidates; @@ -268,15 +268,10 @@ class BackupService { }, ) : assetList.toList(); - var count = 0; for (final candidate in assetsToUpload) { final AssetEntity entity = candidate.asset; final List albums = candidate.albums; - count++; - debugPrint( - "[$count] -Uploading asset ${entity.id} | ${albums} | ${entity.createDateTime}", - ); - continue; + File? file; File? livePhotoFile; @@ -358,6 +353,7 @@ class BackupService { entity.modifiedDateTime.toUtc().toIso8601String(); baseRequest.fields['isFavorite'] = entity.isFavorite.toString(); baseRequest.fields['duration'] = entity.videoDuration.toString(); + baseRequest.fields['albums'] = json.encode(albums); baseRequest.files.add(assetRawUploadData); From c15682eff46348fe78365b1c78a0e9e92a05ccbb Mon Sep 17 00:00:00 2001 From: Alex Tran Date: Wed, 21 Aug 2024 15:05:42 -0500 Subject: [PATCH 04/34] transform fields string array to javascript array --- open-api/immich-openapi-specs.json | 12 ++++++++++++ server/src/dtos/asset-media.dto.ts | 8 ++++++-- server/src/validation.ts | 2 ++ 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 35fecdb1ee29f..94cd981788db5 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -8052,6 +8052,12 @@ }, "AssetMediaCreateDto": { "properties": { + "albums": { + "items": { + "type": "string" + }, + "type": "array" + }, "assetData": { "format": "binary", "type": "string" @@ -8105,6 +8111,12 @@ }, "AssetMediaReplaceDto": { "properties": { + "albums": { + "items": { + "type": "string" + }, + "type": "array" + }, "assetData": { "format": "binary", "type": "string" diff --git a/server/src/dtos/asset-media.dto.ts b/server/src/dtos/asset-media.dto.ts index e9e346c4cb593..d1c36616d757a 100644 --- a/server/src/dtos/asset-media.dto.ts +++ b/server/src/dtos/asset-media.dto.ts @@ -1,7 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; +import { Transform, Type } from 'class-transformer'; import { ArrayNotEmpty, IsArray, IsEnum, IsNotEmpty, IsString, ValidateNested } from 'class-validator'; -import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation'; +import { Optional, toStringArray, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation'; export enum AssetMediaSize { PREVIEW = 'preview', @@ -40,6 +40,10 @@ class AssetMediaBase { @IsString() duration?: string; + @Optional() + @Transform(toStringArray) + albums?: string[]; + // The properties below are added to correctly generate the API docs // and client SDKs. Validation should be handled in the controller. @ApiProperty({ type: 'string', format: 'binary' }) diff --git a/server/src/validation.ts b/server/src/validation.ts index 81b309d66358f..c1efc89f04a75 100644 --- a/server/src/validation.ts +++ b/server/src/validation.ts @@ -167,6 +167,8 @@ export function validateCronExpression(expression: string) { type IValue = { value: unknown }; +export const toStringArray = ({ value }: IValue) => (typeof value === 'string' ? value.split(',') : value); + export const toEmail = ({ value }: IValue) => (typeof value === 'string' ? value.toLowerCase() : value); export const toSanitized = ({ value }: IValue) => { From ee29bca3cea29b78c676e07218664fd909a44ee9 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 21 Aug 2024 15:05:53 -0500 Subject: [PATCH 05/34] send json array --- mobile/lib/services/backup.service.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mobile/lib/services/backup.service.dart b/mobile/lib/services/backup.service.dart index 5a198fdf303fc..f6529fb6e919d 100644 --- a/mobile/lib/services/backup.service.dart +++ b/mobile/lib/services/backup.service.dart @@ -353,7 +353,7 @@ class BackupService { entity.modifiedDateTime.toUtc().toIso8601String(); baseRequest.fields['isFavorite'] = entity.isFavorite.toString(); baseRequest.fields['duration'] = entity.videoDuration.toString(); - baseRequest.fields['albums'] = json.encode(albums); + baseRequest.fields['albums'] = albums.join(','); baseRequest.files.add(assetRawUploadData); @@ -400,6 +400,7 @@ class BackupService { debugPrint( "Error(${error['statusCode']}) uploading ${entity.id} | $originalFileName | Created on ${entity.createDateTime} | ${error['error']}", ); + debugPrint(error); errorCb( ErrorUploadAsset( From a1f130800a1dd99787a83e4995340b1806eda0c8 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 21 Aug 2024 21:36:57 -0500 Subject: [PATCH 06/34] generate sql --- mobile/lib/services/background.service.dart | 21 ++++++-- mobile/lib/services/backup.service.dart | 54 ++++++++++++--------- 2 files changed, 47 insertions(+), 28 deletions(-) diff --git a/mobile/lib/services/background.service.dart b/mobile/lib/services/background.service.dart index 585b8f861b78c..e9cbd688f5e63 100644 --- a/mobile/lib/services/background.service.dart +++ b/mobile/lib/services/background.service.dart @@ -462,10 +462,12 @@ class BackgroundService { toUpload, _cancellationToken!, pmProgressHandler, - notifyTotalProgress ? _onAssetUploaded : (assetId, deviceId, isDup) {}, - notifySingleProgress ? _onProgress : (sent, total) {}, - notifySingleProgress ? _onSetCurrentBackupAsset : (asset) {}, - _onBackupError, + onSuccess: + notifyTotalProgress ? onAssetUploaded : (assetId, deviceId, isDup) {}, + onProgress: notifySingleProgress ? _onProgress : (bytes, totalBytes) {}, + onCurrentAsset: + notifySingleProgress ? _onSetCurrentBackupAsset : (asset) {}, + onError: _onBackupError, sortAssets: true, ); if (!ok && !_cancellationToken!.isCancelled) { @@ -477,7 +479,16 @@ class BackgroundService { return ok; } - void _onAssetUploaded(String deviceAssetId, String deviceId, bool isDup) { + void onAssetUploaded( + String deviceAssetId, + String deviceId, + bool isDuplicate, { + bool shouldNotify = false, + }) { + if (!shouldNotify) { + return; + } + _uploadedAssetsCount++; _throttledNotifiy(); } diff --git a/mobile/lib/services/backup.service.dart b/mobile/lib/services/backup.service.dart index f6529fb6e919d..ca26ad1b1e2aa 100644 --- a/mobile/lib/services/backup.service.dart +++ b/mobile/lib/services/backup.service.dart @@ -229,12 +229,16 @@ class BackupService { Future backupAsset( Iterable assetList, http.CancellationToken cancelToken, - PMProgressHandler? pmProgressHandler, - Function(String, String, bool) uploadSuccessCb, - Function(int, int) uploadProgressCb, - Function(CurrentUploadAsset) setCurrentUploadAssetCb, - Function(ErrorUploadAsset) errorCb, { + PMProgressHandler? pmProgressHandler, { bool sortAssets = false, + required void Function({ + BackupCandidate asset, + String deviceId, + bool isDuplicate, + }) onSuccess, + required void Function(int bytes, int totalBytes) onProgress, + required void Function(CurrentUploadAsset asset) onCurrentAsset, + required void Function(ErrorUploadAsset error) onError, }) async { final bool isIgnoreIcloudAssets = _appSetting.getSetting(AppSettingsEnum.ignoreIcloudAssets); @@ -257,7 +261,7 @@ class BackupService { await PhotoManager.requestPermissionExtend(); } - List assetsToUpload = sortAssets + List candidates = sortAssets // Upload images before video assets // these are further sorted by using their creation date ? assetList.sorted( @@ -268,7 +272,8 @@ class BackupService { }, ) : assetList.toList(); - for (final candidate in assetsToUpload) { + + for (final candidate in candidates) { final AssetEntity entity = candidate.asset; final List albums = candidate.albums; @@ -286,7 +291,7 @@ class BackupService { continue; } - setCurrentUploadAssetCb( + onCurrentAsset( CurrentUploadAsset( id: entity.id, fileCreatedAt: entity.createDateTime.year == 1970 @@ -328,19 +333,18 @@ class BackupService { } } - var fileStream = file.openRead(); - var assetRawUploadData = http.MultipartFile( + final fileStream = file.openRead(); + final assetRawUploadData = http.MultipartFile( "assetData", fileStream, file.lengthSync(), filename: originalFileName, ); - var baseRequest = MultipartRequest( + final baseRequest = MultipartRequest( 'POST', Uri.parse('$savedEndpoint/assets'), - onProgress: ((bytes, totalBytes) => - uploadProgressCb(bytes, totalBytes)), + onProgress: ((bytes, totalBytes) => onProgress(bytes, totalBytes)), ); baseRequest.headers.addAll(ApiService.getRequestHeaders()); baseRequest.headers["Transfer-Encoding"] = "chunked"; @@ -357,9 +361,9 @@ class BackupService { baseRequest.files.add(assetRawUploadData); - var fileSize = file.lengthSync(); + final fileSize = file.lengthSync(); - setCurrentUploadAssetCb( + onCurrentAsset( CurrentUploadAsset( id: entity.id, fileCreatedAt: entity.createDateTime.year == 1970 @@ -386,23 +390,23 @@ class BackupService { baseRequest.fields['livePhotoVideoId'] = livePhotoVideoId; } - var response = await httpClient.send( + final response = await httpClient.send( baseRequest, cancellationToken: cancelToken, ); - var responseBody = jsonDecode(await response.stream.bytesToString()); + final responseBody = + jsonDecode(await response.stream.bytesToString()); if (![200, 201].contains(response.statusCode)) { - var error = responseBody; - var errorMessage = error['message'] ?? error['error']; + final error = responseBody; + final errorMessage = error['message'] ?? error['error']; debugPrint( "Error(${error['statusCode']}) uploading ${entity.id} | $originalFileName | Created on ${entity.createDateTime} | ${error['error']}", ); - debugPrint(error); - errorCb( + onError( ErrorUploadAsset( asset: entity, id: entity.id, @@ -420,13 +424,17 @@ class BackupService { continue; } - var isDuplicate = false; + bool isDuplicate = false; if (response.statusCode == 200) { isDuplicate = true; duplicatedAssetIds.add(entity.id); } - uploadSuccessCb(entity.id, deviceId, isDuplicate); + onSuccess( + asset: candidate, + deviceId: deviceId, + isDuplicate: isDuplicate, + ); } } on http.CancelledException { debugPrint("Backup was cancelled by the user"); From b671bc12b3b7b6830b1eabc28f270268f4370b1a Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 22 Aug 2024 08:51:32 -0500 Subject: [PATCH 07/34] refactor upload callback --- .../backup/success_upload_asset.model.dart | 42 +++++++++++++++++++ .../lib/providers/backup/backup.provider.dart | 28 ++++++------- .../backup/manual_upload.provider.dart | 17 ++++---- mobile/lib/services/background.service.dart | 39 +++++++++++------ mobile/lib/services/backup.service.dart | 19 ++++----- 5 files changed, 98 insertions(+), 47 deletions(-) create mode 100644 mobile/lib/models/backup/success_upload_asset.model.dart diff --git a/mobile/lib/models/backup/success_upload_asset.model.dart b/mobile/lib/models/backup/success_upload_asset.model.dart new file mode 100644 index 0000000000000..69ecbe73b8c34 --- /dev/null +++ b/mobile/lib/models/backup/success_upload_asset.model.dart @@ -0,0 +1,42 @@ +import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; + +class SuccessUploadAsset { + final BackupCandidate asset; + final String deviceAssetId; + final bool isDuplicate; + + SuccessUploadAsset({ + required this.asset, + required this.deviceAssetId, + required this.isDuplicate, + }); + + SuccessUploadAsset copyWith({ + BackupCandidate? asset, + String? deviceAssetId, + bool? isDuplicate, + }) { + return SuccessUploadAsset( + asset: asset ?? this.asset, + deviceAssetId: deviceAssetId ?? this.deviceAssetId, + isDuplicate: isDuplicate ?? this.isDuplicate, + ); + } + + @override + String toString() => + 'SuccessUploadAsset(asset: $asset, deviceId: $deviceAssetId, isDuplicate: $isDuplicate)'; + + @override + bool operator ==(covariant SuccessUploadAsset other) { + if (identical(this, other)) return true; + + return other.asset == asset && + other.deviceAssetId == deviceAssetId && + other.isDuplicate == isDuplicate; + } + + @override + int get hashCode => + asset.hashCode ^ deviceAssetId.hashCode ^ isDuplicate.hashCode; +} diff --git a/mobile/lib/providers/backup/backup.provider.dart b/mobile/lib/providers/backup/backup.provider.dart index 015a120bd754c..4f51a78395fe1 100644 --- a/mobile/lib/providers/backup/backup.provider.dart +++ b/mobile/lib/providers/backup/backup.provider.dart @@ -10,6 +10,7 @@ import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; import 'package:immich_mobile/models/backup/backup_state.model.dart'; import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; import 'package:immich_mobile/models/backup/error_upload_asset.model.dart'; +import 'package:immich_mobile/models/backup/success_upload_asset.model.dart'; import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart'; import 'package:immich_mobile/services/background.service.dart'; import 'package:immich_mobile/services/backup.service.dart'; @@ -483,11 +484,11 @@ class BackupNotifier extends StateNotifier { await _backupService.backupAsset( assetsWillBeBackup, state.cancelToken, - pmProgressHandler, - _onAssetUploaded, - _onUploadProgress, - _onSetCurrentBackupAsset, - _onBackupError, + pmProgressHandler: pmProgressHandler, + onSuccess: _onAssetUploaded, + onProgress: _onUploadProgress, + onCurrentAsset: _onSetCurrentBackupAsset, + onError: _onBackupError, ); await notifyBackgroundServiceCanRun(); } else { @@ -524,24 +525,23 @@ class BackupNotifier extends StateNotifier { ); } - void _onAssetUploaded( - String deviceAssetId, - String deviceId, - bool isDuplicated, - ) { - if (isDuplicated) { + void _onAssetUploaded(SuccessUploadAsset result) { + if (result.isDuplicate) { state = state.copyWith( allUniqueAssets: state.allUniqueAssets - .where((candidate) => candidate.asset.id != deviceAssetId) + .where((candidate) => candidate.asset.id != result.deviceAssetId) .toSet(), ); } else { state = state.copyWith( selectedAlbumsBackupAssetsIds: { ...state.selectedAlbumsBackupAssetsIds, - deviceAssetId, + result.deviceAssetId, }, - allAssetsInDatabase: [...state.allAssetsInDatabase, deviceAssetId], + allAssetsInDatabase: [ + ...state.allAssetsInDatabase, + result.deviceAssetId, + ], ); } diff --git a/mobile/lib/providers/backup/manual_upload.provider.dart b/mobile/lib/providers/backup/manual_upload.provider.dart index b50b8d198d099..410ce22a44a8e 100644 --- a/mobile/lib/providers/backup/manual_upload.provider.dart +++ b/mobile/lib/providers/backup/manual_upload.provider.dart @@ -7,6 +7,7 @@ import 'package:flutter/widgets.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; +import 'package:immich_mobile/models/backup/success_upload_asset.model.dart'; import 'package:immich_mobile/services/background.service.dart'; import 'package:immich_mobile/models/backup/backup_state.model.dart'; import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; @@ -116,11 +117,7 @@ class ManualUploadNotifier extends StateNotifier { } } - void _onAssetUploaded( - String deviceAssetId, - String deviceId, - bool isDuplicated, - ) { + void _onAssetUploaded(SuccessUploadAsset result) { state = state.copyWith(successfulUploads: state.successfulUploads + 1); _backupProvider.updateDiskInfo(); } @@ -255,11 +252,11 @@ class ManualUploadNotifier extends StateNotifier { final bool ok = await ref.read(backupServiceProvider).backupAsset( allUploadAssets, state.cancelToken, - pmProgressHandler, - _onAssetUploaded, - _onProgress, - _onSetCurrentBackupAsset, - _onAssetUploadError, + pmProgressHandler: pmProgressHandler, + onSuccess: _onAssetUploaded, + onProgress: _onProgress, + onCurrentAsset: _onSetCurrentBackupAsset, + onError: _onAssetUploadError, ); // Close detailed notification diff --git a/mobile/lib/services/background.service.dart b/mobile/lib/services/background.service.dart index e9cbd688f5e63..9fde33589a839 100644 --- a/mobile/lib/services/background.service.dart +++ b/mobile/lib/services/background.service.dart @@ -11,6 +11,7 @@ import 'package:flutter/widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/main.dart'; import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; +import 'package:immich_mobile/models/backup/success_upload_asset.model.dart'; import 'package:immich_mobile/services/localization.service.dart'; import 'package:immich_mobile/entities/backup_album.entity.dart'; import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; @@ -461,28 +462,29 @@ class BackgroundService { final bool ok = await backupService.backupAsset( toUpload, _cancellationToken!, - pmProgressHandler, - onSuccess: - notifyTotalProgress ? onAssetUploaded : (assetId, deviceId, isDup) {}, - onProgress: notifySingleProgress ? _onProgress : (bytes, totalBytes) {}, - onCurrentAsset: - notifySingleProgress ? _onSetCurrentBackupAsset : (asset) {}, + pmProgressHandler: pmProgressHandler, + onSuccess: (result) => + _onAssetUploaded(result: result, shouldNotify: notifyTotalProgress), + onProgress: (bytes, totalBytes) => + _onProgress(bytes, totalBytes, shouldNotify: notifySingleProgress), + onCurrentAsset: (asset) => + _onSetCurrentBackupAsset(asset, shouldNotify: notifySingleProgress), onError: _onBackupError, sortAssets: true, ); + if (!ok && !_cancellationToken!.isCancelled) { _showErrorNotification( title: "backup_background_service_error_title".tr(), content: "backup_background_service_backup_failed_message".tr(), ); } + return ok; } - void onAssetUploaded( - String deviceAssetId, - String deviceId, - bool isDuplicate, { + void _onAssetUploaded({ + required SuccessUploadAsset result, bool shouldNotify = false, }) { if (!shouldNotify) { @@ -493,8 +495,12 @@ class BackgroundService { _throttledNotifiy(); } - void _onProgress(int sent, int total) { - _throttledDetailNotify(progress: sent, total: total); + void _onProgress(int bytes, int totalBytes, {bool shouldNotify = false}) { + if (!shouldNotify) { + return; + } + + _throttledDetailNotify(progress: bytes, total: totalBytes); } void _updateDetailProgress(String? title, int progress, int total) { @@ -534,7 +540,14 @@ class BackgroundService { ); } - void _onSetCurrentBackupAsset(CurrentUploadAsset currentUploadAsset) { + void _onSetCurrentBackupAsset( + CurrentUploadAsset currentUploadAsset, { + bool shouldNotify = false, + }) { + if (!shouldNotify) { + return; + } + _throttledDetailNotify.title = "backup_background_service_current_upload_notification" .tr(args: [currentUploadAsset.fileName]); diff --git a/mobile/lib/services/backup.service.dart b/mobile/lib/services/backup.service.dart index ca26ad1b1e2aa..4f0fe07248081 100644 --- a/mobile/lib/services/backup.service.dart +++ b/mobile/lib/services/backup.service.dart @@ -12,6 +12,7 @@ import 'package:immich_mobile/entities/store.entity.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'; +import 'package:immich_mobile/models/backup/success_upload_asset.model.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/db.provider.dart'; @@ -228,14 +229,10 @@ class BackupService { Future backupAsset( Iterable assetList, - http.CancellationToken cancelToken, - PMProgressHandler? pmProgressHandler, { + http.CancellationToken cancelToken, { bool sortAssets = false, - required void Function({ - BackupCandidate asset, - String deviceId, - bool isDuplicate, - }) onSuccess, + PMProgressHandler? pmProgressHandler, + required void Function(SuccessUploadAsset result) onSuccess, required void Function(int bytes, int totalBytes) onProgress, required void Function(CurrentUploadAsset asset) onCurrentAsset, required void Function(ErrorUploadAsset error) onError, @@ -431,9 +428,11 @@ class BackupService { } onSuccess( - asset: candidate, - deviceId: deviceId, - isDuplicate: isDuplicate, + SuccessUploadAsset( + asset: candidate, + deviceAssetId: deviceId, + isDuplicate: isDuplicate, + ), ); } } on http.CancelledException { From 2cdf0f7415d2cb7926f278f7d5cf0c032cf55d0f Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 22 Aug 2024 09:13:35 -0500 Subject: [PATCH 08/34] remove albums info from upload payload --- mobile/lib/services/backup.service.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/mobile/lib/services/backup.service.dart b/mobile/lib/services/backup.service.dart index 4f0fe07248081..fdb589fe5264b 100644 --- a/mobile/lib/services/backup.service.dart +++ b/mobile/lib/services/backup.service.dart @@ -272,7 +272,6 @@ class BackupService { for (final candidate in candidates) { final AssetEntity entity = candidate.asset; - final List albums = candidate.albums; File? file; File? livePhotoFile; @@ -354,7 +353,6 @@ class BackupService { entity.modifiedDateTime.toUtc().toIso8601String(); baseRequest.fields['isFavorite'] = entity.isFavorite.toString(); baseRequest.fields['duration'] = entity.videoDuration.toString(); - baseRequest.fields['albums'] = albums.join(','); baseRequest.files.add(assetRawUploadData); From 6943f5531004e7b28ff43fdfe99eb8a9ba834357 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 22 Aug 2024 12:03:46 -0500 Subject: [PATCH 09/34] mechanism to create album on album selection --- mobile/assets/i18n/en-US.json | 4 +- mobile/lib/entities/store.entity.dart | 2 + .../backup/backup_album_selection.page.dart | 106 +++++------------- .../lib/providers/album/album.provider.dart | 13 +++ mobile/lib/services/album.service.dart | 4 + mobile/lib/services/app_settings.service.dart | 1 + mobile/lib/services/backup.service.dart | 26 +++-- .../widgets/backup/album_info_list_tile.dart | 10 ++ .../settings/settings_switch_list_tile.dart | 30 +++-- 9 files changed, 95 insertions(+), 101 deletions(-) diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json index decb0a72e1eda..3fc12c2b8cc01 100644 --- a/mobile/assets/i18n/en-US.json +++ b/mobile/assets/i18n/en-US.json @@ -573,5 +573,7 @@ "version_announcement_overlay_text_2": "please take your time to visit the ", "version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.", "version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89", - "viewer_unstack": "Un-Stack" + "viewer_unstack": "Un-Stack", + "mirror_album_setting_title": "Mirror albums", + "mirror_album_setting_subtitle": "Create and upload your photos and videos to the selected albums on Immich" } diff --git a/mobile/lib/entities/store.entity.dart b/mobile/lib/entities/store.entity.dart index a84f9800019c3..ac1b6fdfc661c 100644 --- a/mobile/lib/entities/store.entity.dart +++ b/mobile/lib/entities/store.entity.dart @@ -234,6 +234,8 @@ enum StoreKey { primaryColor(128, type: String), dynamicTheme(129, type: bool), colorfulInterface(130, type: bool), + + mirrorUploadAlbum(131, type: bool), ; const StoreKey( diff --git a/mobile/lib/pages/backup/backup_album_selection.page.dart b/mobile/lib/pages/backup/backup_album_selection.page.dart index 9f3e387755e85..ab14b17308c9f 100644 --- a/mobile/lib/pages/backup/backup_album_selection.page.dart +++ b/mobile/lib/pages/backup/backup_album_selection.page.dart @@ -4,19 +4,24 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/providers/album/album.provider.dart'; import 'package:immich_mobile/providers/backup/backup.provider.dart'; +import 'package:immich_mobile/services/app_settings.service.dart'; +import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; import 'package:immich_mobile/widgets/backup/album_info_card.dart'; import 'package:immich_mobile/widgets/backup/album_info_list_tile.dart'; import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart'; +import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; @RoutePage() class BackupAlbumSelectionPage extends HookConsumerWidget { const BackupAlbumSelectionPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - // final availableAlbums = ref.watch(backupProvider).availableAlbums; final selectedBackupAlbums = ref.watch(backupProvider).selectedBackupAlbums; final excludedBackupAlbums = ref.watch(backupProvider).excludedBackupAlbums; + final mirroUploadAlbum = + useAppSettingsState(AppSettingsEnum.mirrorUploadAlbum); final isDarkTheme = context.isDarkTheme; final albums = ref.watch(backupProvider).availableAlbums; @@ -52,33 +57,6 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { ); } - buildAlbumSelectionGrid() { - if (albums.isEmpty) { - return const SliverToBoxAdapter( - child: Center( - child: ImmichLoadingIndicator(), - ), - ); - } - - return SliverPadding( - padding: const EdgeInsets.all(12.0), - sliver: SliverGrid.builder( - gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 300, - mainAxisSpacing: 12, - crossAxisSpacing: 12, - ), - itemCount: albums.length, - itemBuilder: ((context, index) { - return AlbumInfoCard( - album: albums[index], - ); - }), - ), - ); - } - buildSelectedAlbumNameChip() { return selectedBackupAlbums.map((album) { void removeSelection() => @@ -144,47 +122,13 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { }).toSet(); } - // buildSearchBar() { - // return Padding( - // padding: const EdgeInsets.only(left: 16.0, right: 16, bottom: 8.0), - // child: TextFormField( - // onChanged: (searchValue) { - // // if (searchValue.isEmpty) { - // // albums = availableAlbums; - // // } else { - // // albums.value = availableAlbums - // // .where( - // // (album) => album.name - // // .toLowerCase() - // // .contains(searchValue.toLowerCase()), - // // ) - // // .toList(); - // // } - // }, - // decoration: InputDecoration( - // contentPadding: const EdgeInsets.symmetric( - // horizontal: 8.0, - // vertical: 8.0, - // ), - // hintText: "Search", - // hintStyle: TextStyle( - // color: isDarkTheme ? Colors.white : Colors.grey, - // fontSize: 14.0, - // ), - // prefixIcon: const Icon( - // Icons.search, - // color: Colors.grey, - // ), - // border: OutlineInputBorder( - // borderRadius: BorderRadius.circular(10), - // borderSide: BorderSide.none, - // ), - // filled: true, - // fillColor: isDarkTheme ? Colors.white30 : Colors.grey[200], - // ), - // ), - // ); - // } + handleMirrorAlbumToggle(bool isEnable) async { + if (isEnable) { + for (final album in selectedBackupAlbums) { + await ref.read(albumProvider.notifier).createMirrorAlbum(album.name); + } + } + } return Scaffold( appBar: AppBar( @@ -226,6 +170,20 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { ), ), + SettingsSwitchListTile( + valueNotifier: mirroUploadAlbum, + title: "mirror_album_setting_title".tr(), + subtitle: "mirror_album_setting_subtitle".tr(), + contentPadding: EdgeInsets.symmetric(horizontal: 16), + titleStyle: context.textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.bold, + ), + subtitleStyle: context.textTheme.labelLarge?.copyWith( + color: context.colorScheme.primary, + ), + onChanged: handleMirrorAlbumToggle, + ), + ListTile( title: Text( "backup_album_selection_page_albums_device".tr( @@ -296,15 +254,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { ], ), ), - SliverLayoutBuilder( - builder: (context, constraints) { - if (constraints.crossAxisExtent > 600) { - return buildAlbumSelectionGrid(); - } else { - return buildAlbumSelectionList(); - } - }, - ), + buildAlbumSelectionList(), ], ), ); diff --git a/mobile/lib/providers/album/album.provider.dart b/mobile/lib/providers/album/album.provider.dart index 8251d5e66bf33..23e3b8fb1ec93 100644 --- a/mobile/lib/providers/album/album.provider.dart +++ b/mobile/lib/providers/album/album.provider.dart @@ -23,6 +23,7 @@ class AlbumNotifier extends StateNotifier> { }); _streamSub = query.watch().listen((data) => state = data); } + final AlbumService _albumService; late final StreamSubscription> _streamSub; @@ -41,6 +42,18 @@ class AlbumNotifier extends StateNotifier> { ) => _albumService.createAlbum(albumTitle, assets, []); + Future getAlbumByName(String albumName) => + _albumService.getAlbumByName(albumName); + + Future createMirrorAlbum( + String albumTitle, + ) { + print("createMirrorAlbum $albumTitle"); + // _albumService.createAlbum(albumTitle, {}, []); + + return Future.value(); + } + @override void dispose() { _streamSub.cancel(); diff --git a/mobile/lib/services/album.service.dart b/mobile/lib/services/album.service.dart index c2494680c7da5..1017e7d0d48a9 100644 --- a/mobile/lib/services/album.service.dart +++ b/mobile/lib/services/album.service.dart @@ -441,4 +441,8 @@ class AlbumService { return false; } } + + Future getAlbumByName(String albumName) async { + return await _db.albums.filter().nameEqualTo(albumName).findFirst(); + } } diff --git a/mobile/lib/services/app_settings.service.dart b/mobile/lib/services/app_settings.service.dart index bd254032159c0..5782ce4bcb40e 100644 --- a/mobile/lib/services/app_settings.service.dart +++ b/mobile/lib/services/app_settings.service.dart @@ -76,6 +76,7 @@ enum AppSettingsEnum { false, ), enableHapticFeedback(StoreKey.enableHapticFeedback, null, true), + mirrorUploadAlbum(StoreKey.mirrorUploadAlbum, null, false), ; const AppSettingsEnum(this.storeKey, this.hiveKey, this.defaultValue); diff --git a/mobile/lib/services/backup.service.dart b/mobile/lib/services/backup.service.dart index fdb589fe5264b..e9416f5356e66 100644 --- a/mobile/lib/services/backup.service.dart +++ b/mobile/lib/services/backup.service.dart @@ -258,17 +258,19 @@ class BackupService { await PhotoManager.requestPermissionExtend(); } - List candidates = sortAssets - // Upload images before video assets - // these are further sorted by using their creation date - ? assetList.sorted( - (a, b) { - final cmp = a.asset.typeInt - b.asset.typeInt; - if (cmp != 0) return cmp; - return a.asset.createDateTime.compareTo(b.asset.createDateTime); - }, - ) - : assetList.toList(); + List candidates = assetList.toList(); + + // Upload images before video assets for background tasks + // these are further sorted by using their creation date + if (sortAssets) { + candidates = assetList.sorted( + (a, b) { + final cmp = a.asset.typeInt - b.asset.typeInt; + if (cmp != 0) return cmp; + return a.asset.createDateTime.compareTo(b.asset.createDateTime); + }, + ); + } for (final candidate in candidates) { final AssetEntity entity = candidate.asset; @@ -452,9 +454,11 @@ class BackupService { } } } + if (duplicatedAssetIds.isNotEmpty) { await _saveDuplicatedAssetIds(duplicatedAssetIds); } + return !anyErrors; } diff --git a/mobile/lib/widgets/backup/album_info_list_tile.dart b/mobile/lib/widgets/backup/album_info_list_tile.dart index 7cdc595c7fc53..664e5b6c9d730 100644 --- a/mobile/lib/widgets/backup/album_info_list_tile.dart +++ b/mobile/lib/widgets/backup/album_info_list_tile.dart @@ -5,9 +5,13 @@ import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/models/backup/available_album.model.dart'; +import 'package:immich_mobile/providers/album/album.provider.dart'; +import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; +import 'package:immich_mobile/services/app_settings.service.dart'; +import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; class AlbumInfoListTile extends HookConsumerWidget { @@ -22,6 +26,9 @@ class AlbumInfoListTile extends HookConsumerWidget { final bool isExcluded = ref.watch(backupProvider).excludedBackupAlbums.contains(album); var assetCount = useState(0); + final mirroUploadAlbum = ref + .watch(appSettingsServiceProvider) + .getSetting(AppSettingsEnum.mirrorUploadAlbum); useEffect( () { @@ -98,6 +105,9 @@ class AlbumInfoListTile extends HookConsumerWidget { ref.read(backupProvider.notifier).removeAlbumForBackup(album); } else { ref.read(backupProvider.notifier).addAlbumForBackup(album); + if (mirroUploadAlbum) { + ref.read(albumProvider.notifier).createMirrorAlbum(album.name); + } } }, leading: buildIcon(), diff --git a/mobile/lib/widgets/settings/settings_switch_list_tile.dart b/mobile/lib/widgets/settings/settings_switch_list_tile.dart index 78f1738266a31..8aa4ec0a60ec0 100644 --- a/mobile/lib/widgets/settings/settings_switch_list_tile.dart +++ b/mobile/lib/widgets/settings/settings_switch_list_tile.dart @@ -9,6 +9,9 @@ class SettingsSwitchListTile extends StatelessWidget { final String? subtitle; final IconData? icon; final Function(bool)? onChanged; + final EdgeInsets? contentPadding; + final TextStyle? titleStyle; + final TextStyle? subtitleStyle; const SettingsSwitchListTile({ required this.valueNotifier, @@ -17,6 +20,9 @@ class SettingsSwitchListTile extends StatelessWidget { this.icon, this.enabled = true, this.onChanged, + this.contentPadding = const EdgeInsets.symmetric(horizontal: 20), + this.titleStyle, + this.subtitleStyle, super.key, }); @@ -30,7 +36,7 @@ class SettingsSwitchListTile extends StatelessWidget { } return SwitchListTile.adaptive( - contentPadding: const EdgeInsets.symmetric(horizontal: 20), + contentPadding: contentPadding, selectedTileColor: enabled ? null : context.themeData.disabledColor, value: valueNotifier.value, onChanged: onSwitchChanged, @@ -45,20 +51,22 @@ class SettingsSwitchListTile extends StatelessWidget { : null, title: Text( title, - style: context.textTheme.bodyLarge?.copyWith( - fontWeight: FontWeight.w500, - color: enabled ? null : context.themeData.disabledColor, - height: 1.5, - ), + style: titleStyle ?? + context.textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.w500, + color: enabled ? null : context.themeData.disabledColor, + height: 1.5, + ), ), subtitle: subtitle != null ? Text( subtitle!, - style: context.textTheme.bodyMedium?.copyWith( - color: enabled - ? context.colorScheme.onSurfaceSecondary - : context.themeData.disabledColor, - ), + style: subtitleStyle ?? + context.textTheme.bodyMedium?.copyWith( + color: enabled + ? context.colorScheme.onSurfaceSecondary + : context.themeData.disabledColor, + ), ) : null, ); From c21929b38822e836131b99d7663efc3e5032ec4b Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 22 Aug 2024 12:34:07 -0500 Subject: [PATCH 10/34] album creation --- .../lib/providers/album/album.provider.dart | 23 +++++++++++++------ mobile/lib/services/album.service.dart | 11 +++++++-- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/mobile/lib/providers/album/album.provider.dart b/mobile/lib/providers/album/album.provider.dart index 23e3b8fb1ec93..5e2d0e073a1d4 100644 --- a/mobile/lib/providers/album/album.provider.dart +++ b/mobile/lib/providers/album/album.provider.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:flutter/foundation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/services/album.service.dart'; import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; @@ -42,16 +43,24 @@ class AlbumNotifier extends StateNotifier> { ) => _albumService.createAlbum(albumTitle, assets, []); - Future getAlbumByName(String albumName) => - _albumService.getAlbumByName(albumName); + Future getAlbumByName(String albumName, {bool remoteOnly = false}) => + _albumService.getAlbumByName(albumName, remoteOnly); + /// Creat an album on the server with the same name as the selected album for backup + /// First this will check if the album already exists on the server with name + /// If it does not exist, it will create the album on the server Future createMirrorAlbum( - String albumTitle, - ) { - print("createMirrorAlbum $albumTitle"); - // _albumService.createAlbum(albumTitle, {}, []); + String albumName, + ) async { + final album = await getAlbumByName(albumName, remoteOnly: true); + print("Album: $album ${album?.localId} ${album?.remoteId}"); + if (album != null) { + return; + } + + await createAlbum(albumName, {}); - return Future.value(); + debugPrint("Create album $albumName on server"); } @override diff --git a/mobile/lib/services/album.service.dart b/mobile/lib/services/album.service.dart index 1017e7d0d48a9..719229e29acaa 100644 --- a/mobile/lib/services/album.service.dart +++ b/mobile/lib/services/album.service.dart @@ -442,7 +442,14 @@ class AlbumService { } } - Future getAlbumByName(String albumName) async { - return await _db.albums.filter().nameEqualTo(albumName).findFirst(); + Future getAlbumByName(String name, bool remoteOnly) async { + final query = _db.albums.filter().nameEqualTo(name).sharedEqualTo(false); + + if (remoteOnly) { + print("find album by name remoteOnly"); + return query.localIdIsNull().findFirst(); + } + + return query.findFirst(); } } From 1e29873b5fd7d9d0050b442c18366dc9da13c1e1 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 22 Aug 2024 16:24:48 -0500 Subject: [PATCH 11/34] Sync to upload album --- mobile/assets/i18n/en-US.json | 2 +- mobile/lib/entities/store.entity.dart | 2 +- .../backup/success_upload_asset.model.dart | 24 +++++++-------- .../backup/backup_album_selection.page.dart | 1 - .../lib/providers/backup/backup.provider.dart | 30 ++++++++++++++++--- mobile/lib/services/album.service.dart | 17 ++++++++++- mobile/lib/services/app_settings.service.dart | 2 +- mobile/lib/services/backup.service.dart | 4 +-- .../widgets/backup/album_info_list_tile.dart | 1 - 9 files changed, 59 insertions(+), 24 deletions(-) diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json index 3fc12c2b8cc01..87b648fdac06d 100644 --- a/mobile/assets/i18n/en-US.json +++ b/mobile/assets/i18n/en-US.json @@ -574,6 +574,6 @@ "version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.", "version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89", "viewer_unstack": "Un-Stack", - "mirror_album_setting_title": "Mirror albums", + "mirror_album_setting_title": "Sync albums", "mirror_album_setting_subtitle": "Create and upload your photos and videos to the selected albums on Immich" } diff --git a/mobile/lib/entities/store.entity.dart b/mobile/lib/entities/store.entity.dart index ac1b6fdfc661c..cecac813f8855 100644 --- a/mobile/lib/entities/store.entity.dart +++ b/mobile/lib/entities/store.entity.dart @@ -235,7 +235,7 @@ enum StoreKey { dynamicTheme(129, type: bool), colorfulInterface(130, type: bool), - mirrorUploadAlbum(131, type: bool), + shouldSyncUploadAlbum(131, type: bool), ; const StoreKey( diff --git a/mobile/lib/models/backup/success_upload_asset.model.dart b/mobile/lib/models/backup/success_upload_asset.model.dart index 69ecbe73b8c34..045715e8cbbda 100644 --- a/mobile/lib/models/backup/success_upload_asset.model.dart +++ b/mobile/lib/models/backup/success_upload_asset.model.dart @@ -1,42 +1,42 @@ import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; class SuccessUploadAsset { - final BackupCandidate asset; - final String deviceAssetId; + final BackupCandidate candidate; + final String remoteAssetId; final bool isDuplicate; SuccessUploadAsset({ - required this.asset, - required this.deviceAssetId, + required this.candidate, + required this.remoteAssetId, required this.isDuplicate, }); SuccessUploadAsset copyWith({ - BackupCandidate? asset, - String? deviceAssetId, + BackupCandidate? candidate, + String? remoteAssetId, bool? isDuplicate, }) { return SuccessUploadAsset( - asset: asset ?? this.asset, - deviceAssetId: deviceAssetId ?? this.deviceAssetId, + candidate: candidate ?? this.candidate, + remoteAssetId: remoteAssetId ?? this.remoteAssetId, isDuplicate: isDuplicate ?? this.isDuplicate, ); } @override String toString() => - 'SuccessUploadAsset(asset: $asset, deviceId: $deviceAssetId, isDuplicate: $isDuplicate)'; + 'SuccessUploadAsset(asset: $candidate, remoteAssetId: $remoteAssetId, isDuplicate: $isDuplicate)'; @override bool operator ==(covariant SuccessUploadAsset other) { if (identical(this, other)) return true; - return other.asset == asset && - other.deviceAssetId == deviceAssetId && + return other.candidate == candidate && + other.remoteAssetId == remoteAssetId && other.isDuplicate == isDuplicate; } @override int get hashCode => - asset.hashCode ^ deviceAssetId.hashCode ^ isDuplicate.hashCode; + candidate.hashCode ^ remoteAssetId.hashCode ^ isDuplicate.hashCode; } diff --git a/mobile/lib/pages/backup/backup_album_selection.page.dart b/mobile/lib/pages/backup/backup_album_selection.page.dart index ab14b17308c9f..cddf3213e7776 100644 --- a/mobile/lib/pages/backup/backup_album_selection.page.dart +++ b/mobile/lib/pages/backup/backup_album_selection.page.dart @@ -8,7 +8,6 @@ import 'package:immich_mobile/providers/album/album.provider.dart'; import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; -import 'package:immich_mobile/widgets/backup/album_info_card.dart'; import 'package:immich_mobile/widgets/backup/album_info_list_tile.dart'; import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart'; import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; diff --git a/mobile/lib/providers/backup/backup.provider.dart b/mobile/lib/providers/backup/backup.provider.dart index 4f51a78395fe1..cee3fdfab78c5 100644 --- a/mobile/lib/providers/backup/backup.provider.dart +++ b/mobile/lib/providers/backup/backup.provider.dart @@ -11,7 +11,10 @@ import 'package:immich_mobile/models/backup/backup_state.model.dart'; import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; import 'package:immich_mobile/models/backup/error_upload_asset.model.dart'; import 'package:immich_mobile/models/backup/success_upload_asset.model.dart'; +import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart'; +import 'package:immich_mobile/services/album.service.dart'; +import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/background.service.dart'; import 'package:immich_mobile/services/backup.service.dart'; import 'package:immich_mobile/models/authentication/authentication_state.model.dart'; @@ -26,6 +29,7 @@ import 'package:immich_mobile/utils/backup_progress.dart'; import 'package:immich_mobile/utils/diff.dart'; import 'package:isar/isar.dart'; import 'package:logging/logging.dart'; +import 'package:openapi/api.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:photo_manager/photo_manager.dart'; @@ -36,6 +40,8 @@ class BackupNotifier extends StateNotifier { this._authState, this._backgroundService, this._galleryPermissionNotifier, + this._albumService, + this._apiService, this._db, this.ref, ) : super( @@ -84,6 +90,8 @@ class BackupNotifier extends StateNotifier { final AuthenticationState _authState; final BackgroundService _backgroundService; final GalleryPermissionNotifier _galleryPermissionNotifier; + final AlbumService _albumService; + final ApiService _apiService; final Isar _db; final Ref ref; @@ -525,22 +533,34 @@ class BackupNotifier extends StateNotifier { ); } - void _onAssetUploaded(SuccessUploadAsset result) { + void _onAssetUploaded(SuccessUploadAsset result) async { if (result.isDuplicate) { state = state.copyWith( allUniqueAssets: state.allUniqueAssets - .where((candidate) => candidate.asset.id != result.deviceAssetId) + .where( + (candidate) => candidate.asset.id != result.candidate.asset.id, + ) .toSet(), ); } else { + final shouldSyncUploadAlbum = + Store.get(StoreKey.shouldSyncUploadAlbum, false); + + if (shouldSyncUploadAlbum) { + await _albumService.syncUploadAlbums( + result.candidate.albums, + result.remoteAssetId, + ); + } + state = state.copyWith( selectedAlbumsBackupAssetsIds: { ...state.selectedAlbumsBackupAssetsIds, - result.deviceAssetId, + result.candidate.asset.id, }, allAssetsInDatabase: [ ...state.allAssetsInDatabase, - result.deviceAssetId, + result.candidate.asset.id, ], ); } @@ -737,6 +757,8 @@ final backupProvider = ref.watch(authenticationProvider), ref.watch(backgroundServiceProvider), ref.watch(galleryPermissionNotifier.notifier), + ref.watch(albumServiceProvider), + ref.watch(apiServiceProvider), ref.watch(dbProvider), ref, ); diff --git a/mobile/lib/services/album.service.dart b/mobile/lib/services/album.service.dart index 719229e29acaa..68ce4914c6101 100644 --- a/mobile/lib/services/album.service.dart +++ b/mobile/lib/services/album.service.dart @@ -446,10 +446,25 @@ class AlbumService { final query = _db.albums.filter().nameEqualTo(name).sharedEqualTo(false); if (remoteOnly) { - print("find album by name remoteOnly"); return query.localIdIsNull().findFirst(); } return query.findFirst(); } + + /// + /// Add the uploaded asset to the selected albums + /// + Future syncUploadAlbums(List albumNames, String assetId) async { + for (final albumName in albumNames) { + final album = await getAlbumByName(albumName, true); + + if (album != null && album.remoteId != null) { + await _apiService.albumsApi.addAssetsToAlbum( + album.remoteId!, + BulkIdsDto(ids: [assetId]), + ); + } + } + } } diff --git a/mobile/lib/services/app_settings.service.dart b/mobile/lib/services/app_settings.service.dart index 5782ce4bcb40e..220a095408c84 100644 --- a/mobile/lib/services/app_settings.service.dart +++ b/mobile/lib/services/app_settings.service.dart @@ -76,7 +76,7 @@ enum AppSettingsEnum { false, ), enableHapticFeedback(StoreKey.enableHapticFeedback, null, true), - mirrorUploadAlbum(StoreKey.mirrorUploadAlbum, null, false), + mirrorUploadAlbum(StoreKey.shouldSyncUploadAlbum, null, false), ; const AppSettingsEnum(this.storeKey, this.hiveKey, this.defaultValue); diff --git a/mobile/lib/services/backup.service.dart b/mobile/lib/services/backup.service.dart index e9416f5356e66..86c13b0f9eb50 100644 --- a/mobile/lib/services/backup.service.dart +++ b/mobile/lib/services/backup.service.dart @@ -429,8 +429,8 @@ class BackupService { onSuccess( SuccessUploadAsset( - asset: candidate, - deviceAssetId: deviceId, + candidate: candidate, + remoteAssetId: responseBody['id'] as String, isDuplicate: isDuplicate, ), ); diff --git a/mobile/lib/widgets/backup/album_info_list_tile.dart b/mobile/lib/widgets/backup/album_info_list_tile.dart index 664e5b6c9d730..2a03c60ed95b9 100644 --- a/mobile/lib/widgets/backup/album_info_list_tile.dart +++ b/mobile/lib/widgets/backup/album_info_list_tile.dart @@ -11,7 +11,6 @@ import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; class AlbumInfoListTile extends HookConsumerWidget { From 180ce1af064ff6df56e734a35b967ef7d04aa4d6 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 22 Aug 2024 16:25:20 -0500 Subject: [PATCH 12/34] Remove unused service --- mobile/lib/providers/backup/backup.provider.dart | 6 ------ 1 file changed, 6 deletions(-) diff --git a/mobile/lib/providers/backup/backup.provider.dart b/mobile/lib/providers/backup/backup.provider.dart index cee3fdfab78c5..e984a80a1b9fa 100644 --- a/mobile/lib/providers/backup/backup.provider.dart +++ b/mobile/lib/providers/backup/backup.provider.dart @@ -11,10 +11,8 @@ import 'package:immich_mobile/models/backup/backup_state.model.dart'; import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; import 'package:immich_mobile/models/backup/error_upload_asset.model.dart'; import 'package:immich_mobile/models/backup/success_upload_asset.model.dart'; -import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart'; import 'package:immich_mobile/services/album.service.dart'; -import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/background.service.dart'; import 'package:immich_mobile/services/backup.service.dart'; import 'package:immich_mobile/models/authentication/authentication_state.model.dart'; @@ -29,7 +27,6 @@ import 'package:immich_mobile/utils/backup_progress.dart'; import 'package:immich_mobile/utils/diff.dart'; import 'package:isar/isar.dart'; import 'package:logging/logging.dart'; -import 'package:openapi/api.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:photo_manager/photo_manager.dart'; @@ -41,7 +38,6 @@ class BackupNotifier extends StateNotifier { this._backgroundService, this._galleryPermissionNotifier, this._albumService, - this._apiService, this._db, this.ref, ) : super( @@ -91,7 +87,6 @@ class BackupNotifier extends StateNotifier { final BackgroundService _backgroundService; final GalleryPermissionNotifier _galleryPermissionNotifier; final AlbumService _albumService; - final ApiService _apiService; final Isar _db; final Ref ref; @@ -758,7 +753,6 @@ final backupProvider = ref.watch(backgroundServiceProvider), ref.watch(galleryPermissionNotifier.notifier), ref.watch(albumServiceProvider), - ref.watch(apiServiceProvider), ref.watch(dbProvider), ref, ); From 125488d149fbeb620e617e82b38a72d0b99e4f69 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 23 Aug 2024 08:43:36 -0500 Subject: [PATCH 13/34] unify name changes --- mobile/assets/i18n/en-US.json | 4 ++-- mobile/lib/entities/store.entity.dart | 2 +- .../lib/pages/backup/backup_album_selection.page.dart | 10 +++++----- mobile/lib/providers/backup/backup.provider.dart | 2 +- mobile/lib/services/app_settings.service.dart | 2 +- mobile/lib/widgets/backup/album_info_list_tile.dart | 6 +++--- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json index 87b648fdac06d..2113a27f120e1 100644 --- a/mobile/assets/i18n/en-US.json +++ b/mobile/assets/i18n/en-US.json @@ -574,6 +574,6 @@ "version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.", "version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89", "viewer_unstack": "Un-Stack", - "mirror_album_setting_title": "Sync albums", - "mirror_album_setting_subtitle": "Create and upload your photos and videos to the selected albums on Immich" + "sync_upload_album_setting_title": "Sync albums", + "sync_upload_album_setting_subtitle": "Create and upload your photos and videos to the selected albums on Immich" } diff --git a/mobile/lib/entities/store.entity.dart b/mobile/lib/entities/store.entity.dart index cecac813f8855..d39ca438559d8 100644 --- a/mobile/lib/entities/store.entity.dart +++ b/mobile/lib/entities/store.entity.dart @@ -235,7 +235,7 @@ enum StoreKey { dynamicTheme(129, type: bool), colorfulInterface(130, type: bool), - shouldSyncUploadAlbum(131, type: bool), + enableSyncUploadAlbum(131, type: bool), ; const StoreKey( diff --git a/mobile/lib/pages/backup/backup_album_selection.page.dart b/mobile/lib/pages/backup/backup_album_selection.page.dart index cddf3213e7776..658039e5ea94f 100644 --- a/mobile/lib/pages/backup/backup_album_selection.page.dart +++ b/mobile/lib/pages/backup/backup_album_selection.page.dart @@ -19,8 +19,8 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final selectedBackupAlbums = ref.watch(backupProvider).selectedBackupAlbums; final excludedBackupAlbums = ref.watch(backupProvider).excludedBackupAlbums; - final mirroUploadAlbum = - useAppSettingsState(AppSettingsEnum.mirrorUploadAlbum); + final enableSyncUploadAlbum = + useAppSettingsState(AppSettingsEnum.enableSyncUploadAlbum); final isDarkTheme = context.isDarkTheme; final albums = ref.watch(backupProvider).availableAlbums; @@ -170,9 +170,9 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { ), SettingsSwitchListTile( - valueNotifier: mirroUploadAlbum, - title: "mirror_album_setting_title".tr(), - subtitle: "mirror_album_setting_subtitle".tr(), + valueNotifier: enableSyncUploadAlbum, + title: "sync_upload_album_setting_title".tr(), + subtitle: "sync_upload_album_setting_subtitle".tr(), contentPadding: EdgeInsets.symmetric(horizontal: 16), titleStyle: context.textTheme.bodyLarge?.copyWith( fontWeight: FontWeight.bold, diff --git a/mobile/lib/providers/backup/backup.provider.dart b/mobile/lib/providers/backup/backup.provider.dart index e984a80a1b9fa..258674dc3096b 100644 --- a/mobile/lib/providers/backup/backup.provider.dart +++ b/mobile/lib/providers/backup/backup.provider.dart @@ -539,7 +539,7 @@ class BackupNotifier extends StateNotifier { ); } else { final shouldSyncUploadAlbum = - Store.get(StoreKey.shouldSyncUploadAlbum, false); + Store.get(StoreKey.enableSyncUploadAlbum, false); if (shouldSyncUploadAlbum) { await _albumService.syncUploadAlbums( diff --git a/mobile/lib/services/app_settings.service.dart b/mobile/lib/services/app_settings.service.dart index 220a095408c84..c81e174fd7ad0 100644 --- a/mobile/lib/services/app_settings.service.dart +++ b/mobile/lib/services/app_settings.service.dart @@ -76,7 +76,7 @@ enum AppSettingsEnum { false, ), enableHapticFeedback(StoreKey.enableHapticFeedback, null, true), - mirrorUploadAlbum(StoreKey.shouldSyncUploadAlbum, null, false), + enableSyncUploadAlbum(StoreKey.enableSyncUploadAlbum, null, false), ; const AppSettingsEnum(this.storeKey, this.hiveKey, this.defaultValue); diff --git a/mobile/lib/widgets/backup/album_info_list_tile.dart b/mobile/lib/widgets/backup/album_info_list_tile.dart index 2a03c60ed95b9..53cd92d290988 100644 --- a/mobile/lib/widgets/backup/album_info_list_tile.dart +++ b/mobile/lib/widgets/backup/album_info_list_tile.dart @@ -25,9 +25,9 @@ class AlbumInfoListTile extends HookConsumerWidget { final bool isExcluded = ref.watch(backupProvider).excludedBackupAlbums.contains(album); var assetCount = useState(0); - final mirroUploadAlbum = ref + final shouldSyncUploadAlbum = ref .watch(appSettingsServiceProvider) - .getSetting(AppSettingsEnum.mirrorUploadAlbum); + .getSetting(AppSettingsEnum.enableSyncUploadAlbum); useEffect( () { @@ -104,7 +104,7 @@ class AlbumInfoListTile extends HookConsumerWidget { ref.read(backupProvider.notifier).removeAlbumForBackup(album); } else { ref.read(backupProvider.notifier).addAlbumForBackup(album); - if (mirroUploadAlbum) { + if (shouldSyncUploadAlbum) { ref.read(albumProvider.notifier).createMirrorAlbum(album.name); } } From 7272107061341909601d37c1b6bcdcc2a32f4f20 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 23 Aug 2024 11:45:21 -0500 Subject: [PATCH 14/34] Add mechanism to sync uploaded assets to albums --- .../pages/backup/backup_controller.page.dart | 7 ++ .../lib/providers/backup/backup.provider.dart | 2 +- mobile/lib/services/album.service.dart | 7 +- mobile/lib/services/asset.service.dart | 68 +++++++++++++++++++ mobile/lib/services/backup.service.dart | 55 +++++++++++---- 5 files changed, 122 insertions(+), 17 deletions(-) diff --git a/mobile/lib/pages/backup/backup_controller.page.dart b/mobile/lib/pages/backup/backup_controller.page.dart index bb9d462e50bc4..894f913d7b6e5 100644 --- a/mobile/lib/pages/backup/backup_controller.page.dart +++ b/mobile/lib/pages/backup/backup_controller.page.dart @@ -18,6 +18,7 @@ import 'package:immich_mobile/providers/backup/ios_background_settings.provider. import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; import 'package:immich_mobile/providers/websocket.provider.dart'; import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/services/asset.service.dart'; import 'package:immich_mobile/widgets/backup/backup_info_card.dart'; import 'package:immich_mobile/widgets/backup/current_backup_asset_info_box.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; @@ -348,6 +349,12 @@ class BackupControllerPage extends HookConsumerWidget { // crossAxisAlignment: CrossAxisAlignment.start, children: hasAnyAlbum ? [ + ElevatedButton( + child: Text("test"), + onPressed: () => ref + .read(assetServiceProvider) + .syncUploadedAssetToAlbums(), + ), buildFolderSelectionTile(), BackupInfoCard( title: "backup_controller_page_total".tr(), diff --git a/mobile/lib/providers/backup/backup.provider.dart b/mobile/lib/providers/backup/backup.provider.dart index 258674dc3096b..a04b3561fb7c6 100644 --- a/mobile/lib/providers/backup/backup.provider.dart +++ b/mobile/lib/providers/backup/backup.provider.dart @@ -544,7 +544,7 @@ class BackupNotifier extends StateNotifier { if (shouldSyncUploadAlbum) { await _albumService.syncUploadAlbums( result.candidate.albums, - result.remoteAssetId, + [result.remoteAssetId], ); } diff --git a/mobile/lib/services/album.service.dart b/mobile/lib/services/album.service.dart index 68ce4914c6101..a26004ac50c5c 100644 --- a/mobile/lib/services/album.service.dart +++ b/mobile/lib/services/album.service.dart @@ -455,14 +455,17 @@ class AlbumService { /// /// Add the uploaded asset to the selected albums /// - Future syncUploadAlbums(List albumNames, String assetId) async { + Future syncUploadAlbums( + List albumNames, + List assetIds, + ) async { for (final albumName in albumNames) { final album = await getAlbumByName(albumName, true); if (album != null && album.remoteId != null) { await _apiService.albumsApi.addAssetsToAlbum( album.remoteId!, - BulkIdsDto(ids: [assetId]), + BulkIdsDto(ids: assetIds), ); } } diff --git a/mobile/lib/services/asset.service.dart b/mobile/lib/services/asset.service.dart index d37133a63b9c7..771b6f046cacb 100644 --- a/mobile/lib/services/asset.service.dart +++ b/mobile/lib/services/asset.service.dart @@ -2,15 +2,20 @@ import 'dart:async'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/etag.entity.dart'; import 'package:immich_mobile/entities/exif_info.entity.dart'; +import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/user.entity.dart'; +import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/db.provider.dart'; +import 'package:immich_mobile/services/album.service.dart'; import 'package:immich_mobile/services/api.service.dart'; +import 'package:immich_mobile/services/backup.service.dart'; import 'package:immich_mobile/services/sync.service.dart'; import 'package:immich_mobile/services/user.service.dart'; import 'package:isar/isar.dart'; @@ -23,6 +28,8 @@ final assetServiceProvider = Provider( ref.watch(apiServiceProvider), ref.watch(syncServiceProvider), ref.watch(userServiceProvider), + ref.watch(backupServiceProvider), + ref.watch(albumServiceProvider), ref.watch(dbProvider), ), ); @@ -31,6 +38,8 @@ class AssetService { final ApiService _apiService; final SyncService _syncService; final UserService _userService; + final BackupService _backupService; + final AlbumService _albumService; final log = Logger('AssetService'); final Isar _db; @@ -38,6 +47,8 @@ class AssetService { this._apiService, this._syncService, this._userService, + this._backupService, + this._albumService, this._db, ); @@ -284,4 +295,61 @@ class AssetService { return Future.value(null); } } + + Future syncUploadedAssetToAlbums() async { + try { + final selectedAlbums = _backupService.selectedAlbumsQuery().findAllSync(); + final excludedAlbums = _backupService.excludedAlbumsQuery().findAllSync(); + + final candidates = await _backupService.buildUploadCandidates( + selectedAlbums, + excludedAlbums, + ignoreTimeFilter: true, + ); + + final duplicates = await _apiService.assetsApi.checkExistingAssets( + CheckExistingAssetsDto( + deviceAssetIds: candidates.map((c) => c.asset.id).toList(), + deviceId: Store.get(StoreKey.deviceId), + ), + ); + + if (duplicates != null) { + candidates + .removeWhere((c) => !duplicates.existingIds.contains(c.asset.id)); + } + + await refreshRemoteAssets(); + final remoteAssets = await _db.assets + .filter() + .localIdIsNotNull() + .remoteIdIsNotNull() + .findAll(); + + /// Map + Map> assetToAlbums = {}; + + for (BackupCandidate candidate in candidates) { + final asset = remoteAssets.firstWhereOrNull( + (a) => a.localId == candidate.asset.id, + ); + + if (asset != null) { + for (final albumName in candidate.albums) { + assetToAlbums.putIfAbsent(albumName, () => []).add(asset.remoteId!); + } + } + } + + for (final entry in assetToAlbums.entries) { + final albumName = entry.key; + final assetIds = entry.value; + + await _albumService.syncUploadAlbums([albumName], assetIds); + } + } catch (error, stack) { + print(error); + log.severe("Error while syncing uploaded asset to albums", error, stack); + } + } } diff --git a/mobile/lib/services/backup.service.dart b/mobile/lib/services/backup.service.dart index 86c13b0f9eb50..f41363c62c303 100644 --- a/mobile/lib/services/backup.service.dart +++ b/mobile/lib/services/backup.service.dart @@ -40,7 +40,11 @@ class BackupService { final Logger _log = Logger("BackupService"); final AppSettingsService _appSetting; - BackupService(this._apiService, this._db, this._appSetting); + BackupService( + this._apiService, + this._db, + this._appSetting, + ); Future?> getDeviceBackupAsset() async { final String deviceId = Store.get(StoreKey.deviceId); @@ -72,10 +76,12 @@ class BackupService { _db.backupAlbums.filter().selectionEqualTo(BackupSelection.exclude); /// Returns all assets newer than the last successful backup per album + /// if `ignoreTimeFilter` is set to true, all assets will be returned Future> buildUploadCandidates( List selectedBackupAlbums, - List excludedBackupAlbums, - ) async { + List excludedBackupAlbums, { + bool ignoreTimeFilter = false, + }) async { final filter = FilterOptionGroup( containsPathModified: true, orders: [const OrderOption(type: OrderOptionType.updateDate)], @@ -85,14 +91,26 @@ class BackupService { ); final now = DateTime.now(); + final List selectedAlbums = - await _loadAlbumsWithTimeFilter(selectedBackupAlbums, filter, now); + await _loadAlbumsWithTimeFilter( + selectedBackupAlbums, + filter, + now, + ignoreTimeFilter: ignoreTimeFilter, + ); + if (selectedAlbums.every((e) => e == null)) { return {}; } final List excludedAlbums = - await _loadAlbumsWithTimeFilter(excludedBackupAlbums, filter, now); + await _loadAlbumsWithTimeFilter( + excludedBackupAlbums, + filter, + now, + ignoreTimeFilter: ignoreTimeFilter, + ); final Set toAdd = await _fetchAssetsAndUpdateLastBackup( selectedAlbums, @@ -100,32 +118,41 @@ class BackupService { now, ); + print("toAdd: ${toAdd.length}"); + final Set toRemove = await _fetchAssetsAndUpdateLastBackup( excludedAlbums, excludedBackupAlbums, now, ); + print("toRemove: ${toRemove.length}"); + return toAdd.difference(toRemove); } Future> _loadAlbumsWithTimeFilter( List albums, FilterOptionGroup filter, - DateTime now, - ) async { + DateTime now, { + bool ignoreTimeFilter = false, + }) async { List result = []; - for (BackupAlbum a in albums) { + + for (BackupAlbum backupAlbum in albums) { try { final AssetPathEntity album = await AssetPathEntity.obtainPathFromProperties( - id: a.id, + id: backupAlbum.id, optionGroup: filter.copyWith( - updateTimeCond: DateTimeCond( - // subtract 2 seconds to prevent missing assets due to rounding issues - min: a.lastBackup.subtract(const Duration(seconds: 2)), - max: now, - ), + updateTimeCond: ignoreTimeFilter + ? null + : DateTimeCond( + // subtract 2 seconds to prevent missing assets due to rounding issues + min: backupAlbum.lastBackup + .subtract(const Duration(seconds: 2)), + max: now, + ), ), maxDateTimeToNow: false, ); From 92b4958d4f37cdc04dfc5de17d94f1221cb932d0 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 23 Aug 2024 12:27:40 -0500 Subject: [PATCH 15/34] Put add to album operation after updating the UI state --- .../backup/backup_album_selection.page.dart | 5 ++-- .../lib/providers/backup/backup.provider.dart | 24 ++++++++++--------- mobile/lib/services/asset.service.dart | 1 + 3 files changed, 17 insertions(+), 13 deletions(-) diff --git a/mobile/lib/pages/backup/backup_album_selection.page.dart b/mobile/lib/pages/backup/backup_album_selection.page.dart index 658039e5ea94f..906540bf5a387 100644 --- a/mobile/lib/pages/backup/backup_album_selection.page.dart +++ b/mobile/lib/pages/backup/backup_album_selection.page.dart @@ -121,8 +121,9 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { }).toSet(); } - handleMirrorAlbumToggle(bool isEnable) async { + handleSyncAlbumToggle(bool isEnable) async { if (isEnable) { + await ref.read(albumProvider.notifier).getAllAlbums(); for (final album in selectedBackupAlbums) { await ref.read(albumProvider.notifier).createMirrorAlbum(album.name); } @@ -180,7 +181,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { subtitleStyle: context.textTheme.labelLarge?.copyWith( color: context.colorScheme.primary, ), - onChanged: handleMirrorAlbumToggle, + onChanged: handleSyncAlbumToggle, ), ListTile( diff --git a/mobile/lib/providers/backup/backup.provider.dart b/mobile/lib/providers/backup/backup.provider.dart index a04b3561fb7c6..d314202fb2b24 100644 --- a/mobile/lib/providers/backup/backup.provider.dart +++ b/mobile/lib/providers/backup/backup.provider.dart @@ -1,7 +1,9 @@ import 'dart:io'; +import 'dart:isolate'; import 'package:cancellation_token_http/http.dart'; import 'package:collection/collection.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/models/backup/available_album.model.dart'; @@ -449,7 +451,7 @@ class BackupNotifier extends StateNotifier { /// Invoke backup process Future startBackupProcess() async { debugPrint("Start backup process"); - assert(state.backupProgress == BackUpProgressEnum.idle); + // assert(state.backupProgress == BackUpProgressEnum.idle); state = state.copyWith(backupProgress: BackUpProgressEnum.inProgress); await getBackupInfo(); @@ -538,16 +540,6 @@ class BackupNotifier extends StateNotifier { .toSet(), ); } else { - final shouldSyncUploadAlbum = - Store.get(StoreKey.enableSyncUploadAlbum, false); - - if (shouldSyncUploadAlbum) { - await _albumService.syncUploadAlbums( - result.candidate.albums, - [result.remoteAssetId], - ); - } - state = state.copyWith( selectedAlbumsBackupAssetsIds: { ...state.selectedAlbumsBackupAssetsIds, @@ -585,6 +577,16 @@ class BackupNotifier extends StateNotifier { _updatePersistentAlbumsSelection(); } + final shouldSyncUploadAlbum = + Store.get(StoreKey.enableSyncUploadAlbum, false); + + if (shouldSyncUploadAlbum && !result.isDuplicate) { + await _albumService.syncUploadAlbums( + result.candidate.albums, + [result.remoteAssetId], + ); + } + updateDiskInfo(); } diff --git a/mobile/lib/services/asset.service.dart b/mobile/lib/services/asset.service.dart index 771b6f046cacb..42c810349fce3 100644 --- a/mobile/lib/services/asset.service.dart +++ b/mobile/lib/services/asset.service.dart @@ -341,6 +341,7 @@ class AssetService { } } + // Upload assets to albums for (final entry in assetToAlbums.entries) { final albumName = entry.key; final assetIds = entry.value; From 54cdc6639f4ba948919be8c58547ad4e026dd3a1 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 23 Aug 2024 12:36:50 -0500 Subject: [PATCH 16/34] clean up --- mobile/lib/pages/backup/backup_album_selection.page.dart | 2 +- mobile/lib/providers/album/album.provider.dart | 5 +---- mobile/lib/providers/backup/backup.provider.dart | 2 +- mobile/lib/services/backup.service.dart | 4 ---- mobile/lib/widgets/backup/album_info_list_tile.dart | 2 +- 5 files changed, 4 insertions(+), 11 deletions(-) diff --git a/mobile/lib/pages/backup/backup_album_selection.page.dart b/mobile/lib/pages/backup/backup_album_selection.page.dart index 906540bf5a387..3d90f85b9f72b 100644 --- a/mobile/lib/pages/backup/backup_album_selection.page.dart +++ b/mobile/lib/pages/backup/backup_album_selection.page.dart @@ -125,7 +125,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { if (isEnable) { await ref.read(albumProvider.notifier).getAllAlbums(); for (final album in selectedBackupAlbums) { - await ref.read(albumProvider.notifier).createMirrorAlbum(album.name); + await ref.read(albumProvider.notifier).createSyncAlbum(album.name); } } } diff --git a/mobile/lib/providers/album/album.provider.dart b/mobile/lib/providers/album/album.provider.dart index 5e2d0e073a1d4..75a941c5fd0ec 100644 --- a/mobile/lib/providers/album/album.provider.dart +++ b/mobile/lib/providers/album/album.provider.dart @@ -49,18 +49,15 @@ class AlbumNotifier extends StateNotifier> { /// Creat an album on the server with the same name as the selected album for backup /// First this will check if the album already exists on the server with name /// If it does not exist, it will create the album on the server - Future createMirrorAlbum( + Future createSyncAlbum( String albumName, ) async { final album = await getAlbumByName(albumName, remoteOnly: true); - print("Album: $album ${album?.localId} ${album?.remoteId}"); if (album != null) { return; } await createAlbum(albumName, {}); - - debugPrint("Create album $albumName on server"); } @override diff --git a/mobile/lib/providers/backup/backup.provider.dart b/mobile/lib/providers/backup/backup.provider.dart index d314202fb2b24..ec5a7e443205f 100644 --- a/mobile/lib/providers/backup/backup.provider.dart +++ b/mobile/lib/providers/backup/backup.provider.dart @@ -451,7 +451,7 @@ class BackupNotifier extends StateNotifier { /// Invoke backup process Future startBackupProcess() async { debugPrint("Start backup process"); - // assert(state.backupProgress == BackUpProgressEnum.idle); + assert(state.backupProgress == BackUpProgressEnum.idle); state = state.copyWith(backupProgress: BackUpProgressEnum.inProgress); await getBackupInfo(); diff --git a/mobile/lib/services/backup.service.dart b/mobile/lib/services/backup.service.dart index f41363c62c303..86147c6007fc4 100644 --- a/mobile/lib/services/backup.service.dart +++ b/mobile/lib/services/backup.service.dart @@ -118,16 +118,12 @@ class BackupService { now, ); - print("toAdd: ${toAdd.length}"); - final Set toRemove = await _fetchAssetsAndUpdateLastBackup( excludedAlbums, excludedBackupAlbums, now, ); - print("toRemove: ${toRemove.length}"); - return toAdd.difference(toRemove); } diff --git a/mobile/lib/widgets/backup/album_info_list_tile.dart b/mobile/lib/widgets/backup/album_info_list_tile.dart index 53cd92d290988..a4e99c21c4ce3 100644 --- a/mobile/lib/widgets/backup/album_info_list_tile.dart +++ b/mobile/lib/widgets/backup/album_info_list_tile.dart @@ -105,7 +105,7 @@ class AlbumInfoListTile extends HookConsumerWidget { } else { ref.read(backupProvider.notifier).addAlbumForBackup(album); if (shouldSyncUploadAlbum) { - ref.read(albumProvider.notifier).createMirrorAlbum(album.name); + ref.read(albumProvider.notifier).createSyncAlbum(album.name); } } }, From 42ed72e137033ef5ae9e7f0c6bf7e3a2468384d0 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 23 Aug 2024 14:23:20 -0500 Subject: [PATCH 17/34] background album sync --- .../lib/providers/album/album.provider.dart | 2 +- mobile/lib/services/album.service.dart | 4 +++ mobile/lib/services/background.service.dart | 34 +++++++++++++++++-- 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/mobile/lib/providers/album/album.provider.dart b/mobile/lib/providers/album/album.provider.dart index 75a941c5fd0ec..09298154fab64 100644 --- a/mobile/lib/providers/album/album.provider.dart +++ b/mobile/lib/providers/album/album.provider.dart @@ -46,7 +46,7 @@ class AlbumNotifier extends StateNotifier> { Future getAlbumByName(String albumName, {bool remoteOnly = false}) => _albumService.getAlbumByName(albumName, remoteOnly); - /// Creat an album on the server with the same name as the selected album for backup + /// Create an album on the server with the same name as the selected album for backup /// First this will check if the album already exists on the server with name /// If it does not exist, it will create the album on the server Future createSyncAlbum( diff --git a/mobile/lib/services/album.service.dart b/mobile/lib/services/album.service.dart index a26004ac50c5c..01c5628072689 100644 --- a/mobile/lib/services/album.service.dart +++ b/mobile/lib/services/album.service.dart @@ -467,6 +467,10 @@ class AlbumService { album.remoteId!, BulkIdsDto(ids: assetIds), ); + + print( + "------------- finished putting assets $assetIds to album $albumName", + ); } } } diff --git a/mobile/lib/services/background.service.dart b/mobile/lib/services/background.service.dart index 9fde33589a839..7cd242c06f8f2 100644 --- a/mobile/lib/services/background.service.dart +++ b/mobile/lib/services/background.service.dart @@ -12,6 +12,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/main.dart'; import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; import 'package:immich_mobile/models/backup/success_upload_asset.model.dart'; +import 'package:immich_mobile/services/album.service.dart'; +import 'package:immich_mobile/services/hash.service.dart'; import 'package:immich_mobile/services/localization.service.dart'; import 'package:immich_mobile/entities/backup_album.entity.dart'; import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; @@ -20,6 +22,9 @@ import 'package:immich_mobile/services/backup.service.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/services/api.service.dart'; +import 'package:immich_mobile/services/partner.service.dart'; +import 'package:immich_mobile/services/sync.service.dart'; +import 'package:immich_mobile/services/user.service.dart'; import 'package:immich_mobile/utils/backup_progress.dart'; import 'package:immich_mobile/utils/diff.dart'; import 'package:immich_mobile/utils/http_ssl_cert_override.dart'; @@ -349,6 +354,13 @@ class BackgroundService { AppSettingsService settingService = AppSettingsService(); BackupService backupService = BackupService(apiService, db, settingService); AppSettingsService settingsService = AppSettingsService(); + PartnerService partnerService = PartnerService(apiService, db); + HashService hashService = HashService(db, this); + SyncService syncSerive = SyncService(db, hashService); + UserService userService = + UserService(apiService, db, syncSerive, partnerService); + AlbumService albumService = + AlbumService(apiService, userService, syncSerive, db, backupService); final selectedAlbums = backupService.selectedAlbumsQuery().findAllSync(); final excludedAlbums = backupService.excludedAlbumsQuery().findAllSync(); @@ -364,6 +376,7 @@ class BackgroundService { settingsService, selectedAlbums, excludedAlbums, + albumService, ); if (backupOk) { await Store.delete(StoreKey.backupFailedSince); @@ -407,6 +420,7 @@ class BackgroundService { AppSettingsService settingsService, List selectedAlbums, List excludedAlbums, + AlbumService albumService, ) async { _errorGracePeriodExceeded = _isErrorGracePeriodExceeded(settingsService); final bool notifyTotalProgress = settingsService @@ -463,8 +477,11 @@ class BackgroundService { toUpload, _cancellationToken!, pmProgressHandler: pmProgressHandler, - onSuccess: (result) => - _onAssetUploaded(result: result, shouldNotify: notifyTotalProgress), + onSuccess: (result) => _onAssetUploaded( + result: result, + shouldNotify: notifyTotalProgress, + albumService: albumService, + ), onProgress: (bytes, totalBytes) => _onProgress(bytes, totalBytes, shouldNotify: notifySingleProgress), onCurrentAsset: (asset) => @@ -485,8 +502,19 @@ class BackgroundService { void _onAssetUploaded({ required SuccessUploadAsset result, + required AlbumService albumService, bool shouldNotify = false, - }) { + }) async { + final shouldSyncUploadAlbum = + Store.get(StoreKey.enableSyncUploadAlbum, false); + + if (shouldSyncUploadAlbum && !result.isDuplicate) { + await albumService.syncUploadAlbums( + result.candidate.albums, + [result.remoteAssetId], + ); + } + if (!shouldNotify) { return; } From 5532df1992cf0328dc48879bc7bf5ae71f3454c9 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 23 Aug 2024 19:40:34 -0500 Subject: [PATCH 18/34] add to album in background context --- .../lib/providers/album/album.provider.dart | 1 - .../lib/providers/backup/backup.provider.dart | 1 - mobile/lib/services/album.service.dart | 19 +++++++++---------- mobile/lib/services/background.service.dart | 19 +++---------------- mobile/lib/services/backup.service.dart | 14 ++++++++++++++ 5 files changed, 26 insertions(+), 28 deletions(-) diff --git a/mobile/lib/providers/album/album.provider.dart b/mobile/lib/providers/album/album.provider.dart index 09298154fab64..ed9dc07f5e5c0 100644 --- a/mobile/lib/providers/album/album.provider.dart +++ b/mobile/lib/providers/album/album.provider.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:flutter/foundation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/services/album.service.dart'; import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; diff --git a/mobile/lib/providers/backup/backup.provider.dart b/mobile/lib/providers/backup/backup.provider.dart index ec5a7e443205f..cdc43269ad91b 100644 --- a/mobile/lib/providers/backup/backup.provider.dart +++ b/mobile/lib/providers/backup/backup.provider.dart @@ -1,5 +1,4 @@ import 'dart:io'; -import 'dart:isolate'; import 'package:cancellation_token_http/http.dart'; import 'package:collection/collection.dart'; diff --git a/mobile/lib/services/album.service.dart b/mobile/lib/services/album.service.dart index 01c5628072689..79aa8e7494363 100644 --- a/mobile/lib/services/album.service.dart +++ b/mobile/lib/services/album.service.dart @@ -7,7 +7,6 @@ import 'package:flutter/foundation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/models/albums/album_add_asset_response.model.dart'; import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/services/backup.service.dart'; import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart'; @@ -28,7 +27,6 @@ final albumServiceProvider = Provider( ref.watch(userServiceProvider), ref.watch(syncServiceProvider), ref.watch(dbProvider), - ref.watch(backupServiceProvider), ), ); @@ -37,7 +35,6 @@ class AlbumService { final UserService _userService; final SyncService _syncService; final Isar _db; - final BackupService _backupService; final Logger _log = Logger('AlbumService'); Completer _localCompleter = Completer()..complete(false); Completer _remoteCompleter = Completer()..complete(false); @@ -47,9 +44,15 @@ class AlbumService { this._userService, this._syncService, this._db, - this._backupService, ); + QueryBuilder + selectedAlbumsQuery() => + _db.backupAlbums.filter().selectionEqualTo(BackupSelection.select); + QueryBuilder + excludedAlbumsQuery() => + _db.backupAlbums.filter().selectionEqualTo(BackupSelection.exclude); + /// Checks all selected device albums for changes of albums and their assets /// Updates the local database and returns `true` if there were any changes Future refreshDeviceAlbums() async { @@ -63,9 +66,9 @@ class AlbumService { bool changes = false; try { final List excludedIds = - await _backupService.excludedAlbumsQuery().idProperty().findAll(); + await excludedAlbumsQuery().idProperty().findAll(); final List selectedIds = - await _backupService.selectedAlbumsQuery().idProperty().findAll(); + await selectedAlbumsQuery().idProperty().findAll(); if (selectedIds.isEmpty) { final numLocal = await _db.albums.where().localIdIsNotNull().count(); if (numLocal > 0) { @@ -467,10 +470,6 @@ class AlbumService { album.remoteId!, BulkIdsDto(ids: assetIds), ); - - print( - "------------- finished putting assets $assetIds to album $albumName", - ); } } } diff --git a/mobile/lib/services/background.service.dart b/mobile/lib/services/background.service.dart index 7cd242c06f8f2..e387e0ed1e2b9 100644 --- a/mobile/lib/services/background.service.dart +++ b/mobile/lib/services/background.service.dart @@ -352,7 +352,6 @@ class BackgroundService { ApiService apiService = ApiService(); apiService.setAccessToken(Store.get(StoreKey.accessToken)); AppSettingsService settingService = AppSettingsService(); - BackupService backupService = BackupService(apiService, db, settingService); AppSettingsService settingsService = AppSettingsService(); PartnerService partnerService = PartnerService(apiService, db); HashService hashService = HashService(db, this); @@ -360,7 +359,9 @@ class BackgroundService { UserService userService = UserService(apiService, db, syncSerive, partnerService); AlbumService albumService = - AlbumService(apiService, userService, syncSerive, db, backupService); + AlbumService(apiService, userService, syncSerive, db); + BackupService backupService = + BackupService(apiService, db, settingService, albumService); final selectedAlbums = backupService.selectedAlbumsQuery().findAllSync(); final excludedAlbums = backupService.excludedAlbumsQuery().findAllSync(); @@ -376,7 +377,6 @@ class BackgroundService { settingsService, selectedAlbums, excludedAlbums, - albumService, ); if (backupOk) { await Store.delete(StoreKey.backupFailedSince); @@ -420,7 +420,6 @@ class BackgroundService { AppSettingsService settingsService, List selectedAlbums, List excludedAlbums, - AlbumService albumService, ) async { _errorGracePeriodExceeded = _isErrorGracePeriodExceeded(settingsService); final bool notifyTotalProgress = settingsService @@ -480,7 +479,6 @@ class BackgroundService { onSuccess: (result) => _onAssetUploaded( result: result, shouldNotify: notifyTotalProgress, - albumService: albumService, ), onProgress: (bytes, totalBytes) => _onProgress(bytes, totalBytes, shouldNotify: notifySingleProgress), @@ -502,19 +500,8 @@ class BackgroundService { void _onAssetUploaded({ required SuccessUploadAsset result, - required AlbumService albumService, bool shouldNotify = false, }) async { - final shouldSyncUploadAlbum = - Store.get(StoreKey.enableSyncUploadAlbum, false); - - if (shouldSyncUploadAlbum && !result.isDuplicate) { - await albumService.syncUploadAlbums( - result.candidate.albums, - [result.remoteAssetId], - ); - } - if (!shouldNotify) { return; } diff --git a/mobile/lib/services/backup.service.dart b/mobile/lib/services/backup.service.dart index 86147c6007fc4..5628561f5a6f9 100644 --- a/mobile/lib/services/backup.service.dart +++ b/mobile/lib/services/backup.service.dart @@ -16,6 +16,7 @@ import 'package:immich_mobile/models/backup/success_upload_asset.model.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/db.provider.dart'; +import 'package:immich_mobile/services/album.service.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:isar/isar.dart'; @@ -30,6 +31,7 @@ final backupServiceProvider = Provider( ref.watch(apiServiceProvider), ref.watch(dbProvider), ref.watch(appSettingsServiceProvider), + ref.watch(albumServiceProvider), ), ); @@ -39,11 +41,13 @@ class BackupService { final Isar _db; final Logger _log = Logger("BackupService"); final AppSettingsService _appSetting; + final AlbumService _albumService; BackupService( this._apiService, this._db, this._appSetting, + this._albumService, ); Future?> getDeviceBackupAsset() async { @@ -457,6 +461,16 @@ class BackupService { isDuplicate: isDuplicate, ), ); + + final shouldSyncUploadAlbum = + Store.get(StoreKey.enableSyncUploadAlbum, false); + + if (shouldSyncUploadAlbum && !isDuplicate) { + await _albumService.syncUploadAlbums( + candidate.albums, + [responseBody['id'] as String], + ); + } } } on http.CancelledException { debugPrint("Backup was cancelled by the user"); From 1ff0bbecfe9944be70c966ba47245a9e413440ca Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 23 Aug 2024 19:44:15 -0500 Subject: [PATCH 19/34] remove add to album in callback --- mobile/lib/providers/backup/backup.provider.dart | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/mobile/lib/providers/backup/backup.provider.dart b/mobile/lib/providers/backup/backup.provider.dart index cdc43269ad91b..2ad0d5cc6e39f 100644 --- a/mobile/lib/providers/backup/backup.provider.dart +++ b/mobile/lib/providers/backup/backup.provider.dart @@ -576,16 +576,6 @@ class BackupNotifier extends StateNotifier { _updatePersistentAlbumsSelection(); } - final shouldSyncUploadAlbum = - Store.get(StoreKey.enableSyncUploadAlbum, false); - - if (shouldSyncUploadAlbum && !result.isDuplicate) { - await _albumService.syncUploadAlbums( - result.candidate.albums, - [result.remoteAssetId], - ); - } - updateDiskInfo(); } From 70e9f5ddf8581e43fa26c2bfee90d201a19effe0 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 23 Aug 2024 21:07:48 -0500 Subject: [PATCH 20/34] refactor --- mobile/lib/entities/store.entity.dart | 2 +- .../models/backup/backup_candidate.model.dart | 4 +- .../backup/backup_album_selection.page.dart | 2 +- .../lib/providers/backup/backup.provider.dart | 12 ++-- .../backup/manual_upload.provider.dart | 2 +- mobile/lib/services/app_settings.service.dart | 2 +- mobile/lib/services/asset.service.dart | 2 +- mobile/lib/services/backup.service.dart | 71 +++++++++++-------- .../widgets/backup/album_info_list_tile.dart | 2 +- 9 files changed, 53 insertions(+), 46 deletions(-) diff --git a/mobile/lib/entities/store.entity.dart b/mobile/lib/entities/store.entity.dart index d39ca438559d8..1dda2b9a12a03 100644 --- a/mobile/lib/entities/store.entity.dart +++ b/mobile/lib/entities/store.entity.dart @@ -235,7 +235,7 @@ enum StoreKey { dynamicTheme(129, type: bool), colorfulInterface(130, type: bool), - enableSyncUploadAlbum(131, type: bool), + syncAlbums(131, type: bool), ; const StoreKey( diff --git a/mobile/lib/models/backup/backup_candidate.model.dart b/mobile/lib/models/backup/backup_candidate.model.dart index 986ad795f92a2..5ef15167455df 100644 --- a/mobile/lib/models/backup/backup_candidate.model.dart +++ b/mobile/lib/models/backup/backup_candidate.model.dart @@ -1,10 +1,10 @@ import 'package:photo_manager/photo_manager.dart'; class BackupCandidate { - BackupCandidate({required this.asset, required this.albums}); + BackupCandidate({required this.asset, required this.albumNames}); AssetEntity asset; - List albums; + List albumNames; @override int get hashCode => asset.hashCode; diff --git a/mobile/lib/pages/backup/backup_album_selection.page.dart b/mobile/lib/pages/backup/backup_album_selection.page.dart index 3d90f85b9f72b..605c2bc47e7d9 100644 --- a/mobile/lib/pages/backup/backup_album_selection.page.dart +++ b/mobile/lib/pages/backup/backup_album_selection.page.dart @@ -20,7 +20,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { final selectedBackupAlbums = ref.watch(backupProvider).selectedBackupAlbums; final excludedBackupAlbums = ref.watch(backupProvider).excludedBackupAlbums; final enableSyncUploadAlbum = - useAppSettingsState(AppSettingsEnum.enableSyncUploadAlbum); + useAppSettingsState(AppSettingsEnum.syncAlbums); final isDarkTheme = context.isDarkTheme; final albums = ref.watch(backupProvider).availableAlbums; diff --git a/mobile/lib/providers/backup/backup.provider.dart b/mobile/lib/providers/backup/backup.provider.dart index 2ad0d5cc6e39f..5ee0366364d2f 100644 --- a/mobile/lib/providers/backup/backup.provider.dart +++ b/mobile/lib/providers/backup/backup.provider.dart @@ -13,7 +13,6 @@ import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; import 'package:immich_mobile/models/backup/error_upload_asset.model.dart'; import 'package:immich_mobile/models/backup/success_upload_asset.model.dart'; import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart'; -import 'package:immich_mobile/services/album.service.dart'; import 'package:immich_mobile/services/background.service.dart'; import 'package:immich_mobile/services/backup.service.dart'; import 'package:immich_mobile/models/authentication/authentication_state.model.dart'; @@ -38,7 +37,6 @@ class BackupNotifier extends StateNotifier { this._authState, this._backgroundService, this._galleryPermissionNotifier, - this._albumService, this._db, this.ref, ) : super( @@ -87,7 +85,6 @@ class BackupNotifier extends StateNotifier { final AuthenticationState _authState; final BackgroundService _backgroundService; final GalleryPermissionNotifier _galleryPermissionNotifier; - final AlbumService _albumService; final Isar _db; final Ref ref; @@ -313,21 +310,21 @@ class BackupNotifier extends StateNotifier { // Add album's name to the asset info for (final asset in assets) { - List albums = [album.name]; + List albumNames = [album.name]; final existingAsset = assetsFromSelectedAlbums.firstWhereOrNull( (a) => a.asset.id == asset.id, ); if (existingAsset != null) { - albums.addAll(existingAsset.albums); + albumNames.addAll(existingAsset.albumNames); assetsFromSelectedAlbums.remove(existingAsset); } assetsFromSelectedAlbums.add( BackupCandidate( asset: asset, - albums: albums, + albumNames: albumNames, ), ); } @@ -347,7 +344,7 @@ class BackupNotifier extends StateNotifier { assets.forEach((asset) { assetsFromExcludedAlbums.add( - BackupCandidate(asset: asset, albums: [album.name]), + BackupCandidate(asset: asset, albumNames: [album.name]), ); }); } @@ -743,7 +740,6 @@ final backupProvider = ref.watch(authenticationProvider), ref.watch(backgroundServiceProvider), ref.watch(galleryPermissionNotifier.notifier), - ref.watch(albumServiceProvider), ref.watch(dbProvider), ref, ); diff --git a/mobile/lib/providers/backup/manual_upload.provider.dart b/mobile/lib/providers/backup/manual_upload.provider.dart index 410ce22a44a8e..c7a20bc812db6 100644 --- a/mobile/lib/providers/backup/manual_upload.provider.dart +++ b/mobile/lib/providers/backup/manual_upload.provider.dart @@ -208,7 +208,7 @@ class ManualUploadNotifier extends StateNotifier { } Set allUploadAssets = allAssetsFromDevice.nonNulls - .map((a) => BackupCandidate(asset: a, albums: [])) + .map((asset) => BackupCandidate(asset: a, albumNames: [])) .toSet(); if (allUploadAssets.isEmpty) { diff --git a/mobile/lib/services/app_settings.service.dart b/mobile/lib/services/app_settings.service.dart index c81e174fd7ad0..8f773e1bb33a9 100644 --- a/mobile/lib/services/app_settings.service.dart +++ b/mobile/lib/services/app_settings.service.dart @@ -76,7 +76,7 @@ enum AppSettingsEnum { false, ), enableHapticFeedback(StoreKey.enableHapticFeedback, null, true), - enableSyncUploadAlbum(StoreKey.enableSyncUploadAlbum, null, false), + syncAlbums(StoreKey.syncAlbums, null, false), ; const AppSettingsEnum(this.storeKey, this.hiveKey, this.defaultValue); diff --git a/mobile/lib/services/asset.service.dart b/mobile/lib/services/asset.service.dart index 42c810349fce3..bffc4ddb2b602 100644 --- a/mobile/lib/services/asset.service.dart +++ b/mobile/lib/services/asset.service.dart @@ -335,7 +335,7 @@ class AssetService { ); if (asset != null) { - for (final albumName in candidate.albums) { + for (final albumName in candidate.albumNames) { assetToAlbums.putIfAbsent(albumName, () => []).add(asset.remoteId!); } } diff --git a/mobile/lib/services/backup.service.dart b/mobile/lib/services/backup.service.dart index 5628561f5a6f9..98cdd1c038dc4 100644 --- a/mobile/lib/services/backup.service.dart +++ b/mobile/lib/services/backup.service.dart @@ -190,14 +190,14 @@ class BackupService { ); if (existingAsset != null) { - albums.addAll(existingAsset.albums); + albums.addAll(existingAsset.albumNames); candidate.remove(existingAsset); } candidate.add( BackupCandidate( asset: asset, - albums: albums, + albumNames: albums, ), ); } @@ -254,6 +254,37 @@ class BackupService { return candidates; } + Future _checkPermissions() async { + if (Platform.isAndroid && + !(await pm.Permission.accessMediaLocation.status).isGranted) { + // double check that permission is granted here, to guard against + // uploading corrupt assets without EXIF information + _log.warning("Media location permission is not granted. " + "Cannot access original assets for backup."); + + return false; + } + + // DON'T KNOW WHY BUT THIS HELPS BACKGROUND BACKUP TO WORK ON IOS + if (Platform.isIOS) { + await PhotoManager.requestPermissionExtend(); + } + + return true; + } + + /// Upload images before video assets for background tasks + /// these are further sorted by using their creation date + List sortVideosFirst(List candidates) { + return candidates.sorted( + (a, b) { + final cmp = a.asset.typeInt - b.asset.typeInt; + if (cmp != 0) return cmp; + return a.asset.createDateTime.compareTo(b.asset.createDateTime); + }, + ); + } + Future backupAsset( Iterable assetList, http.CancellationToken cancelToken, { @@ -266,42 +297,25 @@ class BackupService { }) async { final bool isIgnoreIcloudAssets = _appSetting.getSetting(AppSettingsEnum.ignoreIcloudAssets); - - if (Platform.isAndroid && - !(await pm.Permission.accessMediaLocation.status).isGranted) { - // double check that permission is granted here, to guard against - // uploading corrupt assets without EXIF information - _log.warning("Media location permission is not granted. " - "Cannot access original assets for backup."); - return false; - } + final shouldSyncAlbums = _appSetting.getSetting(AppSettingsEnum.syncAlbums); final String deviceId = Store.get(StoreKey.deviceId); final String savedEndpoint = Store.get(StoreKey.serverEndpoint); - bool anyErrors = false; final List duplicatedAssetIds = []; + bool anyErrors = false; - // DON'T KNOW WHY BUT THIS HELPS BACKGROUND BACKUP TO WORK ON IOS - if (Platform.isIOS) { - await PhotoManager.requestPermissionExtend(); + final hasPermission = await _checkPermissions(); + if (!hasPermission) { + return false; } List candidates = assetList.toList(); - // Upload images before video assets for background tasks - // these are further sorted by using their creation date if (sortAssets) { - candidates = assetList.sorted( - (a, b) { - final cmp = a.asset.typeInt - b.asset.typeInt; - if (cmp != 0) return cmp; - return a.asset.createDateTime.compareTo(b.asset.createDateTime); - }, - ); + candidates = sortVideosFirst(candidates); } for (final candidate in candidates) { final AssetEntity entity = candidate.asset; - File? file; File? livePhotoFile; @@ -462,12 +476,9 @@ class BackupService { ), ); - final shouldSyncUploadAlbum = - Store.get(StoreKey.enableSyncUploadAlbum, false); - - if (shouldSyncUploadAlbum && !isDuplicate) { + if (shouldSyncAlbums && !isDuplicate) { await _albumService.syncUploadAlbums( - candidate.albums, + candidate.albumNames, [responseBody['id'] as String], ); } diff --git a/mobile/lib/widgets/backup/album_info_list_tile.dart b/mobile/lib/widgets/backup/album_info_list_tile.dart index a4e99c21c4ce3..6f4c32cac3fdf 100644 --- a/mobile/lib/widgets/backup/album_info_list_tile.dart +++ b/mobile/lib/widgets/backup/album_info_list_tile.dart @@ -27,7 +27,7 @@ class AlbumInfoListTile extends HookConsumerWidget { var assetCount = useState(0); final shouldSyncUploadAlbum = ref .watch(appSettingsServiceProvider) - .getSetting(AppSettingsEnum.enableSyncUploadAlbum); + .getSetting(AppSettingsEnum.syncAlbums); useEffect( () { From baf39eace34c0aa52bf2e20929c26b99a6adff0e Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 23 Aug 2024 21:08:19 -0500 Subject: [PATCH 21/34] refactor --- mobile/lib/providers/backup/manual_upload.provider.dart | 2 +- mobile/lib/services/backup.service.dart | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/mobile/lib/providers/backup/manual_upload.provider.dart b/mobile/lib/providers/backup/manual_upload.provider.dart index c7a20bc812db6..007f80b4bf5fd 100644 --- a/mobile/lib/providers/backup/manual_upload.provider.dart +++ b/mobile/lib/providers/backup/manual_upload.provider.dart @@ -208,7 +208,7 @@ class ManualUploadNotifier extends StateNotifier { } Set allUploadAssets = allAssetsFromDevice.nonNulls - .map((asset) => BackupCandidate(asset: a, albumNames: [])) + .map((asset) => BackupCandidate(asset: asset, albumNames: [])) .toSet(); if (allUploadAssets.isEmpty) { diff --git a/mobile/lib/services/backup.service.dart b/mobile/lib/services/backup.service.dart index 98cdd1c038dc4..92626be2e401b 100644 --- a/mobile/lib/services/backup.service.dart +++ b/mobile/lib/services/backup.service.dart @@ -183,21 +183,21 @@ class BackupService { // Add album's name to the asset info for (final asset in assets) { - List albums = [album.name]; + List albumNames = [album.name]; final existingAsset = candidate.firstWhereOrNull( (a) => a.asset.id == asset.id, ); if (existingAsset != null) { - albums.addAll(existingAsset.albumNames); + albumNames.addAll(existingAsset.albumNames); candidate.remove(existingAsset); } candidate.add( BackupCandidate( asset: asset, - albumNames: albums, + albumNames: albumNames, ), ); } From af42bfcb3850f372a3b84d73efd466247364ccf9 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 23 Aug 2024 21:28:07 -0500 Subject: [PATCH 22/34] refactor --- mobile/lib/services/background.service.dart | 2 +- mobile/lib/services/backup.service.dart | 13 ++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/mobile/lib/services/background.service.dart b/mobile/lib/services/background.service.dart index e387e0ed1e2b9..b27ed34b946ce 100644 --- a/mobile/lib/services/background.service.dart +++ b/mobile/lib/services/background.service.dart @@ -485,7 +485,7 @@ class BackgroundService { onCurrentAsset: (asset) => _onSetCurrentBackupAsset(asset, shouldNotify: notifySingleProgress), onError: _onBackupError, - sortAssets: true, + isBackground: true, ); if (!ok && !_cancellationToken!.isCancelled) { diff --git a/mobile/lib/services/backup.service.dart b/mobile/lib/services/backup.service.dart index 92626be2e401b..2e4d4970fa951 100644 --- a/mobile/lib/services/backup.service.dart +++ b/mobile/lib/services/backup.service.dart @@ -275,7 +275,7 @@ class BackupService { /// Upload images before video assets for background tasks /// these are further sorted by using their creation date - List sortVideosFirst(List candidates) { + List _sortPhotosFirst(List candidates) { return candidates.sorted( (a, b) { final cmp = a.asset.typeInt - b.asset.typeInt; @@ -286,9 +286,9 @@ class BackupService { } Future backupAsset( - Iterable assetList, + Iterable assets, http.CancellationToken cancelToken, { - bool sortAssets = false, + bool isBackground = false, PMProgressHandler? pmProgressHandler, required void Function(SuccessUploadAsset result) onSuccess, required void Function(int bytes, int totalBytes) onProgress, @@ -308,10 +308,9 @@ class BackupService { return false; } - List candidates = assetList.toList(); - - if (sortAssets) { - candidates = sortVideosFirst(candidates); + List candidates = assets.toList(); + if (isBackground) { + candidates = _sortPhotosFirst(candidates); } for (final candidate in candidates) { From 2472c4cf11c732da14cb78f77a3a4907830669cf Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 23 Aug 2024 22:49:54 -0500 Subject: [PATCH 23/34] fix: make sure all selected albums are selected for building upload candidate --- mobile/lib/services/backup.service.dart | 42 +++++++------------------ 1 file changed, 12 insertions(+), 30 deletions(-) diff --git a/mobile/lib/services/backup.service.dart b/mobile/lib/services/backup.service.dart index 2e4d4970fa951..5cc30a4eb2535 100644 --- a/mobile/lib/services/backup.service.dart +++ b/mobile/lib/services/backup.service.dart @@ -94,14 +94,10 @@ class BackupService { videoOption: const FilterOption(needTitle: true), ); - final now = DateTime.now(); - final List selectedAlbums = await _loadAlbumsWithTimeFilter( selectedBackupAlbums, filter, - now, - ignoreTimeFilter: ignoreTimeFilter, ); if (selectedAlbums.every((e) => e == null)) { @@ -112,20 +108,16 @@ class BackupService { await _loadAlbumsWithTimeFilter( excludedBackupAlbums, filter, - now, - ignoreTimeFilter: ignoreTimeFilter, ); final Set toAdd = await _fetchAssetsAndUpdateLastBackup( selectedAlbums, selectedBackupAlbums, - now, ); final Set toRemove = await _fetchAssetsAndUpdateLastBackup( excludedAlbums, excludedBackupAlbums, - now, ); return toAdd.difference(toRemove); @@ -134,9 +126,7 @@ class BackupService { Future> _loadAlbumsWithTimeFilter( List albums, FilterOptionGroup filter, - DateTime now, { - bool ignoreTimeFilter = false, - }) async { + ) async { List result = []; for (BackupAlbum backupAlbum in albums) { @@ -144,16 +134,7 @@ class BackupService { final AssetPathEntity album = await AssetPathEntity.obtainPathFromProperties( id: backupAlbum.id, - optionGroup: filter.copyWith( - updateTimeCond: ignoreTimeFilter - ? null - : DateTimeCond( - // subtract 2 seconds to prevent missing assets due to rounding issues - min: backupAlbum.lastBackup - .subtract(const Duration(seconds: 2)), - max: now, - ), - ), + optionGroup: filter, maxDateTimeToNow: false, ); result.add(album); @@ -167,7 +148,6 @@ class BackupService { Future> _fetchAssetsAndUpdateLastBackup( List albums, List backupAlbums, - DateTime now, ) async { Set candidate = {}; @@ -201,9 +181,6 @@ class BackupService { ), ); } - - final idx = albums.indexOf(album); - backupAlbums[idx].lastBackup = now; } return candidate; @@ -247,10 +224,15 @@ class BackupService { } } + print( + "[AAAA] before remove existing: ${candidates.length} - Existing: ${existing.length}", + ); if (existing.isNotEmpty) { candidates.removeWhere((c) => existing.contains(c.asset.id)); } + print("[AAAA] after remove existing: ${candidates.length}"); + return candidates; } @@ -315,6 +297,9 @@ class BackupService { for (final candidate in candidates) { final AssetEntity entity = candidate.asset; + print( + "[AAAA] Uploading asset: ${entity.id} | albums ${candidate.albumNames}", + ); File? file; File? livePhotoFile; @@ -384,9 +369,9 @@ class BackupService { Uri.parse('$savedEndpoint/assets'), onProgress: ((bytes, totalBytes) => onProgress(bytes, totalBytes)), ); + baseRequest.headers.addAll(ApiService.getRequestHeaders()); baseRequest.headers["Transfer-Encoding"] = "chunked"; - baseRequest.fields['deviceAssetId'] = entity.id; baseRequest.fields['deviceId'] = deviceId; baseRequest.fields['fileCreatedAt'] = @@ -395,11 +380,8 @@ class BackupService { entity.modifiedDateTime.toUtc().toIso8601String(); baseRequest.fields['isFavorite'] = entity.isFavorite.toString(); baseRequest.fields['duration'] = entity.videoDuration.toString(); - baseRequest.files.add(assetRawUploadData); - final fileSize = file.lengthSync(); - onCurrentAsset( CurrentUploadAsset( id: entity.id, @@ -408,7 +390,7 @@ class BackupService { : entity.createDateTime, fileName: originalFileName, fileType: _getAssetType(entity.type), - fileSize: fileSize, + fileSize: file.lengthSync(), iCloudAsset: false, ), ); From 24a97b8bc8bfe17848e686225fd5140a4cd13e06 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 24 Aug 2024 19:55:23 -0500 Subject: [PATCH 24/34] clean up --- mobile/lib/services/album.service.dart | 6 +++++- mobile/lib/services/asset.service.dart | 1 - mobile/lib/services/backup.service.dart | 13 +++++-------- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/mobile/lib/services/album.service.dart b/mobile/lib/services/album.service.dart index 79aa8e7494363..8f6f22363e8a9 100644 --- a/mobile/lib/services/album.service.dart +++ b/mobile/lib/services/album.service.dart @@ -463,7 +463,11 @@ class AlbumService { List assetIds, ) async { for (final albumName in albumNames) { - final album = await getAlbumByName(albumName, true); + Album? album = await getAlbumByName(albumName, true); + + if (album == null) { + album = await createAlbum(albumName, []); + } if (album != null && album.remoteId != null) { await _apiService.albumsApi.addAssetsToAlbum( diff --git a/mobile/lib/services/asset.service.dart b/mobile/lib/services/asset.service.dart index bffc4ddb2b602..d14fc289bab6b 100644 --- a/mobile/lib/services/asset.service.dart +++ b/mobile/lib/services/asset.service.dart @@ -304,7 +304,6 @@ class AssetService { final candidates = await _backupService.buildUploadCandidates( selectedAlbums, excludedAlbums, - ignoreTimeFilter: true, ); final duplicates = await _apiService.assetsApi.checkExistingAssets( diff --git a/mobile/lib/services/backup.service.dart b/mobile/lib/services/backup.service.dart index 5cc30a4eb2535..885b79eed0f5c 100644 --- a/mobile/lib/services/backup.service.dart +++ b/mobile/lib/services/backup.service.dart @@ -83,9 +83,8 @@ class BackupService { /// if `ignoreTimeFilter` is set to true, all assets will be returned Future> buildUploadCandidates( List selectedBackupAlbums, - List excludedBackupAlbums, { - bool ignoreTimeFilter = false, - }) async { + List excludedBackupAlbums, + ) async { final filter = FilterOptionGroup( containsPathModified: true, orders: [const OrderOption(type: OrderOptionType.updateDate)], @@ -297,9 +296,6 @@ class BackupService { for (final candidate in candidates) { final AssetEntity entity = candidate.asset; - print( - "[AAAA] Uploading asset: ${entity.id} | albums ${candidate.albumNames}", - ); File? file; File? livePhotoFile; @@ -440,6 +436,7 @@ class BackupService { anyErrors = true; break; } + continue; } @@ -468,8 +465,8 @@ class BackupService { debugPrint("Backup was cancelled by the user"); anyErrors = true; break; - } catch (e) { - debugPrint("ERROR backupAsset: ${e.toString()}"); + } catch (error, stackTrace) { + debugPrint("Error backup asset: ${error.toString()}: $stackTrace"); anyErrors = true; continue; } finally { From 9c901bdd768e9fb60d79a56a1db1ef4f70f55d40 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 25 Aug 2024 12:21:27 -0500 Subject: [PATCH 25/34] add manual sync button --- mobile/assets/i18n/en-US.json | 6 ++-- .../backup/backup_album_selection.page.dart | 2 +- .../pages/backup/backup_controller.page.dart | 7 ---- .../backup_settings/backup_settings.dart | 35 +++++++++++++++++++ .../settings/settings_button_list_tile.dart | 5 ++- 5 files changed, 44 insertions(+), 11 deletions(-) diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json index 2113a27f120e1..c092b79bd1d6e 100644 --- a/mobile/assets/i18n/en-US.json +++ b/mobile/assets/i18n/en-US.json @@ -574,6 +574,8 @@ "version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.", "version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89", "viewer_unstack": "Un-Stack", - "sync_upload_album_setting_title": "Sync albums", - "sync_upload_album_setting_subtitle": "Create and upload your photos and videos to the selected albums on Immich" + "sync_albums": "Sync albums", + "sync_upload_album_setting_subtitle": "Create and upload your photos and videos to the selected albums on Immich", + "sync_albums_manual_subtitle": "Sync all uploaded videos and photos to the selected backup albums", + "sync": "Sync" } diff --git a/mobile/lib/pages/backup/backup_album_selection.page.dart b/mobile/lib/pages/backup/backup_album_selection.page.dart index 605c2bc47e7d9..080beaca6148f 100644 --- a/mobile/lib/pages/backup/backup_album_selection.page.dart +++ b/mobile/lib/pages/backup/backup_album_selection.page.dart @@ -172,7 +172,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { SettingsSwitchListTile( valueNotifier: enableSyncUploadAlbum, - title: "sync_upload_album_setting_title".tr(), + title: "sync_albums".tr(), subtitle: "sync_upload_album_setting_subtitle".tr(), contentPadding: EdgeInsets.symmetric(horizontal: 16), titleStyle: context.textTheme.bodyLarge?.copyWith( diff --git a/mobile/lib/pages/backup/backup_controller.page.dart b/mobile/lib/pages/backup/backup_controller.page.dart index 894f913d7b6e5..bb9d462e50bc4 100644 --- a/mobile/lib/pages/backup/backup_controller.page.dart +++ b/mobile/lib/pages/backup/backup_controller.page.dart @@ -18,7 +18,6 @@ import 'package:immich_mobile/providers/backup/ios_background_settings.provider. import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; import 'package:immich_mobile/providers/websocket.provider.dart'; import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/services/asset.service.dart'; import 'package:immich_mobile/widgets/backup/backup_info_card.dart'; import 'package:immich_mobile/widgets/backup/current_backup_asset_info_box.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; @@ -349,12 +348,6 @@ class BackupControllerPage extends HookConsumerWidget { // crossAxisAlignment: CrossAxisAlignment.start, children: hasAnyAlbum ? [ - ElevatedButton( - child: Text("test"), - onPressed: () => ref - .read(assetServiceProvider) - .syncUploadedAssetToAlbums(), - ), buildFolderSelectionTile(), BackupInfoCard( title: "backup_controller_page_total".tr(), diff --git a/mobile/lib/widgets/settings/backup_settings/backup_settings.dart b/mobile/lib/widgets/settings/backup_settings/backup_settings.dart index 25bcf2d06e507..3c4a0b693761b 100644 --- a/mobile/lib/widgets/settings/backup_settings/backup_settings.dart +++ b/mobile/lib/widgets/settings/backup_settings/backup_settings.dart @@ -1,9 +1,13 @@ import 'dart:io'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/providers/asset_viewer/image_viewer_page_state.provider.dart'; import 'package:immich_mobile/providers/backup/backup_verification.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; +import 'package:immich_mobile/services/asset.service.dart'; import 'package:immich_mobile/widgets/settings/backup_settings/background_settings.dart'; import 'package:immich_mobile/widgets/settings/backup_settings/foreground_settings.dart'; import 'package:immich_mobile/widgets/settings/settings_button_list_tile.dart'; @@ -23,7 +27,21 @@ class BackupSettings extends HookConsumerWidget { useAppSettingsState(AppSettingsEnum.ignoreIcloudAssets); final isAdvancedTroubleshooting = useAppSettingsState(AppSettingsEnum.advancedTroubleshooting); + final albumSync = useAppSettingsState(AppSettingsEnum.syncAlbums); final isCorruptCheckInProgress = ref.watch(backupVerificationProvider); + final isAlbumSyncInProgress = useState(false); + + syncAlbums() async { + isAlbumSyncInProgress.value = true; + try { + await ref.read(assetServiceProvider).syncUploadedAssetToAlbums(); + } catch (_) { + } finally { + Future.delayed(Duration(seconds: 1), () { + isAlbumSyncInProgress.value = false; + }); + } + } final backupSettings = [ const ForegroundBackupSettings(), @@ -58,6 +76,23 @@ class BackupSettings extends HookConsumerWidget { .performBackupCheck(context) : null, ), + if (albumSync.value) + SettingsButtonListTile( + icon: Icons.photo_album_outlined, + title: 'sync_albums'.tr(), + subtitle: Text( + "sync_albums_manual_subtitle".tr(), + ), + buttonText: 'sync_albums'.tr(), + child: isAlbumSyncInProgress.value + ? CircularProgressIndicator.adaptive( + strokeWidth: 2, + ) + : ElevatedButton( + onPressed: syncAlbums, + child: Text('sync'.tr()), + ), + ), ]; return SettingsSubPageScaffold( diff --git a/mobile/lib/widgets/settings/settings_button_list_tile.dart b/mobile/lib/widgets/settings/settings_button_list_tile.dart index 196e3d170feaf..c8bd8e4b588c9 100644 --- a/mobile/lib/widgets/settings/settings_button_list_tile.dart +++ b/mobile/lib/widgets/settings/settings_button_list_tile.dart @@ -9,6 +9,7 @@ class SettingsButtonListTile extends StatelessWidget { final Widget? subtitle; final String? subtileText; final String buttonText; + final Widget? child; final void Function()? onButtonTap; const SettingsButtonListTile({ @@ -18,6 +19,7 @@ class SettingsButtonListTile extends StatelessWidget { this.subtileText, this.subtitle, required this.buttonText, + this.child, this.onButtonTap, super.key, }); @@ -48,7 +50,8 @@ class SettingsButtonListTile extends StatelessWidget { ), if (subtitle != null) subtitle!, const SizedBox(height: 6), - ElevatedButton(onPressed: onButtonTap, child: Text(buttonText)), + child ?? + ElevatedButton(onPressed: onButtonTap, child: Text(buttonText)), ], ), ); From 7da1f786ad089c32cdc54c1981d91de715bc0c0a Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 25 Aug 2024 12:26:39 -0500 Subject: [PATCH 26/34] lint --- mobile/lib/pages/backup/backup_album_selection.page.dart | 2 +- mobile/lib/providers/backup/backup.provider.dart | 4 ++-- mobile/lib/services/album.service.dart | 5 +---- mobile/lib/services/asset.service.dart | 1 - mobile/lib/services/backup.service.dart | 5 ----- .../widgets/settings/backup_settings/backup_settings.dart | 5 ++--- 6 files changed, 6 insertions(+), 16 deletions(-) diff --git a/mobile/lib/pages/backup/backup_album_selection.page.dart b/mobile/lib/pages/backup/backup_album_selection.page.dart index 080beaca6148f..7328c8ce79a31 100644 --- a/mobile/lib/pages/backup/backup_album_selection.page.dart +++ b/mobile/lib/pages/backup/backup_album_selection.page.dart @@ -174,7 +174,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { valueNotifier: enableSyncUploadAlbum, title: "sync_albums".tr(), subtitle: "sync_upload_album_setting_subtitle".tr(), - contentPadding: EdgeInsets.symmetric(horizontal: 16), + contentPadding: const EdgeInsets.symmetric(horizontal: 16), titleStyle: context.textTheme.bodyLarge?.copyWith( fontWeight: FontWeight.bold, ), diff --git a/mobile/lib/providers/backup/backup.provider.dart b/mobile/lib/providers/backup/backup.provider.dart index 5ee0366364d2f..02f1f07904f97 100644 --- a/mobile/lib/providers/backup/backup.provider.dart +++ b/mobile/lib/providers/backup/backup.provider.dart @@ -342,11 +342,11 @@ class BackupNotifier extends StateNotifier { end: assetCount, ); - assets.forEach((asset) { + for (final asset in assets) { assetsFromExcludedAlbums.add( BackupCandidate(asset: asset, albumNames: [album.name]), ); - }); + } } final Set allUniqueAssets = diff --git a/mobile/lib/services/album.service.dart b/mobile/lib/services/album.service.dart index 8f6f22363e8a9..5f0318165e37b 100644 --- a/mobile/lib/services/album.service.dart +++ b/mobile/lib/services/album.service.dart @@ -464,10 +464,7 @@ class AlbumService { ) async { for (final albumName in albumNames) { Album? album = await getAlbumByName(albumName, true); - - if (album == null) { - album = await createAlbum(albumName, []); - } + album ??= await createAlbum(albumName, []); if (album != null && album.remoteId != null) { await _apiService.albumsApi.addAssetsToAlbum( diff --git a/mobile/lib/services/asset.service.dart b/mobile/lib/services/asset.service.dart index d14fc289bab6b..1936c7ff2ac7a 100644 --- a/mobile/lib/services/asset.service.dart +++ b/mobile/lib/services/asset.service.dart @@ -348,7 +348,6 @@ class AssetService { await _albumService.syncUploadAlbums([albumName], assetIds); } } catch (error, stack) { - print(error); log.severe("Error while syncing uploaded asset to albums", error, stack); } } diff --git a/mobile/lib/services/backup.service.dart b/mobile/lib/services/backup.service.dart index 885b79eed0f5c..9e170fd3b5766 100644 --- a/mobile/lib/services/backup.service.dart +++ b/mobile/lib/services/backup.service.dart @@ -223,15 +223,10 @@ class BackupService { } } - print( - "[AAAA] before remove existing: ${candidates.length} - Existing: ${existing.length}", - ); if (existing.isNotEmpty) { candidates.removeWhere((c) => existing.contains(c.asset.id)); } - print("[AAAA] after remove existing: ${candidates.length}"); - return candidates; } diff --git a/mobile/lib/widgets/settings/backup_settings/backup_settings.dart b/mobile/lib/widgets/settings/backup_settings/backup_settings.dart index 3c4a0b693761b..c093e8f1e3c98 100644 --- a/mobile/lib/widgets/settings/backup_settings/backup_settings.dart +++ b/mobile/lib/widgets/settings/backup_settings/backup_settings.dart @@ -4,7 +4,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/providers/asset_viewer/image_viewer_page_state.provider.dart'; import 'package:immich_mobile/providers/backup/backup_verification.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/services/asset.service.dart'; @@ -37,7 +36,7 @@ class BackupSettings extends HookConsumerWidget { await ref.read(assetServiceProvider).syncUploadedAssetToAlbums(); } catch (_) { } finally { - Future.delayed(Duration(seconds: 1), () { + Future.delayed(const Duration(seconds: 1), () { isAlbumSyncInProgress.value = false; }); } @@ -85,7 +84,7 @@ class BackupSettings extends HookConsumerWidget { ), buttonText: 'sync_albums'.tr(), child: isAlbumSyncInProgress.value - ? CircularProgressIndicator.adaptive( + ? const CircularProgressIndicator.adaptive( strokeWidth: 2, ) : ElevatedButton( From b5a799d365e5ed467a59c5a0ac2c71587774af08 Mon Sep 17 00:00:00 2001 From: Alex Tran Date: Sun, 25 Aug 2024 12:30:12 -0500 Subject: [PATCH 27/34] revert server changes --- open-api/immich-openapi-specs.json | 12 ------------ server/src/dtos/asset-media.dto.ts | 8 ++------ server/src/validation.ts | 2 -- 3 files changed, 2 insertions(+), 20 deletions(-) diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 1665637116556..2137bf7b11ff1 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -8131,12 +8131,6 @@ }, "AssetMediaCreateDto": { "properties": { - "albums": { - "items": { - "type": "string" - }, - "type": "array" - }, "assetData": { "format": "binary", "type": "string" @@ -8190,12 +8184,6 @@ }, "AssetMediaReplaceDto": { "properties": { - "albums": { - "items": { - "type": "string" - }, - "type": "array" - }, "assetData": { "format": "binary", "type": "string" diff --git a/server/src/dtos/asset-media.dto.ts b/server/src/dtos/asset-media.dto.ts index d1c36616d757a..e9e346c4cb593 100644 --- a/server/src/dtos/asset-media.dto.ts +++ b/server/src/dtos/asset-media.dto.ts @@ -1,7 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; -import { Transform, Type } from 'class-transformer'; +import { Type } from 'class-transformer'; import { ArrayNotEmpty, IsArray, IsEnum, IsNotEmpty, IsString, ValidateNested } from 'class-validator'; -import { Optional, toStringArray, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation'; +import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation'; export enum AssetMediaSize { PREVIEW = 'preview', @@ -40,10 +40,6 @@ class AssetMediaBase { @IsString() duration?: string; - @Optional() - @Transform(toStringArray) - albums?: string[]; - // The properties below are added to correctly generate the API docs // and client SDKs. Validation should be handled in the controller. @ApiProperty({ type: 'string', format: 'binary' }) diff --git a/server/src/validation.ts b/server/src/validation.ts index c1efc89f04a75..81b309d66358f 100644 --- a/server/src/validation.ts +++ b/server/src/validation.ts @@ -167,8 +167,6 @@ export function validateCronExpression(expression: string) { type IValue = { value: unknown }; -export const toStringArray = ({ value }: IValue) => (typeof value === 'string' ? value.split(',') : value); - export const toEmail = ({ value }: IValue) => (typeof value === 'string' ? value.toLowerCase() : value); export const toSanitized = ({ value }: IValue) => { From 4b0d316a435faa1e65340127afff02850f9eee7f Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 26 Aug 2024 08:45:46 -0500 Subject: [PATCH 28/34] pr feedback --- .../backup/backup_album_selection.page.dart | 38 ++++++++++++++++++- mobile/lib/services/album.service.dart | 13 +++---- mobile/lib/services/asset.service.dart | 3 +- .../lib/widgets/backup/album_info_card.dart | 14 ++++++- .../widgets/backup/album_info_list_tile.dart | 6 +-- 5 files changed, 61 insertions(+), 13 deletions(-) diff --git a/mobile/lib/pages/backup/backup_album_selection.page.dart b/mobile/lib/pages/backup/backup_album_selection.page.dart index 7328c8ce79a31..8dccece325d8f 100644 --- a/mobile/lib/pages/backup/backup_album_selection.page.dart +++ b/mobile/lib/pages/backup/backup_album_selection.page.dart @@ -8,6 +8,7 @@ import 'package:immich_mobile/providers/album/album.provider.dart'; import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; +import 'package:immich_mobile/widgets/backup/album_info_card.dart'; import 'package:immich_mobile/widgets/backup/album_info_list_tile.dart'; import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart'; import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; @@ -56,6 +57,33 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { ); } + buildAlbumSelectionGrid() { + if (albums.isEmpty) { + return const SliverToBoxAdapter( + child: Center( + child: ImmichLoadingIndicator(), + ), + ); + } + + return SliverPadding( + padding: const EdgeInsets.all(12.0), + sliver: SliverGrid.builder( + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 300, + mainAxisSpacing: 12, + crossAxisSpacing: 12, + ), + itemCount: albums.length, + itemBuilder: ((context, index) { + return AlbumInfoCard( + album: albums[index], + ); + }), + ), + ); + } + buildSelectedAlbumNameChip() { return selectedBackupAlbums.map((album) { void removeSelection() => @@ -254,7 +282,15 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { ], ), ), - buildAlbumSelectionList(), + SliverLayoutBuilder( + builder: (context, constraints) { + if (constraints.crossAxisExtent > 600) { + return buildAlbumSelectionGrid(); + } else { + return buildAlbumSelectionList(); + } + }, + ), ], ), ); diff --git a/mobile/lib/services/album.service.dart b/mobile/lib/services/album.service.dart index 5f0318165e37b..ef56f9bf6c12a 100644 --- a/mobile/lib/services/album.service.dart +++ b/mobile/lib/services/album.service.dart @@ -446,13 +446,12 @@ class AlbumService { } Future getAlbumByName(String name, bool remoteOnly) async { - final query = _db.albums.filter().nameEqualTo(name).sharedEqualTo(false); - - if (remoteOnly) { - return query.localIdIsNull().findFirst(); - } - - return query.findFirst(); + return _db.albums + .filter() + .optional(remoteOnly, (q) => q.localIdIsNull()) + .nameEqualTo(name) + .sharedEqualTo(false) + .findFirst(); } /// diff --git a/mobile/lib/services/asset.service.dart b/mobile/lib/services/asset.service.dart index 1936c7ff2ac7a..12fa70318f999 100644 --- a/mobile/lib/services/asset.service.dart +++ b/mobile/lib/services/asset.service.dart @@ -320,8 +320,9 @@ class AssetService { await refreshRemoteAssets(); final remoteAssets = await _db.assets - .filter() + .where() .localIdIsNotNull() + .filter() .remoteIdIsNotNull() .findAll(); diff --git a/mobile/lib/widgets/backup/album_info_card.dart b/mobile/lib/widgets/backup/album_info_card.dart index e9349bd69eccf..0c9cd2d89d33c 100644 --- a/mobile/lib/widgets/backup/album_info_card.dart +++ b/mobile/lib/widgets/backup/album_info_card.dart @@ -5,15 +5,21 @@ import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/models/backup/available_album.model.dart'; +import 'package:immich_mobile/providers/album/album.provider.dart'; +import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; +import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; class AlbumInfoCard extends HookConsumerWidget { final AvailableAlbum album; - const AlbumInfoCard({super.key, required this.album}); + const AlbumInfoCard({ + super.key, + required this.album, + }); @override Widget build(BuildContext context, WidgetRef ref) { @@ -21,6 +27,9 @@ class AlbumInfoCard extends HookConsumerWidget { ref.watch(backupProvider).selectedBackupAlbums.contains(album); final bool isExcluded = ref.watch(backupProvider).excludedBackupAlbums.contains(album); + final syncAlbum = ref + .watch(appSettingsServiceProvider) + .getSetting(AppSettingsEnum.syncAlbums); final isDarkTheme = context.isDarkTheme; @@ -85,6 +94,9 @@ class AlbumInfoCard extends HookConsumerWidget { ref.read(backupProvider.notifier).removeAlbumForBackup(album); } else { ref.read(backupProvider.notifier).addAlbumForBackup(album); + if (syncAlbum) { + ref.read(albumProvider.notifier).createSyncAlbum(album.name); + } } }, onDoubleTap: () { diff --git a/mobile/lib/widgets/backup/album_info_list_tile.dart b/mobile/lib/widgets/backup/album_info_list_tile.dart index 6f4c32cac3fdf..d326bad3e0fc7 100644 --- a/mobile/lib/widgets/backup/album_info_list_tile.dart +++ b/mobile/lib/widgets/backup/album_info_list_tile.dart @@ -24,8 +24,8 @@ class AlbumInfoListTile extends HookConsumerWidget { ref.watch(backupProvider).selectedBackupAlbums.contains(album); final bool isExcluded = ref.watch(backupProvider).excludedBackupAlbums.contains(album); - var assetCount = useState(0); - final shouldSyncUploadAlbum = ref + final assetCount = useState(0); + final syncAlbum = ref .watch(appSettingsServiceProvider) .getSetting(AppSettingsEnum.syncAlbums); @@ -104,7 +104,7 @@ class AlbumInfoListTile extends HookConsumerWidget { ref.read(backupProvider.notifier).removeAlbumForBackup(album); } else { ref.read(backupProvider.notifier).addAlbumForBackup(album); - if (shouldSyncUploadAlbum) { + if (syncAlbum) { ref.read(albumProvider.notifier).createSyncAlbum(album.name); } } From 72b59e35b1b0e18c0153c39a5f27e9fd25e978b3 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 26 Aug 2024 10:02:33 -0500 Subject: [PATCH 29/34] revert time filtering --- mobile/lib/services/backup.service.dart | 40 +++++++++++++++++++------ 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/mobile/lib/services/backup.service.dart b/mobile/lib/services/backup.service.dart index 9e170fd3b5766..3262467e263cc 100644 --- a/mobile/lib/services/backup.service.dart +++ b/mobile/lib/services/backup.service.dart @@ -92,11 +92,13 @@ class BackupService { imageOption: const FilterOption(needTitle: true), videoOption: const FilterOption(needTitle: true), ); + final now = DateTime.now(); final List selectedAlbums = await _loadAlbumsWithTimeFilter( selectedBackupAlbums, filter, + now, ); if (selectedAlbums.every((e) => e == null)) { @@ -107,16 +109,19 @@ class BackupService { await _loadAlbumsWithTimeFilter( excludedBackupAlbums, filter, + now, ); final Set toAdd = await _fetchAssetsAndUpdateLastBackup( selectedAlbums, selectedBackupAlbums, + now, ); final Set toRemove = await _fetchAssetsAndUpdateLastBackup( excludedAlbums, excludedBackupAlbums, + now, ); return toAdd.difference(toRemove); @@ -125,44 +130,59 @@ class BackupService { Future> _loadAlbumsWithTimeFilter( List albums, FilterOptionGroup filter, + DateTime now, ) async { List result = []; - for (BackupAlbum backupAlbum in albums) { try { final AssetPathEntity album = await AssetPathEntity.obtainPathFromProperties( id: backupAlbum.id, - optionGroup: filter, - maxDateTimeToNow: false, + optionGroup: filter.copyWith( + updateTimeCond: DateTimeCond( + // subtract 2 seconds to prevent missing assets due to rounding issues + min: backupAlbum.lastBackup.subtract(Duration(seconds: 2)), + max: now, + ), + ), + maxDateTimeToNow: true, ); + result.add(album); } on StateError { // either there are no assets matching the filter criteria OR the album no longer exists } } + return result; } Future> _fetchAssetsAndUpdateLastBackup( - List albums, + List localAlbums, List backupAlbums, + DateTime now, ) async { Set candidate = {}; - for (final album in albums) { - if (album == null) { + for (int i = 0; i < localAlbums.length; i++) { + final localAlbum = localAlbums[i]; + if (localAlbum == null) { continue; } - final assets = await album.getAssetListRange( + if (localAlbum.lastModified?.isBefore(backupAlbums[i].lastBackup) == + true) { + continue; + } + + final assets = await localAlbum.getAssetListRange( start: 0, - end: await album.assetCountAsync, + end: await localAlbum.assetCountAsync, ); // Add album's name to the asset info for (final asset in assets) { - List albumNames = [album.name]; + List albumNames = [localAlbum.name]; final existingAsset = candidate.firstWhereOrNull( (a) => a.asset.id == asset.id, @@ -180,6 +200,8 @@ class BackupService { ), ); } + + backupAlbums[i].lastBackup = now; } return candidate; From 8166c91e05e84c4422fef4382e8f21cf3dac19c7 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 26 Aug 2024 10:05:27 -0500 Subject: [PATCH 30/34] const --- mobile/lib/services/backup.service.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/lib/services/backup.service.dart b/mobile/lib/services/backup.service.dart index 3262467e263cc..ee27b395af1f7 100644 --- a/mobile/lib/services/backup.service.dart +++ b/mobile/lib/services/backup.service.dart @@ -141,7 +141,7 @@ class BackupService { optionGroup: filter.copyWith( updateTimeCond: DateTimeCond( // subtract 2 seconds to prevent missing assets due to rounding issues - min: backupAlbum.lastBackup.subtract(Duration(seconds: 2)), + min: backupAlbum.lastBackup.subtract(const Duration(seconds: 2)), max: now, ), ), From ce80459f295b1732cb5b4ebc3ba87ac1199fa9ea Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 26 Aug 2024 10:28:35 -0500 Subject: [PATCH 31/34] sync album on manual upload --- .../backup/manual_upload.provider.dart | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/mobile/lib/providers/backup/manual_upload.provider.dart b/mobile/lib/providers/backup/manual_upload.provider.dart index 007f80b4bf5fd..9239c256c0d41 100644 --- a/mobile/lib/providers/backup/manual_upload.provider.dart +++ b/mobile/lib/providers/backup/manual_upload.provider.dart @@ -8,6 +8,7 @@ import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; import 'package:immich_mobile/models/backup/success_upload_asset.model.dart'; +import 'package:immich_mobile/providers/trash.provider.dart'; import 'package:immich_mobile/services/background.service.dart'; import 'package:immich_mobile/models/backup/backup_state.model.dart'; import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; @@ -24,6 +25,7 @@ import 'package:immich_mobile/providers/app_life_cycle.provider.dart'; import 'package:immich_mobile/services/local_notification.service.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; import 'package:immich_mobile/utils/backup_progress.dart'; +import 'package:isar/isar.dart'; import 'package:logging/logging.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:photo_manager/photo_manager.dart'; @@ -33,6 +35,7 @@ final manualUploadProvider = return ManualUploadNotifier( ref.watch(localNotificationService), ref.watch(backupProvider.notifier), + ref.watch(backupServiceProvider), ref, ); }); @@ -41,11 +44,13 @@ class ManualUploadNotifier extends StateNotifier { final Logger _log = Logger("ManualUploadNotifier"); final LocalNotificationService _localNotificationService; final BackupNotifier _backupProvider; + final BackupService _backupService; final Ref ref; ManualUploadNotifier( this._localNotificationService, this._backupProvider, + this._backupService, this.ref, ) : super( ManualUploadState( @@ -207,11 +212,23 @@ class ManualUploadNotifier extends StateNotifier { ); } - Set allUploadAssets = allAssetsFromDevice.nonNulls - .map((asset) => BackupCandidate(asset: asset, albumNames: [])) - .toSet(); + final selectedBackupAlbums = + _backupService.selectedAlbumsQuery().findAllSync(); + final excludedBackupAlbums = + _backupService.excludedAlbumsQuery().findAllSync(); - if (allUploadAssets.isEmpty) { + // Get candidates from selected albums and excluded albums + Set candidates = + await _backupService.buildUploadCandidates( + selectedBackupAlbums, + excludedBackupAlbums, + ); + + // Extrack candidate from allAssetsFromDevice.nonNulls + final uploadAssets = candidates + .where((e) => allAssetsFromDevice.nonNulls.contains(e.asset)); + + if (uploadAssets.isEmpty) { debugPrint("[_startUpload] No Assets to upload - Abort Process"); _backupProvider.updateBackupProgress(BackUpProgressEnum.idle); return false; @@ -221,7 +238,7 @@ class ManualUploadNotifier extends StateNotifier { progressInPercentage: 0, progressInFileSize: "0 B / 0 B", progressInFileSpeed: 0, - totalAssetsToUpload: allUploadAssets.length, + totalAssetsToUpload: uploadAssets.length, successfulUploads: 0, currentAssetIndex: 0, currentUploadAsset: CurrentUploadAsset( @@ -250,7 +267,7 @@ class ManualUploadNotifier extends StateNotifier { final pmProgressHandler = Platform.isIOS ? PMProgressHandler() : null; final bool ok = await ref.read(backupServiceProvider).backupAsset( - allUploadAssets, + uploadAssets, state.cancelToken, pmProgressHandler: pmProgressHandler, onSuccess: _onAssetUploaded, From 77ad569adbd9412af43cc205de224a80591694c1 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 26 Aug 2024 10:31:06 -0500 Subject: [PATCH 32/34] linting --- mobile/lib/providers/backup/manual_upload.provider.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/mobile/lib/providers/backup/manual_upload.provider.dart b/mobile/lib/providers/backup/manual_upload.provider.dart index 9239c256c0d41..a76b56fea7f8a 100644 --- a/mobile/lib/providers/backup/manual_upload.provider.dart +++ b/mobile/lib/providers/backup/manual_upload.provider.dart @@ -8,7 +8,6 @@ import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; import 'package:immich_mobile/models/backup/success_upload_asset.model.dart'; -import 'package:immich_mobile/providers/trash.provider.dart'; import 'package:immich_mobile/services/background.service.dart'; import 'package:immich_mobile/models/backup/backup_state.model.dart'; import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; From e7220034159c97a9f00e6e85ba40f3c9667811aa Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 26 Aug 2024 12:46:41 -0500 Subject: [PATCH 33/34] pr feedback and proper time filtering --- mobile/lib/services/asset.service.dart | 7 ++-- mobile/lib/services/backup.service.dart | 45 ++++++++++++++++--------- 2 files changed, 34 insertions(+), 18 deletions(-) diff --git a/mobile/lib/services/asset.service.dart b/mobile/lib/services/asset.service.dart index 12fa70318f999..17508cba5153e 100644 --- a/mobile/lib/services/asset.service.dart +++ b/mobile/lib/services/asset.service.dart @@ -298,12 +298,15 @@ class AssetService { Future syncUploadedAssetToAlbums() async { try { - final selectedAlbums = _backupService.selectedAlbumsQuery().findAllSync(); - final excludedAlbums = _backupService.excludedAlbumsQuery().findAllSync(); + final [selectedAlbums, excludedAlbums] = await Future.wait([ + _backupService.selectedAlbumsQuery().findAll(), + _backupService.excludedAlbumsQuery().findAll(), + ]); final candidates = await _backupService.buildUploadCandidates( selectedAlbums, excludedAlbums, + useTimeFilter: false, ); final duplicates = await _apiService.assetsApi.checkExistingAssets( diff --git a/mobile/lib/services/backup.service.dart b/mobile/lib/services/backup.service.dart index ee27b395af1f7..eb0928c837b99 100644 --- a/mobile/lib/services/backup.service.dart +++ b/mobile/lib/services/backup.service.dart @@ -83,8 +83,9 @@ class BackupService { /// if `ignoreTimeFilter` is set to true, all assets will be returned Future> buildUploadCandidates( List selectedBackupAlbums, - List excludedBackupAlbums, - ) async { + List excludedBackupAlbums, { + bool useTimeFilter = true, + }) async { final filter = FilterOptionGroup( containsPathModified: true, orders: [const OrderOption(type: OrderOptionType.updateDate)], @@ -99,6 +100,7 @@ class BackupService { selectedBackupAlbums, filter, now, + useTimeFilter: useTimeFilter, ); if (selectedAlbums.every((e) => e == null)) { @@ -110,18 +112,21 @@ class BackupService { excludedBackupAlbums, filter, now, + useTimeFilter: useTimeFilter, ); final Set toAdd = await _fetchAssetsAndUpdateLastBackup( selectedAlbums, selectedBackupAlbums, now, + useTimeFilter: useTimeFilter, ); final Set toRemove = await _fetchAssetsAndUpdateLastBackup( excludedAlbums, excludedBackupAlbums, now, + useTimeFilter: useTimeFilter, ); return toAdd.difference(toRemove); @@ -130,22 +135,28 @@ class BackupService { Future> _loadAlbumsWithTimeFilter( List albums, FilterOptionGroup filter, - DateTime now, - ) async { + DateTime now, { + bool useTimeFilter = true, + }) async { List result = []; for (BackupAlbum backupAlbum in albums) { try { + final optionGroup = useTimeFilter + ? filter.copyWith( + updateTimeCond: DateTimeCond( + // subtract 2 seconds to prevent missing assets due to rounding issues + min: backupAlbum.lastBackup + .subtract(const Duration(seconds: 2)), + max: now, + ), + ) + : filter; + final AssetPathEntity album = await AssetPathEntity.obtainPathFromProperties( id: backupAlbum.id, - optionGroup: filter.copyWith( - updateTimeCond: DateTimeCond( - // subtract 2 seconds to prevent missing assets due to rounding issues - min: backupAlbum.lastBackup.subtract(const Duration(seconds: 2)), - max: now, - ), - ), - maxDateTimeToNow: true, + optionGroup: optionGroup, + maxDateTimeToNow: false, ); result.add(album); @@ -160,8 +171,9 @@ class BackupService { Future> _fetchAssetsAndUpdateLastBackup( List localAlbums, List backupAlbums, - DateTime now, - ) async { + DateTime now, { + bool useTimeFilter = true, + }) async { Set candidate = {}; for (int i = 0; i < localAlbums.length; i++) { @@ -170,8 +182,9 @@ class BackupService { continue; } - if (localAlbum.lastModified?.isBefore(backupAlbums[i].lastBackup) == - true) { + if (useTimeFilter && + localAlbum.lastModified?.isBefore(backupAlbums[i].lastBackup) == + true) { continue; } From 45cdec87ff1bd82180f658b58f69808acd876ae7 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 26 Aug 2024 12:51:29 -0500 Subject: [PATCH 34/34] wording --- mobile/lib/services/backup.service.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/lib/services/backup.service.dart b/mobile/lib/services/backup.service.dart index eb0928c837b99..12edd14d609ca 100644 --- a/mobile/lib/services/backup.service.dart +++ b/mobile/lib/services/backup.service.dart @@ -80,7 +80,7 @@ class BackupService { _db.backupAlbums.filter().selectionEqualTo(BackupSelection.exclude); /// Returns all assets newer than the last successful backup per album - /// if `ignoreTimeFilter` is set to true, all assets will be returned + /// if `useTimeFilter` is set to true, all assets will be returned Future> buildUploadCandidates( List selectedBackupAlbums, List excludedBackupAlbums, {