mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-04 03:39:37 -05: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