From b7a645637e992ab2c40ee8051851a58e95163b1b Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 2 Jul 2024 13:56:05 -0500 Subject: [PATCH] Revert "feat(mobile): render assets on device by default (#10470)" This reverts commit 32da9d90e4b1165b78b64af09cbe6455e8d02fa6. --- mobile/lib/services/album.service.dart | 73 +++++++++++++++++++++++++- mobile/lib/services/hash.service.dart | 6 ++- mobile/lib/services/sync.service.dart | 32 +++++++---- 3 files changed, 97 insertions(+), 14 deletions(-) diff --git a/mobile/lib/services/album.service.dart b/mobile/lib/services/album.service.dart index b367318553d84..c2494680c7da5 100644 --- a/mobile/lib/services/album.service.dart +++ b/mobile/lib/services/album.service.dart @@ -1,8 +1,13 @@ import 'dart:async'; +import 'dart:collection'; +import 'dart:io'; +import 'package:collection/collection.dart'; 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'; @@ -23,6 +28,7 @@ final albumServiceProvider = Provider( ref.watch(userServiceProvider), ref.watch(syncServiceProvider), ref.watch(dbProvider), + ref.watch(backupServiceProvider), ), ); @@ -31,6 +37,7 @@ 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); @@ -40,6 +47,7 @@ class AlbumService { this._userService, this._syncService, this._db, + this._backupService, ); /// Checks all selected device albums for changes of albums and their assets @@ -54,14 +62,60 @@ class AlbumService { final Stopwatch sw = Stopwatch()..start(); bool changes = false; try { + final List excludedIds = + await _backupService.excludedAlbumsQuery().idProperty().findAll(); + final List selectedIds = + await _backupService.selectedAlbumsQuery().idProperty().findAll(); + if (selectedIds.isEmpty) { + final numLocal = await _db.albums.where().localIdIsNotNull().count(); + if (numLocal > 0) { + _syncService.removeAllLocalAlbumsAndAssets(); + } + return false; + } final List onDevice = await PhotoManager.getAssetPathList( hasAll: true, filterOption: FilterOptionGroup(containsPathModified: true), ); _log.info("Found ${onDevice.length} device albums"); - - changes = await _syncService.syncLocalAlbumAssetsToDb(onDevice); + Set? excludedAssets; + if (excludedIds.isNotEmpty) { + if (Platform.isIOS) { + // iOS and Android device album working principle differ significantly + // on iOS, an asset can be in multiple albums + // on Android, an asset can only be in exactly one album (folder!) at the same time + // thus, on Android, excluding an album can be done by ignoring that album + // however, on iOS, it it necessary to load the assets from all excluded + // albums and check every asset from any selected album against the set + // of excluded assets + excludedAssets = await _loadExcludedAssetIds(onDevice, excludedIds); + _log.info("Found ${excludedAssets.length} assets to exclude"); + } + // remove all excluded albums + onDevice.removeWhere((e) => excludedIds.contains(e.id)); + _log.info( + "Ignoring ${excludedIds.length} excluded albums resulting in ${onDevice.length} device albums", + ); + } + final hasAll = selectedIds + .map((id) => onDevice.firstWhereOrNull((a) => a.id == id)) + .whereNotNull() + .any((a) => a.isAll); + if (hasAll) { + if (Platform.isAndroid) { + // remove the virtual "Recent" album and keep and individual albums + // on Android, the virtual "Recent" `lastModified` value is always null + onDevice.removeWhere((e) => e.isAll); + _log.info("'Recents' is selected, keeping all individual albums"); + } + } else { + // keep only the explicitly selected albums + onDevice.removeWhere((e) => !selectedIds.contains(e.id)); + _log.info("'Recents' is not selected, keeping only selected albums"); + } + changes = + await _syncService.syncLocalAlbumAssetsToDb(onDevice, excludedAssets); _log.info("Syncing completed. Changes: $changes"); } finally { _localCompleter.complete(changes); @@ -70,6 +124,21 @@ class AlbumService { return changes; } + Future> _loadExcludedAssetIds( + List albums, + List excludedAlbumIds, + ) async { + final Set result = HashSet(); + for (AssetPathEntity a in albums) { + if (excludedAlbumIds.contains(a.id)) { + final List assets = + await a.getAssetListRange(start: 0, end: 0x7fffffffffffffff); + result.addAll(assets.map((e) => e.id)); + } + } + return result; + } + /// Checks remote albums (owned if `isShared` is false) for changes, /// updates the local database and returns `true` if there were any changes Future refreshRemoteAlbums({required bool isShared}) async { diff --git a/mobile/lib/services/hash.service.dart b/mobile/lib/services/hash.service.dart index 3071a89c0039d..ffc81a3445acf 100644 --- a/mobile/lib/services/hash.service.dart +++ b/mobile/lib/services/hash.service.dart @@ -24,9 +24,13 @@ class HashService { AssetPathEntity album, { int start = 0, int end = 0x7fffffffffffffff, + Set? excludedAssets, }) async { final entities = await album.getAssetListRange(start: start, end: end); - return _hashAssets(entities); + final filtered = excludedAssets == null + ? entities + : entities.where((e) => !excludedAssets.contains(e.id)).toList(); + return _hashAssets(filtered); } /// Converts a list of [AssetEntity]s to [Asset]s including only those diff --git a/mobile/lib/services/sync.service.dart b/mobile/lib/services/sync.service.dart index 2120dda589150..8ec56e925f7f8 100644 --- a/mobile/lib/services/sync.service.dart +++ b/mobile/lib/services/sync.service.dart @@ -68,9 +68,10 @@ class SyncService { /// Syncs all device albums and their assets to the database /// Returns `true` if there were any changes Future syncLocalAlbumAssetsToDb( - List onDevice, - ) => - _lock.run(() => _syncLocalAlbumAssetsToDb(onDevice)); + List onDevice, [ + Set? excludedAssets, + ]) => + _lock.run(() => _syncLocalAlbumAssetsToDb(onDevice, excludedAssets)); /// returns all Asset IDs that are not contained in the existing list List sharedAssetsToRemove( @@ -491,8 +492,9 @@ class SyncService { /// Syncs all device albums and their assets to the database /// Returns `true` if there were any changes Future _syncLocalAlbumAssetsToDb( - List onDevice, - ) async { + List onDevice, [ + Set? excludedAssets, + ]) async { onDevice.sort((a, b) => a.id.compareTo(b.id)); final inDb = await _db.albums.where().localIdIsNotNull().sortByLocalId().findAll(); @@ -508,8 +510,10 @@ class SyncService { album, deleteCandidates, existing, + excludedAssets, ), - onlyFirst: (AssetPathEntity ape) => _addAlbumFromDevice(ape, existing), + onlyFirst: (AssetPathEntity ape) => + _addAlbumFromDevice(ape, existing, excludedAssets), onlySecond: (Album a) => _removeAlbumFromDb(a, deleteCandidates), ); _log.fine( @@ -541,13 +545,16 @@ class SyncService { Album album, List deleteCandidates, List existing, [ + Set? excludedAssets, bool forceRefresh = false, ]) async { if (!forceRefresh && !await _hasAssetPathEntityChanged(ape, album)) { _log.fine("Local album ${ape.name} has not changed. Skipping sync."); return false; } - if (!forceRefresh && await _syncDeviceAlbumFast(ape, album)) { + if (!forceRefresh && + excludedAssets == null && + await _syncDeviceAlbumFast(ape, album)) { return true; } @@ -559,7 +566,8 @@ class SyncService { .findAll(); assert(inDb.isSorted(Asset.compareByChecksum), "inDb not sorted!"); final int assetCountOnDevice = await ape.assetCountAsync; - final List onDevice = await _hashService.getHashedAssets(ape); + final List onDevice = + await _hashService.getHashedAssets(ape, excludedAssets: excludedAssets); _removeDuplicates(onDevice); // _removeDuplicates sorts `onDevice` by checksum final (toAdd, toUpdate, toDelete) = _diffAssets(onDevice, inDb); @@ -670,11 +678,13 @@ class SyncService { /// assets already existing in the database to the list of `existing` assets Future _addAlbumFromDevice( AssetPathEntity ape, - List existing, - ) async { + List existing, [ + Set? excludedAssets, + ]) async { _log.info("Syncing a new local album to DB: ${ape.name}"); final Album a = Album.local(ape); - final assets = await _hashService.getHashedAssets(ape); + final assets = + await _hashService.getHashedAssets(ape, excludedAssets: excludedAssets); _removeDuplicates(assets); final (existingInDb, updated) = await _linkWithExistingFromDb(assets); _log.info(