mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-31 10:49:11 -04:00 
			
		
		
		
	* 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
		
			
				
	
	
		
			122 lines
		
	
	
		
			3.6 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			122 lines
		
	
	
		
			3.6 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
| import 'package:immich_mobile/shared/models/user.dart';
 | |
| import 'package:isar/isar.dart';
 | |
| import 'dart:convert';
 | |
| 
 | |
| part 'store.g.dart';
 | |
| 
 | |
| /// Key-value store for individual items enumerated in StoreKey.
 | |
| /// Supports String, int and JSON-serializable Objects
 | |
| /// Can be used concurrently from multiple isolates
 | |
| class Store {
 | |
|   static late final Isar _db;
 | |
|   static final List<dynamic> _cache = List.filled(StoreKey.values.length, null);
 | |
| 
 | |
|   /// Initializes the store (call exactly once per app start)
 | |
|   static void init(Isar db) {
 | |
|     _db = db;
 | |
|     _populateCache();
 | |
|     _db.storeValues.where().build().watch().listen(_onChangeListener);
 | |
|   }
 | |
| 
 | |
|   /// clears all values from this store (cache and DB), only for testing!
 | |
|   static Future<void> clear() {
 | |
|     _cache.fillRange(0, _cache.length, null);
 | |
|     return _db.writeTxn(() => _db.storeValues.clear());
 | |
|   }
 | |
| 
 | |
|   /// Returns the stored value for the given key, or the default value if null
 | |
|   static T? get<T>(StoreKey key, [T? defaultValue]) =>
 | |
|       _cache[key.id] ?? defaultValue;
 | |
| 
 | |
|   /// Stores the value synchronously in the cache and asynchronously in the DB
 | |
|   static Future<void> put<T>(StoreKey key, T value) {
 | |
|     _cache[key.id] = value;
 | |
|     return _db.writeTxn(
 | |
|       () async => _db.storeValues.put(await StoreValue._of(value, key)),
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   /// Removes the value synchronously from the cache and asynchronously from the DB
 | |
|   static Future<void> delete(StoreKey key) {
 | |
|     _cache[key.id] = null;
 | |
|     return _db.writeTxn(() => _db.storeValues.delete(key.id));
 | |
|   }
 | |
| 
 | |
|   /// Fills the cache with the values from the DB
 | |
|   static _populateCache() {
 | |
|     for (StoreKey key in StoreKey.values) {
 | |
|       final StoreValue? value = _db.storeValues.getSync(key.id);
 | |
|       if (value != null) {
 | |
|         _cache[key.id] = value._extract(key);
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /// updates the state if a value is updated in any isolate
 | |
|   static void _onChangeListener(List<StoreValue>? data) {
 | |
|     if (data != null) {
 | |
|       for (StoreValue value in data) {
 | |
|         _cache[value.id] = value._extract(StoreKey.values[value.id]);
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| /// Internal class for `Store`, do not use elsewhere.
 | |
| @Collection(inheritance: false)
 | |
| class StoreValue {
 | |
|   StoreValue(this.id, {this.intValue, this.strValue});
 | |
|   Id id;
 | |
|   int? intValue;
 | |
|   String? strValue;
 | |
| 
 | |
|   T? _extract<T>(StoreKey key) => key.isInt
 | |
|       ? (key.fromDb == null ? intValue : key.fromDb!.call(Store._db, intValue!))
 | |
|       : (key.fromJson != null
 | |
|           ? key.fromJson!(json.decode(strValue!))
 | |
|           : strValue);
 | |
|   static Future<StoreValue> _of(dynamic value, StoreKey key) async =>
 | |
|       StoreValue(
 | |
|         key.id,
 | |
|         intValue: key.isInt
 | |
|             ? (key.toDb == null
 | |
|                 ? value
 | |
|                 : await key.toDb!.call(Store._db, value))
 | |
|             : null,
 | |
|         strValue: key.isInt
 | |
|             ? null
 | |
|             : (key.fromJson == null ? value : json.encode(value.toJson())),
 | |
|       );
 | |
| }
 | |
| 
 | |
| /// Key for each possible value in the `Store`.
 | |
| /// Defines the data type (int, String, JSON) for each value
 | |
| enum StoreKey {
 | |
|   userRemoteId(0),
 | |
|   assetETag(1),
 | |
|   currentUser(2, isInt: true, fromDb: _getUser, toDb: _toUser),
 | |
|   deviceIdHash(3, isInt: true),
 | |
|   deviceId(4),
 | |
|   ;
 | |
| 
 | |
|   const StoreKey(
 | |
|     this.id, {
 | |
|     this.isInt = false,
 | |
|     this.fromDb,
 | |
|     this.toDb,
 | |
|     // ignore: unused_element
 | |
|     this.fromJson,
 | |
|   });
 | |
|   final int id;
 | |
|   final bool isInt;
 | |
|   final dynamic Function(Isar, int)? fromDb;
 | |
|   final Future<int> Function(Isar, dynamic)? toDb;
 | |
|   final Function(dynamic)? fromJson;
 | |
| }
 | |
| 
 | |
| User? _getUser(Isar db, int i) => db.users.getSync(i);
 | |
| Future<int> _toUser(Isar db, dynamic u) {
 | |
|   User user = (u as User);
 | |
|   return db.users.put(user);
 | |
| }
 |