mirror of
https://github.com/KevinMidboe/immich.git
synced 2025-10-29 17:40:28 +00:00
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:
committed by
GitHub
parent
8f11529a75
commit
8708867c1c
16
mobile/lib/utils/async_mutex.dart
Normal file
16
mobile/lib/utils/async_mutex.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
71
mobile/lib/utils/diff.dart
Normal file
71
mobile/lib/utils/diff.dart
Normal 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;
|
||||
}
|
||||
15
mobile/lib/utils/hash.dart
Normal file
15
mobile/lib/utils/hash.dart
Normal 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;
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user