feature(mobile): sync assets, albums & users to local database on device (#1759)

* feature(mobile): sync assets, albums & users to local database on device

* try to fix tests

* move DB sync operations to new SyncService

* clear db on user logout

* fix reason for endless loading timeline

* fix error when deleting album

* fix thumbnail of device albums

* add a few comments

* fix Hive box not open in album service when loading local assets

* adjust tests to int IDs

* fix bug: show all albums when Recent is selected

* update generated api

* reworked Recents album isAll handling

* guard against wrongly interleaved sync operations

* fix: timeline asset ordering (sort asset state by created at)

* fix: sort assets in albums by created at
This commit is contained in:
Fynn Petersen-Frey
2023-03-03 23:38:30 +01:00
committed by GitHub
parent 8f11529a75
commit 8708867c1c
61 changed files with 9024 additions and 893 deletions

View File

@@ -0,0 +1,16 @@
import 'dart:async';
/// Async mutex to guarantee actions are performed sequentially and do not interleave
class AsyncMutex {
Future _running = Future.value(null);
/// Execute [operation] exclusively, after any currently running operations.
/// Returns a [Future] with the result of the [operation].
Future<T> run<T>(Future<T> Function() operation) {
final completer = Completer<T>();
_running.whenComplete(() {
completer.complete(Future<T>.sync(operation));
});
return _running = completer.future;
}
}

View File

@@ -5,7 +5,11 @@ extension DurationExtension on String {
return Duration(hours: parts[0], minutes: parts[1], seconds: parts[2]);
}
double? toDouble() {
return double.tryParse(this);
double toDouble() {
return double.parse(this);
}
int toInt() {
return int.parse(this);
}
}

View File

@@ -0,0 +1,71 @@
import 'dart:async';
/// Efficiently compares two sorted lists in O(n), calling the given callback
/// for each item.
/// Return `true` if there are any differences found, else `false`
Future<bool> diffSortedLists<A, B>(
List<A> la,
List<B> lb, {
required int Function(A a, B b) compare,
required FutureOr<bool> Function(A a, B b) both,
required FutureOr<void> Function(A a) onlyFirst,
required FutureOr<void> Function(B b) onlySecond,
}) async {
bool diff = false;
int i = 0, j = 0;
for (; i < la.length && j < lb.length;) {
final int order = compare(la[i], lb[j]);
if (order == 0) {
diff |= await both(la[i++], lb[j++]);
} else if (order < 0) {
await onlyFirst(la[i++]);
diff = true;
} else if (order > 0) {
await onlySecond(lb[j++]);
diff = true;
}
}
diff |= i < la.length || j < lb.length;
for (; i < la.length; i++) {
await onlyFirst(la[i]);
}
for (; j < lb.length; j++) {
await onlySecond(lb[j]);
}
return diff;
}
/// Efficiently compares two sorted lists in O(n), calling the given callback
/// for each item.
/// Return `true` if there are any differences found, else `false`
bool diffSortedListsSync<A, B>(
List<A> la,
List<B> lb, {
required int Function(A a, B b) compare,
required bool Function(A a, B b) both,
required void Function(A a) onlyFirst,
required void Function(B b) onlySecond,
}) {
bool diff = false;
int i = 0, j = 0;
for (; i < la.length && j < lb.length;) {
final int order = compare(la[i], lb[j]);
if (order == 0) {
diff |= both(la[i++], lb[j++]);
} else if (order < 0) {
onlyFirst(la[i++]);
diff = true;
} else if (order > 0) {
onlySecond(lb[j++]);
diff = true;
}
}
diff |= i < la.length || j < lb.length;
for (; i < la.length; i++) {
onlyFirst(la[i]);
}
for (; j < lb.length; j++) {
onlySecond(lb[j]);
}
return diff;
}

View File

@@ -0,0 +1,15 @@
/// FNV-1a 64bit hash algorithm optimized for Dart Strings
int fastHash(String string) {
var hash = 0xcbf29ce484222325;
var i = 0;
while (i < string.length) {
final codeUnit = string.codeUnitAt(i++);
hash ^= codeUnit >> 8;
hash *= 0x100000001b3;
hash ^= codeUnit & 0xFF;
hash *= 0x100000001b3;
}
return hash;
}

View File

@@ -31,20 +31,20 @@ String getAlbumThumbnailUrl(
final Album album, {
ThumbnailFormat type = ThumbnailFormat.WEBP,
}) {
if (album.albumThumbnailAssetId == null) {
if (album.thumbnail.value?.remoteId == null) {
return '';
}
return _getThumbnailUrl(album.albumThumbnailAssetId!, type: type);
return _getThumbnailUrl(album.thumbnail.value!.remoteId!, type: type);
}
String getAlbumThumbNailCacheKey(
final Album album, {
ThumbnailFormat type = ThumbnailFormat.WEBP,
}) {
if (album.albumThumbnailAssetId == null) {
if (album.thumbnail.value?.remoteId == null) {
return '';
}
return _getThumbnailCacheKey(album.albumThumbnailAssetId!, type);
return _getThumbnailCacheKey(album.thumbnail.value!.remoteId!, type);
}
String getImageUrl(final Asset asset) {

View File

@@ -1,7 +1,11 @@
// ignore_for_file: deprecated_member_use_from_same_package
import 'package:flutter/cupertino.dart';
import 'package:hive/hive.dart';
import 'package:immich_mobile/constants/hive_box.dart';
import 'package:immich_mobile/modules/album/services/album_cache.service.dart';
import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/shared/services/asset_cache.service.dart';
Future<void> migrateHiveToStoreIfNecessary() async {
try {
@@ -22,3 +26,9 @@ _migrateSingleKey(Box box, String hiveKey, StoreKey key) async {
await box.delete(hiveKey);
}
}
Future<void> migrateJsonCacheIfNecessary() async {
await AlbumCacheService().invalidate();
await SharedAlbumCacheService().invalidate();
await AssetCacheService().invalidate();
}

View File

@@ -6,3 +6,13 @@ class Pair<T1, T2> {
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);
}