diff --git a/.github/workflows/static_analysis.yml b/.github/workflows/static_analysis.yml index 191cb2e1f8..4f1a200261 100644 --- a/.github/workflows/static_analysis.yml +++ b/.github/workflows/static_analysis.yml @@ -65,7 +65,7 @@ jobs: - name: Run Build Runner run: make build working-directory: ./mobile - + - name: Generate platform API run: make pigeon; dart format ib/platform/native_sync_api.g.dart working-directory: ./mobile diff --git a/mobile/lib/domain/services/device_sync.service.dart b/mobile/lib/domain/services/device_sync.service.dart index e614b1b88d..38e2bfbe03 100644 --- a/mobile/lib/domain/services/device_sync.service.dart +++ b/mobile/lib/domain/services/device_sync.service.dart @@ -6,6 +6,8 @@ import 'package:immich_mobile/domain/interfaces/album_media.interface.dart'; import 'package:immich_mobile/domain/interfaces/local_album.interface.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/local_album.model.dart'; +import 'package:immich_mobile/domain/models/store.model.dart'; +import 'package:immich_mobile/domain/services/store.service.dart'; import 'package:immich_mobile/platform/native_sync_api.g.dart'; import 'package:immich_mobile/utils/diff.dart'; import 'package:logging/logging.dart'; @@ -14,19 +16,25 @@ import 'package:platform/platform.dart'; class DeviceSyncService { final IAlbumMediaRepository _albumMediaRepository; final ILocalAlbumRepository _localAlbumRepository; - final Platform _platform; final NativeSyncApi _nativeSyncApi; + final Platform _platform; + final StoreService _storeService; final Logger _log = Logger("DeviceSyncService"); DeviceSyncService({ required IAlbumMediaRepository albumMediaRepository, required ILocalAlbumRepository localAlbumRepository, required NativeSyncApi nativeSyncApi, + required StoreService storeService, Platform? platform, }) : _albumMediaRepository = albumMediaRepository, _localAlbumRepository = localAlbumRepository, - _platform = platform ?? const LocalPlatform(), - _nativeSyncApi = nativeSyncApi; + _nativeSyncApi = nativeSyncApi, + _storeService = storeService, + _platform = platform ?? const LocalPlatform(); + + bool get _ignoreIcloudAssets => + _storeService.get(StoreKey.ignoreIcloudAssets, false) == true; Future sync() async { final Stopwatch stopwatch = Stopwatch()..start(); @@ -46,14 +54,38 @@ class DeviceSyncService { await _localAlbumRepository.updateAll(deviceAlbums.toLocalAlbums()); await _localAlbumRepository.processDelta(delta); + final dbAlbums = await _localAlbumRepository.getAll(); + // On Android, we need to sync all albums since it is not possible to + // detect album deletions from the native side if (_platform.isAndroid) { - final dbAlbums = await _localAlbumRepository.getAll(); for (final album in dbAlbums) { final deviceIds = await _nativeSyncApi.getAssetIdsForAlbum(album.id); await _localAlbumRepository.syncAlbumDeletes(album.id, deviceIds); } } + if (_platform.isIOS) { + // On iOS, we need to full sync albums that are marked as cloud as the delta sync + // does not include changes for cloud albums. If ignoreIcloudAssets is enabled, + // remove the albums from the local database from the previous sync + final cloudAlbums = + deviceAlbums.where((a) => a.isCloud).toLocalAlbums(); + for (final album in cloudAlbums) { + final dbAlbum = dbAlbums.firstWhereOrNull((a) => a.id == album.id); + if (dbAlbum == null) { + _log.warning( + "Cloud album ${album.name} not found in local database. Skipping sync.", + ); + continue; + } + if (_ignoreIcloudAssets) { + await removeAlbum(dbAlbum); + } else { + await updateAlbum(dbAlbum, album); + } + } + } + await _nativeSyncApi.checkpointSync(); } catch (e, s) { _log.severe("Error performing device sync", e, s); @@ -67,14 +99,17 @@ class DeviceSyncService { try { final Stopwatch stopwatch = Stopwatch()..start(); - final deviceAlbums = (await _nativeSyncApi.getAlbums()).toLocalAlbums(); + List deviceAlbums = List.of(await _nativeSyncApi.getAlbums()); + if (_platform.isIOS && _ignoreIcloudAssets) { + deviceAlbums.removeWhere((album) => album.isCloud); + } final dbAlbums = await _localAlbumRepository.getAll(sortBy: SortLocalAlbumsBy.id); await diffSortedLists( dbAlbums, - deviceAlbums, + deviceAlbums.toLocalAlbums(), compare: (a, b) => a.id.compareTo(b.id), both: updateAlbum, onlyFirst: removeAlbum, diff --git a/mobile/lib/providers/infrastructure/sync.provider.dart b/mobile/lib/providers/infrastructure/sync.provider.dart index 1c8e586d31..54a53823c1 100644 --- a/mobile/lib/providers/infrastructure/sync.provider.dart +++ b/mobile/lib/providers/infrastructure/sync.provider.dart @@ -8,6 +8,7 @@ import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/cancel.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/store.provider.dart'; final syncStreamServiceProvider = Provider( (ref) => SyncStreamService( @@ -30,5 +31,6 @@ final deviceSyncServiceProvider = Provider( albumMediaRepository: ref.watch(albumMediaRepositoryProvider), localAlbumRepository: ref.watch(localAlbumRepository), nativeSyncApi: ref.watch(nativeSyncApiProvider), + storeService: ref.watch(storeServiceProvider), ), );