mirror of
https://github.com/immich-app/immich.git
synced 2025-01-16 16:56:46 +01: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
3 changed files with 14 additions and 97 deletions
|
@ -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…
Reference in a new issue