mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-31 10:49:11 -04:00 
			
		
		
		
	feat(mobile): render assets on device by default (#10470)
* feat(mobile): render asset on device by default * remove unused service
This commit is contained in:
		
							parent
							
								
									6164640575
								
							
						
					
					
						commit
						32da9d90e4
					
				| @ -1,13 +1,8 @@ | |||||||
| import 'dart:async'; | import 'dart:async'; | ||||||
| import 'dart:collection'; |  | ||||||
| import 'dart:io'; |  | ||||||
| 
 | 
 | ||||||
| import 'package:collection/collection.dart'; |  | ||||||
| import 'package:flutter/foundation.dart'; | import 'package:flutter/foundation.dart'; | ||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:immich_mobile/models/albums/album_add_asset_response.model.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/album.entity.dart'; | ||||||
| import 'package:immich_mobile/entities/asset.entity.dart'; | import 'package:immich_mobile/entities/asset.entity.dart'; | ||||||
| import 'package:immich_mobile/entities/store.entity.dart'; | import 'package:immich_mobile/entities/store.entity.dart'; | ||||||
| @ -28,7 +23,6 @@ final albumServiceProvider = Provider( | |||||||
|     ref.watch(userServiceProvider), |     ref.watch(userServiceProvider), | ||||||
|     ref.watch(syncServiceProvider), |     ref.watch(syncServiceProvider), | ||||||
|     ref.watch(dbProvider), |     ref.watch(dbProvider), | ||||||
|     ref.watch(backupServiceProvider), |  | ||||||
|   ), |   ), | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| @ -37,7 +31,6 @@ class AlbumService { | |||||||
|   final UserService _userService; |   final UserService _userService; | ||||||
|   final SyncService _syncService; |   final SyncService _syncService; | ||||||
|   final Isar _db; |   final Isar _db; | ||||||
|   final BackupService _backupService; |  | ||||||
|   final Logger _log = Logger('AlbumService'); |   final Logger _log = Logger('AlbumService'); | ||||||
|   Completer<bool> _localCompleter = Completer()..complete(false); |   Completer<bool> _localCompleter = Completer()..complete(false); | ||||||
|   Completer<bool> _remoteCompleter = Completer()..complete(false); |   Completer<bool> _remoteCompleter = Completer()..complete(false); | ||||||
| @ -47,7 +40,6 @@ class AlbumService { | |||||||
|     this._userService, |     this._userService, | ||||||
|     this._syncService, |     this._syncService, | ||||||
|     this._db, |     this._db, | ||||||
|     this._backupService, |  | ||||||
|   ); |   ); | ||||||
| 
 | 
 | ||||||
|   /// Checks all selected device albums for changes of albums and their assets |   /// Checks all selected device albums for changes of albums and their assets | ||||||
| @ -62,60 +54,14 @@ class AlbumService { | |||||||
|     final Stopwatch sw = Stopwatch()..start(); |     final Stopwatch sw = Stopwatch()..start(); | ||||||
|     bool changes = false; |     bool changes = false; | ||||||
|     try { |     try { | ||||||
|       final List<String> excludedIds = |  | ||||||
|           await _backupService.excludedAlbumsQuery().idProperty().findAll(); |  | ||||||
|       final List<String> 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<AssetPathEntity> onDevice = |       final List<AssetPathEntity> onDevice = | ||||||
|           await PhotoManager.getAssetPathList( |           await PhotoManager.getAssetPathList( | ||||||
|         hasAll: true, |         hasAll: true, | ||||||
|         filterOption: FilterOptionGroup(containsPathModified: true), |         filterOption: FilterOptionGroup(containsPathModified: true), | ||||||
|       ); |       ); | ||||||
|       _log.info("Found ${onDevice.length} device albums"); |       _log.info("Found ${onDevice.length} device albums"); | ||||||
|       Set<String>? excludedAssets; | 
 | ||||||
|       if (excludedIds.isNotEmpty) { |       changes = await _syncService.syncLocalAlbumAssetsToDb(onDevice); | ||||||
|         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"); |       _log.info("Syncing completed. Changes: $changes"); | ||||||
|     } finally { |     } finally { | ||||||
|       _localCompleter.complete(changes); |       _localCompleter.complete(changes); | ||||||
| @ -124,21 +70,6 @@ class AlbumService { | |||||||
|     return changes; |     return changes; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   Future<Set<String>> _loadExcludedAssetIds( |  | ||||||
|     List<AssetPathEntity> albums, |  | ||||||
|     List<String> excludedAlbumIds, |  | ||||||
|   ) async { |  | ||||||
|     final Set<String> result = HashSet<String>(); |  | ||||||
|     for (AssetPathEntity a in albums) { |  | ||||||
|       if (excludedAlbumIds.contains(a.id)) { |  | ||||||
|         final List<AssetEntity> 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, |   /// Checks remote albums (owned if `isShared` is false) for changes, | ||||||
|   /// updates the local database and returns `true` if there were any changes |   /// updates the local database and returns `true` if there were any changes | ||||||
|   Future<bool> refreshRemoteAlbums({required bool isShared}) async { |   Future<bool> refreshRemoteAlbums({required bool isShared}) async { | ||||||
|  | |||||||
| @ -24,13 +24,9 @@ class HashService { | |||||||
|     AssetPathEntity album, { |     AssetPathEntity album, { | ||||||
|     int start = 0, |     int start = 0, | ||||||
|     int end = 0x7fffffffffffffff, |     int end = 0x7fffffffffffffff, | ||||||
|     Set<String>? excludedAssets, |  | ||||||
|   }) async { |   }) async { | ||||||
|     final entities = await album.getAssetListRange(start: start, end: end); |     final entities = await album.getAssetListRange(start: start, end: end); | ||||||
|     final filtered = excludedAssets == null |     return _hashAssets(entities); | ||||||
|         ? entities |  | ||||||
|         : entities.where((e) => !excludedAssets.contains(e.id)).toList(); |  | ||||||
|     return _hashAssets(filtered); |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   /// Converts a list of [AssetEntity]s to [Asset]s including only those |   /// Converts a list of [AssetEntity]s to [Asset]s including only those | ||||||
|  | |||||||
| @ -68,10 +68,9 @@ class SyncService { | |||||||
|   /// Syncs all device albums and their assets to the database |   /// Syncs all device albums and their assets to the database | ||||||
|   /// Returns `true` if there were any changes |   /// Returns `true` if there were any changes | ||||||
|   Future<bool> syncLocalAlbumAssetsToDb( |   Future<bool> syncLocalAlbumAssetsToDb( | ||||||
|     List<AssetPathEntity> onDevice, [ |     List<AssetPathEntity> onDevice, | ||||||
|     Set<String>? excludedAssets, |   ) => | ||||||
|   ]) => |       _lock.run(() => _syncLocalAlbumAssetsToDb(onDevice)); | ||||||
|       _lock.run(() => _syncLocalAlbumAssetsToDb(onDevice, excludedAssets)); |  | ||||||
| 
 | 
 | ||||||
|   /// returns all Asset IDs that are not contained in the existing list |   /// returns all Asset IDs that are not contained in the existing list | ||||||
|   List<int> sharedAssetsToRemove( |   List<int> sharedAssetsToRemove( | ||||||
| @ -492,9 +491,8 @@ class SyncService { | |||||||
|   /// Syncs all device albums and their assets to the database |   /// Syncs all device albums and their assets to the database | ||||||
|   /// Returns `true` if there were any changes |   /// Returns `true` if there were any changes | ||||||
|   Future<bool> _syncLocalAlbumAssetsToDb( |   Future<bool> _syncLocalAlbumAssetsToDb( | ||||||
|     List<AssetPathEntity> onDevice, [ |     List<AssetPathEntity> onDevice, | ||||||
|     Set<String>? excludedAssets, |   ) async { | ||||||
|   ]) async { |  | ||||||
|     onDevice.sort((a, b) => a.id.compareTo(b.id)); |     onDevice.sort((a, b) => a.id.compareTo(b.id)); | ||||||
|     final inDb = |     final inDb = | ||||||
|         await _db.albums.where().localIdIsNotNull().sortByLocalId().findAll(); |         await _db.albums.where().localIdIsNotNull().sortByLocalId().findAll(); | ||||||
| @ -510,10 +508,8 @@ class SyncService { | |||||||
|         album, |         album, | ||||||
|         deleteCandidates, |         deleteCandidates, | ||||||
|         existing, |         existing, | ||||||
|         excludedAssets, |  | ||||||
|       ), |       ), | ||||||
|       onlyFirst: (AssetPathEntity ape) => |       onlyFirst: (AssetPathEntity ape) => _addAlbumFromDevice(ape, existing), | ||||||
|           _addAlbumFromDevice(ape, existing, excludedAssets), |  | ||||||
|       onlySecond: (Album a) => _removeAlbumFromDb(a, deleteCandidates), |       onlySecond: (Album a) => _removeAlbumFromDb(a, deleteCandidates), | ||||||
|     ); |     ); | ||||||
|     _log.fine( |     _log.fine( | ||||||
| @ -545,16 +541,13 @@ class SyncService { | |||||||
|     Album album, |     Album album, | ||||||
|     List<Asset> deleteCandidates, |     List<Asset> deleteCandidates, | ||||||
|     List<Asset> existing, [ |     List<Asset> existing, [ | ||||||
|     Set<String>? excludedAssets, |  | ||||||
|     bool forceRefresh = false, |     bool forceRefresh = false, | ||||||
|   ]) async { |   ]) async { | ||||||
|     if (!forceRefresh && !await _hasAssetPathEntityChanged(ape, album)) { |     if (!forceRefresh && !await _hasAssetPathEntityChanged(ape, album)) { | ||||||
|       _log.fine("Local album ${ape.name} has not changed. Skipping sync."); |       _log.fine("Local album ${ape.name} has not changed. Skipping sync."); | ||||||
|       return false; |       return false; | ||||||
|     } |     } | ||||||
|     if (!forceRefresh && |     if (!forceRefresh && await _syncDeviceAlbumFast(ape, album)) { | ||||||
|         excludedAssets == null && |  | ||||||
|         await _syncDeviceAlbumFast(ape, album)) { |  | ||||||
|       return true; |       return true; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -566,8 +559,7 @@ class SyncService { | |||||||
|         .findAll(); |         .findAll(); | ||||||
|     assert(inDb.isSorted(Asset.compareByChecksum), "inDb not sorted!"); |     assert(inDb.isSorted(Asset.compareByChecksum), "inDb not sorted!"); | ||||||
|     final int assetCountOnDevice = await ape.assetCountAsync; |     final int assetCountOnDevice = await ape.assetCountAsync; | ||||||
|     final List<Asset> onDevice = |     final List<Asset> onDevice = await _hashService.getHashedAssets(ape); | ||||||
|         await _hashService.getHashedAssets(ape, excludedAssets: excludedAssets); |  | ||||||
|     _removeDuplicates(onDevice); |     _removeDuplicates(onDevice); | ||||||
|     // _removeDuplicates sorts `onDevice` by checksum |     // _removeDuplicates sorts `onDevice` by checksum | ||||||
|     final (toAdd, toUpdate, toDelete) = _diffAssets(onDevice, inDb); |     final (toAdd, toUpdate, toDelete) = _diffAssets(onDevice, inDb); | ||||||
| @ -678,13 +670,11 @@ class SyncService { | |||||||
|   /// assets already existing in the database to the list of `existing` assets |   /// assets already existing in the database to the list of `existing` assets | ||||||
|   Future<void> _addAlbumFromDevice( |   Future<void> _addAlbumFromDevice( | ||||||
|     AssetPathEntity ape, |     AssetPathEntity ape, | ||||||
|     List<Asset> existing, [ |     List<Asset> existing, | ||||||
|     Set<String>? excludedAssets, |   ) async { | ||||||
|   ]) async { |  | ||||||
|     _log.info("Syncing a new local album to DB: ${ape.name}"); |     _log.info("Syncing a new local album to DB: ${ape.name}"); | ||||||
|     final Album a = Album.local(ape); |     final Album a = Album.local(ape); | ||||||
|     final assets = |     final assets = await _hashService.getHashedAssets(ape); | ||||||
|         await _hashService.getHashedAssets(ape, excludedAssets: excludedAssets); |  | ||||||
|     _removeDuplicates(assets); |     _removeDuplicates(assets); | ||||||
|     final (existingInDb, updated) = await _linkWithExistingFromDb(assets); |     final (existingInDb, updated) = await _linkWithExistingFromDb(assets); | ||||||
|     _log.info( |     _log.info( | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user