diff --git a/mobile/lib/modules/backup/views/backup_controller_page.dart b/mobile/lib/modules/backup/views/backup_controller_page.dart index 1ee66f9737..fc04e52a6f 100644 --- a/mobile/lib/modules/backup/views/backup_controller_page.dart +++ b/mobile/lib/modules/backup/views/backup_controller_page.dart @@ -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) {} diff --git a/mobile/lib/shared/services/asset.service.dart b/mobile/lib/shared/services/asset.service.dart index f7eb2a066d..edd0df4366 100644 --- a/mobile/lib/shared/services/asset.service.dart +++ b/mobile/lib/shared/services/asset.service.dart @@ -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, String?>? remote = + final (List? 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; diff --git a/mobile/lib/shared/services/sync.service.dart b/mobile/lib/shared/services/sync.service.dart index efd0954ee5..b02aa9c15c 100644 --- a/mobile/lib/shared/services/sync.service.dart +++ b/mobile/lib/shared/services/sync.service.dart @@ -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 assetsOnRemote = dto.getAssets(); assetsOnRemote.sort(Asset.compareByOwnerDeviceLocalIdModified); - final d = _diffAssets(assetsOnRemote, assetsInDb); - final List toAdd = d.first, toUpdate = d.second, toUnlink = d.third; + final (toAdd, toUpdate, toUnlink) = _diffAssets(assetsOnRemote, assetsInDb); // update shared users final List 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(); 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 onDevice = await ape.getAssets(excludedAssets: excludedAssets); onDevice.sort(Asset.compareByLocalId); - final d = _diffAssets(onDevice, inDb, compare: Asset.compareByLocalId); - final List 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, List>> _linkWithExistingFromDb( + Future<(List existing, List updated)> _linkWithExistingFromDb( List assets, ) async { if (assets.isEmpty) { - return const Pair([], []); + return ([].cast(), [].cast()); } final List 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, List> _diffAssets( +(List toAdd, List toUpdate, List toRemove) _diffAssets( List assets, List inDb, { bool? remote, @@ -660,30 +660,30 @@ Triple, List, List> _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> _handleAssetRemoval( +(List toDelete, List toUpdate) _handleAssetRemoval( List deleteCandidates, List 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 diff --git a/mobile/lib/utils/openapi_extensions.dart b/mobile/lib/utils/openapi_extensions.dart index 96623514dc..d45cb94cf3 100644 --- a/mobile/lib/utils/openapi_extensions.dart +++ b/mobile/lib/utils/openapi_extensions.dart @@ -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, String?>?> getAllAssetsWithETag({ + Future<(List? assets, String? eTag)> getAllAssetsWithETag({ String? eTag, }) async { final response = await getAllAssetsWithHttpInfo( @@ -36,9 +34,9 @@ extension WithETag on AssetApi { ) as List) .cast() .toList(); - return Pair(data, etag); + return (data, etag); } - return null; + return (null, null); } } diff --git a/mobile/lib/utils/tuple.dart b/mobile/lib/utils/tuple.dart deleted file mode 100644 index 5663e03324..0000000000 --- a/mobile/lib/utils/tuple.dart +++ /dev/null @@ -1,18 +0,0 @@ -/// An immutable pair or 2-tuple -/// TODO replace with Record once Dart 2.19 is available -class Pair { - 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 { - final T1 first; - final T2 second; - final T3 third; - - const Triple(this.first, this.second, this.third); -} diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index b81eca0111..28c9f55118 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -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: