chore(mobile): use Record instead of custom pair+triple (#2483)

This commit is contained in:
Fynn Petersen-Frey 2023-05-21 03:41:34 +02:00 committed by GitHub
parent a089d9891d
commit dc7b0f75bb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 58 additions and 80 deletions

View File

@ -364,7 +364,7 @@ class BackupControllerPage extends HookConsumerWidget {
.read(backgroundServiceProvider)
.getIOSBackgroundAppRefreshEnabled(),
builder: (context, snapshot) {
final enabled = snapshot.data as bool?;
final enabled = snapshot.data;
// If it's not enabled, show them some kind of alert that says
// background refresh is not enabled
if (enabled != null && !enabled) {}

View File

@ -10,7 +10,6 @@ import 'package:immich_mobile/shared/providers/db.provider.dart';
import 'package:immich_mobile/shared/services/api.service.dart';
import 'package:immich_mobile/shared/services/sync.service.dart';
import 'package:immich_mobile/utils/openapi_extensions.dart';
import 'package:immich_mobile/utils/tuple.dart';
import 'package:isar/isar.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
@ -60,15 +59,14 @@ class AssetService {
}) async {
try {
final etag = hasCache ? Store.tryGet(StoreKey.assetETag) : null;
final Pair<List<AssetResponseDto>, String?>? remote =
final (List<AssetResponseDto>? assets, String? newETag) =
await _apiService.assetApi.getAllAssetsWithETag(eTag: etag);
if (remote == null) {
if (assets == null) {
return null;
} else if (newETag != etag) {
Store.put(StoreKey.assetETag, newETag);
}
if (remote.second != null && remote.second != etag) {
Store.put(StoreKey.assetETag, remote.second);
}
return remote.first;
return assets;
} catch (e, stack) {
log.severe('Error while getting remote assets', e, stack);
return null;

View File

@ -11,7 +11,6 @@ import 'package:immich_mobile/shared/providers/db.provider.dart';
import 'package:immich_mobile/utils/async_mutex.dart';
import 'package:immich_mobile/utils/builtin_extensions.dart';
import 'package:immich_mobile/utils/diff.dart';
import 'package:immich_mobile/utils/tuple.dart';
import 'package:isar/isar.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
@ -94,7 +93,7 @@ class SyncService {
deleteCandidates.sort(Asset.compareById);
existing.sort(Asset.compareById);
return _diffAssets(existing, deleteCandidates, compare: Asset.compareById)
.third
.$3
.map((e) => e.id)
.toList();
}
@ -165,14 +164,14 @@ class SyncService {
.thenByFileModifiedAt()
.findAll();
remote.sort(Asset.compareByOwnerDeviceLocalIdModified);
final diff = _diffAssets(remote, inDb, remote: true);
if (diff.first.isEmpty && diff.second.isEmpty && diff.third.isEmpty) {
final (toAdd, toUpdate, toRemove) = _diffAssets(remote, inDb, remote: true);
if (toAdd.isEmpty && toUpdate.isEmpty && toRemove.isEmpty) {
return false;
}
final idsToDelete = diff.third.map((e) => e.id).toList();
final idsToDelete = toRemove.map((e) => e.id).toList();
try {
await _db.writeTxn(() => _db.assets.deleteAll(idsToDelete));
await upsertAssetsWithExif(diff.first + diff.second);
await upsertAssetsWithExif(toAdd + toUpdate);
} on IsarError catch (e) {
_log.severe("Failed to sync remote assets to db: $e");
}
@ -252,8 +251,7 @@ class SyncService {
.findAll();
final List<Asset> assetsOnRemote = dto.getAssets();
assetsOnRemote.sort(Asset.compareByOwnerDeviceLocalIdModified);
final d = _diffAssets(assetsOnRemote, assetsInDb);
final List<Asset> toAdd = d.first, toUpdate = d.second, toUnlink = d.third;
final (toAdd, toUpdate, toUnlink) = _diffAssets(assetsOnRemote, assetsInDb);
// update shared users
final List<User> sharedUsers = album.sharedUsers.toList(growable: false);
@ -271,9 +269,9 @@ class SyncService {
);
// for shared album: put missing album assets into local DB
final resultPair = await _linkWithExistingFromDb(toAdd);
await upsertAssetsWithExif(resultPair.second);
final assetsToLink = resultPair.first + resultPair.second;
final (existingInDb, updated) = await _linkWithExistingFromDb(toAdd);
await upsertAssetsWithExif(updated);
final assetsToLink = existingInDb + updated;
final usersToLink = (await _db.users.getAllById(userIdsToAdd)).cast<User>();
album.name = dto.albumName;
@ -327,9 +325,10 @@ class SyncService {
if (dto.assetCount == dto.assets.length) {
// in case an album contains assets not yet present in local DB:
// put missing album assets into local DB
final result = await _linkWithExistingFromDb(dto.getAssets());
existing.addAll(result.first);
await upsertAssetsWithExif(result.second);
final (existingInDb, updated) =
await _linkWithExistingFromDb(dto.getAssets());
existing.addAll(existingInDb);
await upsertAssetsWithExif(updated);
final Album a = await Album.remote(dto);
await _db.writeTxn(() => _db.albums.store(a));
@ -393,18 +392,19 @@ class SyncService {
_log.fine(
"Syncing all local albums almost done. Collected ${deleteCandidates.length} asset candidates to delete",
);
final pair = _handleAssetRemoval(deleteCandidates, existing, remote: false);
final (toDelete, toUpdate) =
_handleAssetRemoval(deleteCandidates, existing, remote: false);
_log.fine(
"${pair.first.length} assets to delete, ${pair.second.length} to update",
"${toDelete.length} assets to delete, ${toUpdate.length} to update",
);
if (pair.first.isNotEmpty || pair.second.isNotEmpty) {
if (toDelete.isNotEmpty || toUpdate.isNotEmpty) {
await _db.writeTxn(() async {
await _db.assets.deleteAll(pair.first);
await _db.exifInfos.deleteAll(pair.first);
await _db.assets.putAll(pair.second);
await _db.assets.deleteAll(toDelete);
await _db.exifInfos.deleteAll(toDelete);
await _db.assets.putAll(toUpdate);
});
_log.info(
"Removed ${pair.first.length} and updated ${pair.second.length} local assets from DB",
"Removed ${toDelete.length} and updated ${toUpdate.length} local assets from DB",
);
}
return anyChanges;
@ -441,8 +441,8 @@ class SyncService {
final List<Asset> onDevice =
await ape.getAssets(excludedAssets: excludedAssets);
onDevice.sort(Asset.compareByLocalId);
final d = _diffAssets(onDevice, inDb, compare: Asset.compareByLocalId);
final List<Asset> toAdd = d.first, toUpdate = d.second, toDelete = d.third;
final (toAdd, toUpdate, toDelete) =
_diffAssets(onDevice, inDb, compare: Asset.compareByLocalId);
if (toAdd.isEmpty &&
toUpdate.isEmpty &&
toDelete.isEmpty &&
@ -458,12 +458,12 @@ class SyncService {
_log.fine(
"Syncing local album ${ape.name}. ${toAdd.length} assets to add, ${toUpdate.length} to update, ${toDelete.length} to delete",
);
final result = await _linkWithExistingFromDb(toAdd);
final (existingInDb, updated) = await _linkWithExistingFromDb(toAdd);
_log.fine(
"Linking assets to add with existing from db. ${result.first.length} existing, ${result.second.length} to update",
"Linking assets to add with existing from db. ${existingInDb.length} existing, ${updated.length} to update",
);
deleteCandidates.addAll(toDelete);
existing.addAll(result.first);
existing.addAll(existingInDb);
album.name = ape.name;
album.modifiedAt = ape.lastModified ?? DateTime.now();
if (album.thumbnail.value != null &&
@ -472,10 +472,10 @@ class SyncService {
}
try {
await _db.writeTxn(() async {
await _db.assets.putAll(result.second);
await _db.assets.putAll(updated);
await _db.assets.putAll(toUpdate);
await album.assets
.update(link: result.first + result.second, unlink: toDelete);
.update(link: existingInDb + updated, unlink: toDelete);
await _db.albums.put(album);
album.thumbnail.value ??= await album.assets.filter().findFirst();
await album.thumbnail.save();
@ -510,11 +510,11 @@ class SyncService {
return false;
}
album.modifiedAt = ape.lastModified ?? DateTime.now();
final result = await _linkWithExistingFromDb(newAssets);
final (existingInDb, updated) = await _linkWithExistingFromDb(newAssets);
try {
await _db.writeTxn(() async {
await _db.assets.putAll(result.second);
await album.assets.update(link: result.first + result.second);
await _db.assets.putAll(updated);
await album.assets.update(link: existingInDb + updated);
await _db.albums.put(album);
});
_log.info("Fast synced local album ${ape.name} to DB");
@ -536,15 +536,15 @@ class SyncService {
_log.info("Syncing a new local album to DB: ${ape.name}");
final Album a = Album.local(ape);
final assets = await ape.getAssets(excludedAssets: excludedAssets);
final result = await _linkWithExistingFromDb(assets);
final (existingInDb, updated) = await _linkWithExistingFromDb(assets);
_log.info(
"${result.first.length} assets already existed in DB, to upsert ${result.second.length}",
"${existingInDb.length} assets already existed in DB, to upsert ${updated.length}",
);
await upsertAssetsWithExif(result.second);
existing.addAll(result.first);
a.assets.addAll(result.first);
a.assets.addAll(result.second);
final thumb = result.first.firstOrNull ?? result.second.firstOrNull;
await upsertAssetsWithExif(updated);
existing.addAll(existingInDb);
a.assets.addAll(existingInDb);
a.assets.addAll(updated);
final thumb = existingInDb.firstOrNull ?? updated.firstOrNull;
a.thumbnail.value = thumb;
try {
await _db.writeTxn(() => _db.albums.store(a));
@ -555,11 +555,11 @@ class SyncService {
}
/// Returns a tuple (existing, updated)
Future<Pair<List<Asset>, List<Asset>>> _linkWithExistingFromDb(
Future<(List<Asset> existing, List<Asset> updated)> _linkWithExistingFromDb(
List<Asset> assets,
) async {
if (assets.isEmpty) {
return const Pair([], []);
return ([].cast<Asset>(), [].cast<Asset>());
}
final List<Asset> inDb = await _db.assets
.where()
@ -596,7 +596,7 @@ class SyncService {
),
onlySecond: (Asset b) => toUpsert.add(b),
);
return Pair(existing, toUpsert);
return (existing, toUpsert);
}
/// Inserts or updates the assets in the database with their ExifInfo (if any)
@ -623,7 +623,7 @@ class SyncService {
}
/// Returns a triple(toAdd, toUpdate, toRemove)
Triple<List<Asset>, List<Asset>, List<Asset>> _diffAssets(
(List<Asset> toAdd, List<Asset> toUpdate, List<Asset> toRemove) _diffAssets(
List<Asset> assets,
List<Asset> inDb, {
bool? remote,
@ -660,30 +660,30 @@ Triple<List<Asset>, List<Asset>, List<Asset>> _diffAssets(
},
onlySecond: (Asset b) => toAdd.add(b),
);
return Triple(toAdd, toUpdate, toRemove);
return (toAdd, toUpdate, toRemove);
}
/// returns a tuple (toDelete toUpdate) when assets are to be deleted
Pair<List<int>, List<Asset>> _handleAssetRemoval(
(List<int> toDelete, List<Asset> toUpdate) _handleAssetRemoval(
List<Asset> deleteCandidates,
List<Asset> existing, {
bool? remote,
}) {
if (deleteCandidates.isEmpty) {
return const Pair([], []);
return const ([], []);
}
deleteCandidates.sort(Asset.compareById);
deleteCandidates.uniqueConsecutive((a) => a.id);
existing.sort(Asset.compareById);
existing.uniqueConsecutive((a) => a.id);
final triple = _diffAssets(
final (tooAdd, toUpdate, toRemove) = _diffAssets(
existing,
deleteCandidates,
compare: Asset.compareById,
remote: remote,
);
assert(triple.first.isEmpty, "toAdd should be empty in _handleAssetRemoval");
return Pair(triple.third.map((e) => e.id).toList(), triple.second);
assert(tooAdd.isEmpty, "toAdd should be empty in _handleAssetRemoval");
return (toRemove.map((e) => e.id).toList(), toUpdate);
}
/// returns `true` if the albums differ on the surface

View File

@ -4,8 +4,6 @@ import 'dart:io';
import 'package:http/http.dart';
import 'package:openapi/api.dart';
import 'tuple.dart';
/// Extension methods to retrieve ETag together with the API call
extension WithETag on AssetApi {
/// Get all AssetEntity belong to the user
@ -14,7 +12,7 @@ extension WithETag on AssetApi {
///
/// * [String] eTag:
/// ETag of data already cached on the client
Future<Pair<List<AssetResponseDto>, String?>?> getAllAssetsWithETag({
Future<(List<AssetResponseDto>? assets, String? eTag)> getAllAssetsWithETag({
String? eTag,
}) async {
final response = await getAllAssetsWithHttpInfo(
@ -36,9 +34,9 @@ extension WithETag on AssetApi {
) as List)
.cast<AssetResponseDto>()
.toList();
return Pair(data, etag);
return (data, etag);
}
return null;
return (null, null);
}
}

View File

@ -1,18 +0,0 @@
/// An immutable pair or 2-tuple
/// TODO replace with Record once Dart 2.19 is available
class Pair<T1, T2> {
final T1 first;
final T2 second;
const Pair(this.first, this.second);
}
/// An immutable triple or 3-tuple
/// TODO replace with Record once Dart 2.19 is available
class Triple<T1, T2, T3> {
final T1 first;
final T2 second;
final T3 third;
const Triple(this.first, this.second, this.third);
}

View File

@ -6,7 +6,7 @@ version: 1.56.2+79
isar_version: &isar_version 3.0.5
environment:
sdk: ">=2.17.0 <3.0.0"
sdk: ">=3.0.0-0 <4.0.0"
dependencies:
flutter: