mirror of
https://github.com/samsonjs/immich.git
synced 2026-04-27 15:07:45 +00:00
feat: migrate store to sqlite (#21078)
* add store entity and migration * make store service take both isar and drift repos * migrate and switch store on beta timeline state change * chore: make drift variables final * dispose old store before switching repos * use store to update values for beta timeline * change log service to use the proper store * migrate store when beta already enabled * use isar repository to check beta timeline in store service * remove unused update method from store repo * dispose after create * change watchAll signature in store repo * fix test * rename init isar to initDB * request user to close and reopen on beta migration * fix tests * handle empty version in migration * wait for cache to be populated after migration --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
parent
ed3997d844
commit
6f4f79d8cc
26 changed files with 409 additions and 164 deletions
BIN
mobile/drift_schemas/main/drift_schema_v8.json
generated
Normal file
BIN
mobile/drift_schemas/main/drift_schema_v8.json
generated
Normal file
Binary file not shown.
|
|
@ -4,7 +4,6 @@ import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart';
|
|
||||||
import 'package:immich_mobile/main.dart' as app;
|
import 'package:immich_mobile/main.dart' as app;
|
||||||
import 'package:immich_mobile/providers/db.provider.dart';
|
import 'package:immich_mobile/providers/db.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||||
|
|
@ -40,15 +39,18 @@ class ImmichTestHelper {
|
||||||
static Future<void> loadApp(WidgetTester tester) async {
|
static Future<void> loadApp(WidgetTester tester) async {
|
||||||
await EasyLocalization.ensureInitialized();
|
await EasyLocalization.ensureInitialized();
|
||||||
// Clear all data from Isar (reuse existing instance if available)
|
// Clear all data from Isar (reuse existing instance if available)
|
||||||
final db = await Bootstrap.initIsar();
|
final (isar, drift, logDb) = await Bootstrap.initDB();
|
||||||
final logDb = DriftLogger();
|
await Bootstrap.initDomain(isar, drift, logDb);
|
||||||
await Bootstrap.initDomain(db, logDb);
|
|
||||||
await Store.clear();
|
await Store.clear();
|
||||||
await db.writeTxn(() => db.clear());
|
await isar.writeTxn(() => isar.clear());
|
||||||
// Load main Widget
|
// Load main Widget
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
ProviderScope(
|
ProviderScope(
|
||||||
overrides: [dbProvider.overrideWithValue(db), isarProvider.overrideWithValue(db)],
|
overrides: [
|
||||||
|
dbProvider.overrideWithValue(isar),
|
||||||
|
isarProvider.overrideWithValue(isar),
|
||||||
|
driftProvider.overrideWith(driftOverride(drift)),
|
||||||
|
],
|
||||||
child: const app.MainWidget(),
|
child: const app.MainWidget(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ import 'package:logging/logging.dart';
|
||||||
/// via [IStoreRepository]
|
/// via [IStoreRepository]
|
||||||
class LogService {
|
class LogService {
|
||||||
final LogRepository _logRepository;
|
final LogRepository _logRepository;
|
||||||
final IsarStoreRepository _storeRepository;
|
final IStoreRepository _storeRepository;
|
||||||
|
|
||||||
final List<LogMessage> _msgBuffer = [];
|
final List<LogMessage> _msgBuffer = [];
|
||||||
|
|
||||||
|
|
@ -38,7 +38,7 @@ class LogService {
|
||||||
|
|
||||||
static Future<LogService> init({
|
static Future<LogService> init({
|
||||||
required LogRepository logRepository,
|
required LogRepository logRepository,
|
||||||
required IsarStoreRepository storeRepository,
|
required IStoreRepository storeRepository,
|
||||||
bool shouldBuffer = true,
|
bool shouldBuffer = true,
|
||||||
}) async {
|
}) async {
|
||||||
_instance ??= await create(
|
_instance ??= await create(
|
||||||
|
|
@ -51,7 +51,7 @@ class LogService {
|
||||||
|
|
||||||
static Future<LogService> create({
|
static Future<LogService> create({
|
||||||
required LogRepository logRepository,
|
required LogRepository logRepository,
|
||||||
required IsarStoreRepository storeRepository,
|
required IStoreRepository storeRepository,
|
||||||
bool shouldBuffer = true,
|
bool shouldBuffer = true,
|
||||||
}) async {
|
}) async {
|
||||||
final instance = LogService._(logRepository, storeRepository, shouldBuffer);
|
final instance = LogService._(logRepository, storeRepository, shouldBuffer);
|
||||||
|
|
@ -92,7 +92,7 @@ class LogService {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> setLogLevel(LogLevel level) async {
|
Future<void> setLogLevel(LogLevel level) async {
|
||||||
await _storeRepository.insert(StoreKey.logLevel, level.index);
|
await _storeRepository.upsert(StoreKey.logLevel, level.index);
|
||||||
Logger.root.level = level.toLevel();
|
Logger.root.level = level.toLevel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,13 @@ import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'
|
||||||
/// Provides access to a persistent key-value store with an in-memory cache.
|
/// Provides access to a persistent key-value store with an in-memory cache.
|
||||||
/// Listens for repository changes to keep the cache updated.
|
/// Listens for repository changes to keep the cache updated.
|
||||||
class StoreService {
|
class StoreService {
|
||||||
final IsarStoreRepository _storeRepository;
|
final IStoreRepository _storeRepository;
|
||||||
|
|
||||||
/// In-memory cache. Keys are [StoreKey.id]
|
/// In-memory cache. Keys are [StoreKey.id]
|
||||||
final Map<int, Object?> _cache = {};
|
final Map<int, Object?> _cache = {};
|
||||||
late final StreamSubscription<StoreDto> _storeUpdateSubscription;
|
late final StreamSubscription<List<StoreDto>> _storeUpdateSubscription;
|
||||||
|
|
||||||
StoreService._({required IsarStoreRepository storeRepository}) : _storeRepository = storeRepository;
|
StoreService._({required IStoreRepository isarStoreRepository}) : _storeRepository = isarStoreRepository;
|
||||||
|
|
||||||
// TODO: Temporary typedef to make minimal changes. Remove this and make the presentation layer access store through a provider
|
// TODO: Temporary typedef to make minimal changes. Remove this and make the presentation layer access store through a provider
|
||||||
static StoreService? _instance;
|
static StoreService? _instance;
|
||||||
|
|
@ -24,27 +24,29 @@ class StoreService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Replace the implementation with the one from create after removing the typedef
|
// TODO: Replace the implementation with the one from create after removing the typedef
|
||||||
static Future<StoreService> init({required IsarStoreRepository storeRepository}) async {
|
static Future<StoreService> init({required IStoreRepository storeRepository}) async {
|
||||||
_instance ??= await create(storeRepository: storeRepository);
|
_instance ??= await create(storeRepository: storeRepository);
|
||||||
return _instance!;
|
return _instance!;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<StoreService> create({required IsarStoreRepository storeRepository}) async {
|
static Future<StoreService> create({required IStoreRepository storeRepository}) async {
|
||||||
final instance = StoreService._(storeRepository: storeRepository);
|
final instance = StoreService._(isarStoreRepository: storeRepository);
|
||||||
await instance._populateCache();
|
await instance.populateCache();
|
||||||
instance._storeUpdateSubscription = instance._listenForChange();
|
instance._storeUpdateSubscription = instance._listenForChange();
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _populateCache() async {
|
Future<void> populateCache() async {
|
||||||
final storeValues = await _storeRepository.getAll();
|
final storeValues = await _storeRepository.getAll();
|
||||||
for (StoreDto storeValue in storeValues) {
|
for (StoreDto storeValue in storeValues) {
|
||||||
_cache[storeValue.key.id] = storeValue.value;
|
_cache[storeValue.key.id] = storeValue.value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
StreamSubscription<StoreDto> _listenForChange() => _storeRepository.watchAll().listen((event) {
|
StreamSubscription<List<StoreDto>> _listenForChange() => _storeRepository.watchAll().listen((events) {
|
||||||
|
for (final event in events) {
|
||||||
_cache[event.key.id] = event.value;
|
_cache[event.key.id] = event.value;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Disposes the store and cancels the subscription. To reuse the store call init() again
|
/// Disposes the store and cancels the subscription. To reuse the store call init() again
|
||||||
|
|
@ -69,7 +71,7 @@ class StoreService {
|
||||||
/// Stores the [value] for the [key]. Skips write if value hasn't changed.
|
/// Stores the [value] for the [key]. Skips write if value hasn't changed.
|
||||||
Future<void> put<U extends StoreKey<T>, T>(U key, T value) async {
|
Future<void> put<U extends StoreKey<T>, T>(U key, T value) async {
|
||||||
if (_cache[key.id] == value) return;
|
if (_cache[key.id] == value) return;
|
||||||
await _storeRepository.insert(key, value);
|
await _storeRepository.upsert(key, value);
|
||||||
_cache[key.id] = value;
|
_cache[key.id] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
|
|
||||||
part 'store.entity.g.dart';
|
part 'store.entity.g.dart';
|
||||||
|
|
@ -11,3 +13,13 @@ class StoreValue {
|
||||||
|
|
||||||
const StoreValue(this.id, {this.intValue, this.strValue});
|
const StoreValue(this.id, {this.intValue, this.strValue});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class StoreEntity extends Table with DriftDefaultsMixin {
|
||||||
|
IntColumn get id => integer()();
|
||||||
|
|
||||||
|
TextColumn get stringValue => text().nullable()();
|
||||||
|
IntColumn get intValue => integer().nullable()();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Set<Column> get primaryKey => {id};
|
||||||
|
}
|
||||||
|
|
|
||||||
BIN
mobile/lib/infrastructure/entities/store.entity.drift.dart
generated
Normal file
BIN
mobile/lib/infrastructure/entities/store.entity.drift.dart
generated
Normal file
Binary file not shown.
|
|
@ -18,6 +18,7 @@ import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.
|
||||||
import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/stack.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/stack.entity.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.steps.dart';
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.steps.dart';
|
||||||
|
|
@ -58,6 +59,7 @@ class IsarDatabaseRepository implements IDatabaseRepository {
|
||||||
StackEntity,
|
StackEntity,
|
||||||
PersonEntity,
|
PersonEntity,
|
||||||
AssetFaceEntity,
|
AssetFaceEntity,
|
||||||
|
StoreEntity,
|
||||||
],
|
],
|
||||||
include: {'package:immich_mobile/infrastructure/entities/merged_asset.drift'},
|
include: {'package:immich_mobile/infrastructure/entities/merged_asset.drift'},
|
||||||
)
|
)
|
||||||
|
|
@ -66,7 +68,7 @@ class Drift extends $Drift implements IDatabaseRepository {
|
||||||
: super(executor ?? driftDatabase(name: 'immich', native: const DriftNativeOptions(shareAcrossIsolates: true)));
|
: super(executor ?? driftDatabase(name: 'immich', native: const DriftNativeOptions(shareAcrossIsolates: true)));
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get schemaVersion => 7;
|
int get schemaVersion => 8;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MigrationStrategy get migration => MigrationStrategy(
|
MigrationStrategy get migration => MigrationStrategy(
|
||||||
|
|
@ -118,6 +120,9 @@ class Drift extends $Drift implements IDatabaseRepository {
|
||||||
from6To7: (m, v7) async {
|
from6To7: (m, v7) async {
|
||||||
await m.createIndex(v7.idxLatLng);
|
await m.createIndex(v7.idxLatLng);
|
||||||
},
|
},
|
||||||
|
from7To8: (m, v8) async {
|
||||||
|
await m.create(v8.storeEntity);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
|
|
@ -1,16 +1,30 @@
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/store.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/store.entity.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/user.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/user.repository.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
|
|
||||||
class IsarStoreRepository extends IsarDatabaseRepository {
|
// Temporary interface until Isar is removed to make the service work with both Isar and Sqlite
|
||||||
|
abstract class IStoreRepository {
|
||||||
|
Future<bool> deleteAll();
|
||||||
|
Stream<List<StoreDto<Object>>> watchAll();
|
||||||
|
Future<void> delete<T>(StoreKey<T> key);
|
||||||
|
Future<bool> upsert<T>(StoreKey<T> key, T value);
|
||||||
|
Future<T?> tryGet<T>(StoreKey<T> key);
|
||||||
|
Stream<T?> watch<T>(StoreKey<T> key);
|
||||||
|
Future<List<StoreDto<Object>>> getAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
class IsarStoreRepository extends IsarDatabaseRepository implements IStoreRepository {
|
||||||
final Isar _db;
|
final Isar _db;
|
||||||
final validStoreKeys = StoreKey.values.map((e) => e.id).toSet();
|
final validStoreKeys = StoreKey.values.map((e) => e.id).toSet();
|
||||||
|
|
||||||
IsarStoreRepository(super.db) : _db = db;
|
IsarStoreRepository(super.db) : _db = db;
|
||||||
|
|
||||||
|
@override
|
||||||
Future<bool> deleteAll() async {
|
Future<bool> deleteAll() async {
|
||||||
return await transaction(() async {
|
return await transaction(() async {
|
||||||
await _db.storeValues.clear();
|
await _db.storeValues.clear();
|
||||||
|
|
@ -18,25 +32,29 @@ class IsarStoreRepository extends IsarDatabaseRepository {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream<StoreDto<Object>> watchAll() {
|
@override
|
||||||
|
Stream<List<StoreDto<Object>>> watchAll() {
|
||||||
return _db.storeValues
|
return _db.storeValues
|
||||||
.filter()
|
.filter()
|
||||||
.anyOf(validStoreKeys, (query, id) => query.idEqualTo(id))
|
.anyOf(validStoreKeys, (query, id) => query.idEqualTo(id))
|
||||||
.watch(fireImmediately: true)
|
.watch(fireImmediately: true)
|
||||||
.asyncExpand((entities) => Stream.fromFutures(entities.map((e) async => _toUpdateEvent(e))));
|
.asyncMap((entities) => Future.wait(entities.map((entity) => _toUpdateEvent(entity))));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
Future<void> delete<T>(StoreKey<T> key) async {
|
Future<void> delete<T>(StoreKey<T> key) async {
|
||||||
return await transaction(() async => await _db.storeValues.delete(key.id));
|
return await transaction(() async => await _db.storeValues.delete(key.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> insert<T>(StoreKey<T> key, T value) async {
|
@override
|
||||||
|
Future<bool> upsert<T>(StoreKey<T> key, T value) async {
|
||||||
return await transaction(() async {
|
return await transaction(() async {
|
||||||
await _db.storeValues.put(await _fromValue(key, value));
|
await _db.storeValues.put(await _fromValue(key, value));
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
Future<T?> tryGet<T>(StoreKey<T> key) async {
|
Future<T?> tryGet<T>(StoreKey<T> key) async {
|
||||||
final entity = (await _db.storeValues.get(key.id));
|
final entity = (await _db.storeValues.get(key.id));
|
||||||
if (entity == null) {
|
if (entity == null) {
|
||||||
|
|
@ -45,13 +63,7 @@ class IsarStoreRepository extends IsarDatabaseRepository {
|
||||||
return await _toValue(key, entity);
|
return await _toValue(key, entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> update<T>(StoreKey<T> key, T value) async {
|
@override
|
||||||
return await transaction(() async {
|
|
||||||
await _db.storeValues.put(await _fromValue(key, value));
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Stream<T?> watch<T>(StoreKey<T> key) async* {
|
Stream<T?> watch<T>(StoreKey<T> key) async* {
|
||||||
yield* _db.storeValues
|
yield* _db.storeValues
|
||||||
.watchObject(key.id, fireImmediately: true)
|
.watchObject(key.id, fireImmediately: true)
|
||||||
|
|
@ -88,8 +100,93 @@ class IsarStoreRepository extends IsarDatabaseRepository {
|
||||||
return StoreValue(key.id, intValue: intValue, strValue: strValue);
|
return StoreValue(key.id, intValue: intValue, strValue: strValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
Future<List<StoreDto<Object>>> getAll() async {
|
Future<List<StoreDto<Object>>> getAll() async {
|
||||||
final entities = await _db.storeValues.filter().anyOf(validStoreKeys, (query, id) => query.idEqualTo(id)).findAll();
|
final entities = await _db.storeValues.filter().anyOf(validStoreKeys, (query, id) => query.idEqualTo(id)).findAll();
|
||||||
return Future.wait(entities.map((e) => _toUpdateEvent(e)).toList());
|
return Future.wait(entities.map((e) => _toUpdateEvent(e)).toList());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class DriftStoreRepository extends DriftDatabaseRepository implements IStoreRepository {
|
||||||
|
final Drift _db;
|
||||||
|
final validStoreKeys = StoreKey.values.map((e) => e.id).toSet();
|
||||||
|
|
||||||
|
DriftStoreRepository(super.db) : _db = db;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> deleteAll() async {
|
||||||
|
await _db.storeEntity.deleteAll();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<StoreDto<Object>>> getAll() async {
|
||||||
|
final query = _db.storeEntity.select()..where((entity) => entity.id.isIn(validStoreKeys));
|
||||||
|
return query.asyncMap((entity) => _toUpdateEvent(entity)).get();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<List<StoreDto<Object>>> watchAll() {
|
||||||
|
final query = _db.storeEntity.select()..where((entity) => entity.id.isIn(validStoreKeys));
|
||||||
|
|
||||||
|
return query.asyncMap((entity) => _toUpdateEvent(entity)).watch();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> delete<T>(StoreKey<T> key) async {
|
||||||
|
await _db.storeEntity.deleteWhere((entity) => entity.id.equals(key.id));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> upsert<T>(StoreKey<T> key, T value) async {
|
||||||
|
await _db.storeEntity.insertOnConflictUpdate(await _fromValue(key, value));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<T?> tryGet<T>(StoreKey<T> key) async {
|
||||||
|
final entity = await _db.managers.storeEntity.filter((entity) => entity.id.equals(key.id)).getSingleOrNull();
|
||||||
|
if (entity == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return await _toValue(key, entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<T?> watch<T>(StoreKey<T> key) async* {
|
||||||
|
final query = _db.storeEntity.select()..where((entity) => entity.id.equals(key.id));
|
||||||
|
|
||||||
|
yield* query.watchSingleOrNull().asyncMap((e) async => e == null ? null : await _toValue(key, e));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<StoreDto<Object>> _toUpdateEvent(StoreEntityData entity) async {
|
||||||
|
final key = StoreKey.values.firstWhere((e) => e.id == entity.id) as StoreKey<Object>;
|
||||||
|
final value = await _toValue(key, entity);
|
||||||
|
return StoreDto(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<T?> _toValue<T>(StoreKey<T> key, StoreEntityData entity) async =>
|
||||||
|
switch (key.type) {
|
||||||
|
const (int) => entity.intValue,
|
||||||
|
const (String) => entity.stringValue,
|
||||||
|
const (bool) => entity.intValue == 1,
|
||||||
|
const (DateTime) => entity.intValue == null ? null : DateTime.fromMillisecondsSinceEpoch(entity.intValue!),
|
||||||
|
const (UserDto) =>
|
||||||
|
entity.stringValue == null ? null : await DriftUserRepository(_db).get(entity.stringValue!),
|
||||||
|
_ => null,
|
||||||
|
}
|
||||||
|
as T?;
|
||||||
|
|
||||||
|
Future<StoreEntityCompanion> _fromValue<T>(StoreKey<T> key, T value) async {
|
||||||
|
final (int? intValue, String? strValue) = switch (key.type) {
|
||||||
|
const (int) => (value as int, null),
|
||||||
|
const (String) => (null, value as String),
|
||||||
|
const (bool) => ((value as bool) ? 1 : 0, null),
|
||||||
|
const (DateTime) => ((value as DateTime).millisecondsSinceEpoch, null),
|
||||||
|
const (UserDto) => (null, (await DriftUserRepository(_db).upsert(value as UserDto)).id),
|
||||||
|
_ => throw UnsupportedError("Unsupported primitive type: ${key.type} for key: ${key.name}"),
|
||||||
|
};
|
||||||
|
return StoreEntityCompanion(id: Value(key.id), intValue: Value(intValue), stringValue: Value(strValue));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
import 'package:immich_mobile/constants/enums.dart';
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as entity;
|
import 'package:immich_mobile/infrastructure/entities/user.entity.dart' as entity;
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
|
|
||||||
|
|
@ -63,3 +65,40 @@ class IsarUserRepository extends IsarDatabaseRepository {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class DriftUserRepository extends DriftDatabaseRepository {
|
||||||
|
final Drift _db;
|
||||||
|
const DriftUserRepository(super.db) : _db = db;
|
||||||
|
|
||||||
|
Future<UserDto?> get(String id) =>
|
||||||
|
_db.managers.userEntity.filter((user) => user.id.equals(id)).getSingleOrNull().then((user) => user?.toDto());
|
||||||
|
|
||||||
|
Future<UserDto> upsert(UserDto user) async {
|
||||||
|
await _db.userEntity.insertOnConflictUpdate(
|
||||||
|
UserEntityCompanion(
|
||||||
|
id: Value(user.id),
|
||||||
|
isAdmin: Value(user.isAdmin),
|
||||||
|
updatedAt: Value(user.updatedAt),
|
||||||
|
name: Value(user.name),
|
||||||
|
email: Value(user.email),
|
||||||
|
hasProfileImage: Value(user.hasProfileImage),
|
||||||
|
profileChangedAt: Value(user.profileChangedAt),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension on UserEntityData {
|
||||||
|
UserDto toDto() {
|
||||||
|
return UserDto(
|
||||||
|
id: id,
|
||||||
|
email: email,
|
||||||
|
name: name,
|
||||||
|
isAdmin: isAdmin,
|
||||||
|
updatedAt: updatedAt,
|
||||||
|
profileChangedAt: profileChangedAt,
|
||||||
|
hasProfileImage: hasProfileImage,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ import 'package:immich_mobile/constants/constants.dart';
|
||||||
import 'package:immich_mobile/constants/locales.dart';
|
import 'package:immich_mobile/constants/locales.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/generated/codegen_loader.g.dart';
|
import 'package:immich_mobile/generated/codegen_loader.g.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart';
|
|
||||||
import 'package:immich_mobile/providers/app_life_cycle.provider.dart';
|
import 'package:immich_mobile/providers/app_life_cycle.provider.dart';
|
||||||
import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart';
|
||||||
import 'package:immich_mobile/providers/db.provider.dart';
|
import 'package:immich_mobile/providers/db.provider.dart';
|
||||||
|
|
@ -41,18 +40,21 @@ import 'package:worker_manager/worker_manager.dart';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
ImmichWidgetsBinding();
|
ImmichWidgetsBinding();
|
||||||
final db = await Bootstrap.initIsar();
|
final (isar, drift, logDb) = await Bootstrap.initDB();
|
||||||
final logDb = DriftLogger();
|
await Bootstrap.initDomain(isar, drift, logDb);
|
||||||
await Bootstrap.initDomain(db, logDb);
|
|
||||||
await initApp();
|
await initApp();
|
||||||
// Warm-up isolate pool for worker manager
|
// Warm-up isolate pool for worker manager
|
||||||
await workerManager.init(dynamicSpawning: true);
|
await workerManager.init(dynamicSpawning: true);
|
||||||
await migrateDatabaseIfNeeded(db);
|
await migrateDatabaseIfNeeded(isar, drift);
|
||||||
HttpSSLOptions.apply();
|
HttpSSLOptions.apply();
|
||||||
|
|
||||||
runApp(
|
runApp(
|
||||||
ProviderScope(
|
ProviderScope(
|
||||||
overrides: [dbProvider.overrideWithValue(db), isarProvider.overrideWithValue(db)],
|
overrides: [
|
||||||
|
dbProvider.overrideWithValue(isar),
|
||||||
|
isarProvider.overrideWithValue(isar),
|
||||||
|
driftProvider.overrideWith(driftOverride(drift)),
|
||||||
|
],
|
||||||
child: const MainWidget(),
|
child: const MainWidget(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
|
||||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/asset.provider.dart';
|
import 'package:immich_mobile/providers/asset.provider.dart';
|
||||||
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||||
|
|
@ -13,8 +14,8 @@ import 'package:immich_mobile/providers/backup/manual_upload.provider.dart';
|
||||||
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||||
import 'package:immich_mobile/providers/websocket.provider.dart';
|
import 'package:immich_mobile/providers/websocket.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
|
||||||
import 'package:immich_mobile/utils/migration.dart';
|
import 'package:immich_mobile/utils/migration.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
import 'package:permission_handler/permission_handler.dart';
|
import 'package:permission_handler/permission_handler.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
|
|
@ -28,7 +29,7 @@ class ChangeExperiencePage extends ConsumerStatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ChangeExperiencePageState extends ConsumerState<ChangeExperiencePage> {
|
class _ChangeExperiencePageState extends ConsumerState<ChangeExperiencePage> {
|
||||||
bool hasMigrated = false;
|
AsyncValue<bool> hasMigrated = const AsyncValue.loading();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
|
@ -37,6 +38,7 @@ class _ChangeExperiencePageState extends ConsumerState<ChangeExperiencePage> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _handleMigration() async {
|
Future<void> _handleMigration() async {
|
||||||
|
try {
|
||||||
if (widget.switchingToBeta) {
|
if (widget.switchingToBeta) {
|
||||||
final assetNotifier = ref.read(assetProvider.notifier);
|
final assetNotifier = ref.read(assetProvider.notifier);
|
||||||
if (assetNotifier.mounted) {
|
if (assetNotifier.mounted) {
|
||||||
|
|
@ -65,19 +67,32 @@ class _ChangeExperiencePageState extends ConsumerState<ChangeExperiencePage> {
|
||||||
await ref.read(backgroundSyncProvider).syncLocal(full: true);
|
await ref.read(backgroundSyncProvider).syncLocal(full: true);
|
||||||
await migrateDeviceAssetToSqlite(ref.read(isarProvider), ref.read(driftProvider));
|
await migrateDeviceAssetToSqlite(ref.read(isarProvider), ref.read(driftProvider));
|
||||||
await migrateBackupAlbumsToSqlite(ref.read(isarProvider), ref.read(driftProvider));
|
await migrateBackupAlbumsToSqlite(ref.read(isarProvider), ref.read(driftProvider));
|
||||||
|
await migrateStoreToSqlite(ref.read(isarProvider), ref.read(driftProvider));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
await ref.read(backgroundSyncProvider).cancel();
|
await ref.read(backgroundSyncProvider).cancel();
|
||||||
ref.read(websocketProvider.notifier).stopListeningToBetaEvents();
|
ref.read(websocketProvider.notifier).stopListeningToBetaEvents();
|
||||||
ref.read(websocketProvider.notifier).startListeningToOldEvents();
|
ref.read(websocketProvider.notifier).startListeningToOldEvents();
|
||||||
|
await migrateStoreToIsar(ref.read(isarProvider), ref.read(driftProvider));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await IsarStoreRepository(ref.read(isarProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta);
|
||||||
|
await DriftStoreRepository(ref.read(driftProvider)).upsert(StoreKey.betaTimeline, widget.switchingToBeta);
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
HapticFeedback.heavyImpact();
|
HapticFeedback.heavyImpact();
|
||||||
hasMigrated = true;
|
hasMigrated = const AsyncValue.data(true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} catch (e, s) {
|
||||||
|
Logger("ChangeExperiencePage").severe("Error during migration", e, s);
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
hasMigrated = AsyncValue.error(e, s);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -89,45 +104,35 @@ class _ChangeExperiencePageState extends ConsumerState<ChangeExperiencePage> {
|
||||||
children: [
|
children: [
|
||||||
AnimatedSwitcher(
|
AnimatedSwitcher(
|
||||||
duration: Durations.long4,
|
duration: Durations.long4,
|
||||||
child: hasMigrated
|
child: hasMigrated.when(
|
||||||
? const Icon(Icons.check_circle_rounded, color: Colors.green, size: 48.0)
|
data: (data) => const Icon(Icons.check_circle_rounded, color: Colors.green, size: 48.0),
|
||||||
: const SizedBox(width: 50.0, height: 50.0, child: CircularProgressIndicator()),
|
error: (error, stackTrace) => const Icon(Icons.error, color: Colors.red, size: 48.0),
|
||||||
|
loading: () => const SizedBox(width: 50.0, height: 50.0, child: CircularProgressIndicator()),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16.0),
|
const SizedBox(height: 16.0),
|
||||||
Center(
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: 300.0,
|
width: 300.0,
|
||||||
child: AnimatedSwitcher(
|
child: AnimatedSwitcher(
|
||||||
duration: Durations.long4,
|
duration: Durations.long4,
|
||||||
child: hasMigrated
|
child: hasMigrated.when(
|
||||||
? Text(
|
data: (data) => Text(
|
||||||
"Migration success!",
|
"Migration success!\nPlease close and reopen the app to apply changes",
|
||||||
style: context.textTheme.titleMedium,
|
style: context.textTheme.titleMedium,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
)
|
),
|
||||||
: Text(
|
error: (error, stackTrace) => Text(
|
||||||
|
"Migration failed!\nError: $error",
|
||||||
|
style: context.textTheme.titleMedium,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
loading: () => Text(
|
||||||
"Data migration in progress...\nPlease wait and don't close this page",
|
"Data migration in progress...\nPlease wait and don't close this page",
|
||||||
style: context.textTheme.titleMedium,
|
style: context.textTheme.titleMedium,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (hasMigrated)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 16.0),
|
|
||||||
child: ElevatedButton(
|
|
||||||
onPressed: () {
|
|
||||||
context.replaceRoute(
|
|
||||||
widget.switchingToBeta ? const TabShellRoute() : const TabControllerRoute(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: const Text("Continue"),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,12 @@ part 'db.provider.g.dart';
|
||||||
@Riverpod(keepAlive: true)
|
@Riverpod(keepAlive: true)
|
||||||
Isar isar(Ref ref) => throw UnimplementedError('isar');
|
Isar isar(Ref ref) => throw UnimplementedError('isar');
|
||||||
|
|
||||||
final driftProvider = Provider<Drift>((ref) {
|
Drift Function(Ref ref) driftOverride(Drift drift) => (ref) {
|
||||||
final drift = Drift();
|
|
||||||
ref.onDispose(() => unawaited(drift.close()));
|
ref.onDispose(() => unawaited(drift.close()));
|
||||||
ref.keepAlive();
|
ref.keepAlive();
|
||||||
return drift;
|
return drift;
|
||||||
});
|
};
|
||||||
|
|
||||||
|
final driftProvider = Provider<Drift>(
|
||||||
|
(ref) => throw UnimplementedError("driftProvider must be overridden in the isolate's ProviderContainer before use"),
|
||||||
|
);
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||||
import 'package:immich_mobile/entities/backup_album.entity.dart';
|
import 'package:immich_mobile/entities/backup_album.entity.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart';
|
|
||||||
import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
|
import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
|
||||||
import 'package:immich_mobile/models/backup/current_upload_asset.model.dart';
|
import 'package:immich_mobile/models/backup/current_upload_asset.model.dart';
|
||||||
import 'package:immich_mobile/models/backup/error_upload_asset.model.dart';
|
import 'package:immich_mobile/models/backup/error_upload_asset.model.dart';
|
||||||
|
|
@ -331,11 +330,16 @@ class BackgroundService {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> _onAssetsChanged() async {
|
Future<bool> _onAssetsChanged() async {
|
||||||
final db = await Bootstrap.initIsar();
|
final (isar, drift, logDb) = await Bootstrap.initDB();
|
||||||
final logDb = DriftLogger();
|
await Bootstrap.initDomain(isar, drift, logDb);
|
||||||
await Bootstrap.initDomain(db, logDb);
|
|
||||||
|
|
||||||
final ref = ProviderContainer(overrides: [dbProvider.overrideWithValue(db), isarProvider.overrideWithValue(db)]);
|
final ref = ProviderContainer(
|
||||||
|
overrides: [
|
||||||
|
dbProvider.overrideWithValue(isar),
|
||||||
|
isarProvider.overrideWithValue(isar),
|
||||||
|
driftProvider.overrideWith(driftOverride(drift)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
HttpSSLOptions.apply();
|
HttpSSLOptions.apply();
|
||||||
ref.read(apiServiceProvider).setAccessToken(Store.get(StoreKey.accessToken));
|
ref.read(apiServiceProvider).setAccessToken(Store.get(StoreKey.accessToken));
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ import 'package:immich_mobile/domain/services/user.service.dart';
|
||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/exif.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/exif.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart';
|
|
||||||
import 'package:immich_mobile/infrastructure/utils/exif.converter.dart';
|
import 'package:immich_mobile/infrastructure/utils/exif.converter.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/exif.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/exif.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
|
||||||
|
|
@ -116,9 +115,8 @@ class BackupVerificationService {
|
||||||
assert(tuple.deleteCandidates.length == tuple.originals.length);
|
assert(tuple.deleteCandidates.length == tuple.originals.length);
|
||||||
final List<Asset> result = [];
|
final List<Asset> result = [];
|
||||||
BackgroundIsolateBinaryMessenger.ensureInitialized(tuple.rootIsolateToken);
|
BackgroundIsolateBinaryMessenger.ensureInitialized(tuple.rootIsolateToken);
|
||||||
final db = await Bootstrap.initIsar();
|
final (isar, drift, logDb) = await Bootstrap.initDB();
|
||||||
final logDb = DriftLogger();
|
await Bootstrap.initDomain(isar, drift, logDb);
|
||||||
await Bootstrap.initDomain(db, logDb);
|
|
||||||
await tuple.fileMediaRepository.enableBackgroundAccess();
|
await tuple.fileMediaRepository.enableBackgroundAccess();
|
||||||
final ApiService apiService = ApiService();
|
final ApiService apiService = ApiService();
|
||||||
apiService.setEndpoint(tuple.endpoint);
|
apiService.setEndpoint(tuple.endpoint);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||||
import 'package:immich_mobile/domain/services/log.service.dart';
|
import 'package:immich_mobile/domain/services/log.service.dart';
|
||||||
import 'package:immich_mobile/domain/services/store.service.dart';
|
import 'package:immich_mobile/domain/services/store.service.dart';
|
||||||
import 'package:immich_mobile/entities/album.entity.dart';
|
import 'package:immich_mobile/entities/album.entity.dart';
|
||||||
|
|
@ -14,6 +15,7 @@ import 'package:immich_mobile/infrastructure/entities/device_asset.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/store.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
|
||||||
|
|
@ -21,18 +23,23 @@ import 'package:isar/isar.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
|
||||||
abstract final class Bootstrap {
|
abstract final class Bootstrap {
|
||||||
static Future<Isar> initIsar() async {
|
static Future<(Isar isar, Drift drift, DriftLogger logDb)> initDB() async {
|
||||||
if (Isar.getInstance() != null) {
|
final drift = Drift();
|
||||||
return Isar.getInstance()!;
|
final logDb = DriftLogger();
|
||||||
|
|
||||||
|
Isar? isar = Isar.getInstance();
|
||||||
|
|
||||||
|
if (isar != null) {
|
||||||
|
return (isar, drift, logDb);
|
||||||
}
|
}
|
||||||
|
|
||||||
final dir = await getApplicationDocumentsDirectory();
|
final dir = await getApplicationDocumentsDirectory();
|
||||||
return await Isar.open(
|
isar = await Isar.open(
|
||||||
[
|
[
|
||||||
StoreValueSchema,
|
StoreValueSchema,
|
||||||
ExifInfoSchema,
|
|
||||||
AssetSchema,
|
AssetSchema,
|
||||||
AlbumSchema,
|
AlbumSchema,
|
||||||
|
ExifInfoSchema,
|
||||||
UserSchema,
|
UserSchema,
|
||||||
BackupAlbumSchema,
|
BackupAlbumSchema,
|
||||||
DuplicatedAssetSchema,
|
DuplicatedAssetSchema,
|
||||||
|
|
@ -45,14 +52,19 @@ abstract final class Bootstrap {
|
||||||
maxSizeMiB: 2048,
|
maxSizeMiB: 2048,
|
||||||
inspector: kDebugMode,
|
inspector: kDebugMode,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return (isar, drift, logDb);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> initDomain(Isar db, DriftLogger logDb, {bool shouldBufferLogs = true}) async {
|
static Future<void> initDomain(Isar db, Drift drift, DriftLogger logDb, {bool shouldBufferLogs = true}) async {
|
||||||
await StoreService.init(storeRepository: IsarStoreRepository(db));
|
final isBeta = await IsarStoreRepository(db).tryGet(StoreKey.betaTimeline) ?? false;
|
||||||
|
final IStoreRepository storeRepo = isBeta ? DriftStoreRepository(drift) : IsarStoreRepository(db);
|
||||||
|
|
||||||
|
await StoreService.init(storeRepository: storeRepo);
|
||||||
|
|
||||||
await LogService.init(
|
await LogService.init(
|
||||||
logRepository: LogRepository(logDb),
|
logRepository: LogRepository(logDb),
|
||||||
storeRepository: IsarStoreRepository(db),
|
storeRepository: storeRepo,
|
||||||
shouldBuffer: shouldBufferLogs,
|
shouldBuffer: shouldBufferLogs,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/domain/services/log.service.dart';
|
import 'package:immich_mobile/domain/services/log.service.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart';
|
|
||||||
import 'package:immich_mobile/providers/db.provider.dart';
|
import 'package:immich_mobile/providers/db.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/cancel.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/cancel.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||||
|
|
@ -35,15 +34,15 @@ Cancelable<T?> runInIsolateGentle<T>({
|
||||||
BackgroundIsolateBinaryMessenger.ensureInitialized(token);
|
BackgroundIsolateBinaryMessenger.ensureInitialized(token);
|
||||||
DartPluginRegistrant.ensureInitialized();
|
DartPluginRegistrant.ensureInitialized();
|
||||||
|
|
||||||
final db = await Bootstrap.initIsar();
|
final (isar, drift, logDb) = await Bootstrap.initDB();
|
||||||
final logDb = DriftLogger();
|
await Bootstrap.initDomain(isar, drift, logDb, shouldBufferLogs: false);
|
||||||
await Bootstrap.initDomain(db, logDb, shouldBufferLogs: false);
|
|
||||||
final ref = ProviderContainer(
|
final ref = ProviderContainer(
|
||||||
overrides: [
|
overrides: [
|
||||||
// TODO: Remove once isar is removed
|
// TODO: Remove once isar is removed
|
||||||
dbProvider.overrideWithValue(db),
|
dbProvider.overrideWithValue(isar),
|
||||||
isarProvider.overrideWithValue(db),
|
isarProvider.overrideWithValue(isar),
|
||||||
cancellationProvider.overrideWithValue(cancelledChecker),
|
cancellationProvider.overrideWithValue(cancelledChecker),
|
||||||
|
driftProvider.overrideWith(driftOverride(drift)),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import 'package:immich_mobile/infrastructure/entities/exif.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart';
|
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart';
|
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/store.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/store.entity.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||||
|
|
@ -30,9 +31,10 @@ import 'package:logging/logging.dart';
|
||||||
// ignore: import_rule_photo_manager
|
// ignore: import_rule_photo_manager
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
|
|
||||||
const int targetVersion = 13;
|
const int targetVersion = 14;
|
||||||
|
|
||||||
Future<void> migrateDatabaseIfNeeded(Isar db) async {
|
Future<void> migrateDatabaseIfNeeded(Isar db, Drift drift) async {
|
||||||
|
final hasVersion = Store.tryGet(StoreKey.version) != null;
|
||||||
final int version = Store.get(StoreKey.version, targetVersion);
|
final int version = Store.get(StoreKey.version, targetVersion);
|
||||||
|
|
||||||
if (version < 9) {
|
if (version < 9) {
|
||||||
|
|
@ -58,6 +60,12 @@ Future<void> migrateDatabaseIfNeeded(Isar db) async {
|
||||||
await Store.put(StoreKey.photoManagerCustomFilter, true);
|
await Store.put(StoreKey.photoManagerCustomFilter, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This means that the SQLite DB is just created and has no version
|
||||||
|
if (version < 14 || !hasVersion) {
|
||||||
|
await migrateStoreToSqlite(db, drift);
|
||||||
|
await Store.populateCache();
|
||||||
|
}
|
||||||
|
|
||||||
if (targetVersion >= 12) {
|
if (targetVersion >= 12) {
|
||||||
await Store.put(StoreKey.version, targetVersion);
|
await Store.put(StoreKey.version, targetVersion);
|
||||||
return;
|
return;
|
||||||
|
|
@ -215,6 +223,39 @@ Future<void> migrateBackupAlbumsToSqlite(Isar db, Drift drift) async {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> migrateStoreToSqlite(Isar db, Drift drift) async {
|
||||||
|
try {
|
||||||
|
final isarStoreValues = await db.storeValues.where().findAll();
|
||||||
|
await drift.batch((batch) {
|
||||||
|
for (final storeValue in isarStoreValues) {
|
||||||
|
final companion = StoreEntityCompanion(
|
||||||
|
id: Value(storeValue.id),
|
||||||
|
stringValue: Value(storeValue.strValue),
|
||||||
|
intValue: Value(storeValue.intValue),
|
||||||
|
);
|
||||||
|
batch.insert(drift.storeEntity, companion, onConflict: DoUpdate((_) => companion));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
debugPrint("[MIGRATION] Error while migrating store values to SQLite: $error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> migrateStoreToIsar(Isar db, Drift drift) async {
|
||||||
|
try {
|
||||||
|
final driftStoreValues = await drift.storeEntity
|
||||||
|
.select()
|
||||||
|
.map((entity) => StoreValue(entity.id, intValue: entity.intValue, strValue: entity.stringValue))
|
||||||
|
.get();
|
||||||
|
|
||||||
|
await db.writeTxn(() async {
|
||||||
|
await db.storeValues.putAll(driftStoreValues);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
debugPrint("[MIGRATION] Error while migrating store values to Isar: $error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _DeviceAsset {
|
class _DeviceAsset {
|
||||||
final String assetId;
|
final String assetId;
|
||||||
final List<int>? hash;
|
final List<int>? hash;
|
||||||
|
|
|
||||||
|
|
@ -89,7 +89,6 @@ class _BetaTimelineListTileState extends ConsumerState<BetaTimelineListTile> wit
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
await ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.betaTimeline, value);
|
|
||||||
context.router.replaceAll([ChangeExperienceRoute(switchingToBeta: value)]);
|
context.router.replaceAll([ChangeExperienceRoute(switchingToBeta: value)]);
|
||||||
},
|
},
|
||||||
child: Text("ok".t(context: context)),
|
child: Text("ok".t(context: context)),
|
||||||
|
|
|
||||||
|
|
@ -64,12 +64,12 @@ void main() {
|
||||||
|
|
||||||
group("Log Service Set Level:", () {
|
group("Log Service Set Level:", () {
|
||||||
setUp(() async {
|
setUp(() async {
|
||||||
when(() => mockStoreRepo.insert<int>(StoreKey.logLevel, any())).thenAnswer((_) async => true);
|
when(() => mockStoreRepo.upsert<int>(StoreKey.logLevel, any())).thenAnswer((_) async => true);
|
||||||
await sut.setLogLevel(LogLevel.shout);
|
await sut.setLogLevel(LogLevel.shout);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Updates the log level in store', () {
|
test('Updates the log level in store', () {
|
||||||
final index = verify(() => mockStoreRepo.insert<int>(StoreKey.logLevel, captureAny())).captured.firstOrNull;
|
final index = verify(() => mockStoreRepo.upsert<int>(StoreKey.logLevel, captureAny())).captured.firstOrNull;
|
||||||
expect(index, LogLevel.shout.index);
|
expect(index, LogLevel.shout.index);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,11 +16,13 @@ final _kBackupFailedSince = DateTime.utc(2023);
|
||||||
void main() {
|
void main() {
|
||||||
late StoreService sut;
|
late StoreService sut;
|
||||||
late IsarStoreRepository mockStoreRepo;
|
late IsarStoreRepository mockStoreRepo;
|
||||||
late StreamController<StoreDto<Object>> controller;
|
late DriftStoreRepository mockDriftStoreRepo;
|
||||||
|
late StreamController<List<StoreDto<Object>>> controller;
|
||||||
|
|
||||||
setUp(() async {
|
setUp(() async {
|
||||||
controller = StreamController<StoreDto<Object>>.broadcast();
|
controller = StreamController<List<StoreDto<Object>>>.broadcast();
|
||||||
mockStoreRepo = MockStoreRepository();
|
mockStoreRepo = MockStoreRepository();
|
||||||
|
mockDriftStoreRepo = MockDriftStoreRepository();
|
||||||
// For generics, we need to provide fallback to each concrete type to avoid runtime errors
|
// For generics, we need to provide fallback to each concrete type to avoid runtime errors
|
||||||
registerFallbackValue(StoreKey.accessToken);
|
registerFallbackValue(StoreKey.accessToken);
|
||||||
registerFallbackValue(StoreKey.backupTriggerDelay);
|
registerFallbackValue(StoreKey.backupTriggerDelay);
|
||||||
|
|
@ -37,6 +39,16 @@ void main() {
|
||||||
);
|
);
|
||||||
when(() => mockStoreRepo.watchAll()).thenAnswer((_) => controller.stream);
|
when(() => mockStoreRepo.watchAll()).thenAnswer((_) => controller.stream);
|
||||||
|
|
||||||
|
when(() => mockDriftStoreRepo.getAll()).thenAnswer(
|
||||||
|
(_) async => [
|
||||||
|
const StoreDto(StoreKey.accessToken, _kAccessToken),
|
||||||
|
const StoreDto(StoreKey.backgroundBackup, _kBackgroundBackup),
|
||||||
|
const StoreDto(StoreKey.groupAssetsBy, _kGroupAssetsBy),
|
||||||
|
StoreDto(StoreKey.backupFailedSince, _kBackupFailedSince),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
when(() => mockDriftStoreRepo.watchAll()).thenAnswer((_) => controller.stream);
|
||||||
|
|
||||||
sut = await StoreService.create(storeRepository: mockStoreRepo);
|
sut = await StoreService.create(storeRepository: mockStoreRepo);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -58,7 +70,7 @@ void main() {
|
||||||
|
|
||||||
test('Listens to stream of store updates', () async {
|
test('Listens to stream of store updates', () async {
|
||||||
final event = StoreDto(StoreKey.accessToken, _kAccessToken.toUpperCase());
|
final event = StoreDto(StoreKey.accessToken, _kAccessToken.toUpperCase());
|
||||||
controller.add(event);
|
controller.add([event]);
|
||||||
|
|
||||||
await pumpEventQueue();
|
await pumpEventQueue();
|
||||||
|
|
||||||
|
|
@ -83,18 +95,19 @@ void main() {
|
||||||
|
|
||||||
group('Store Service put:', () {
|
group('Store Service put:', () {
|
||||||
setUp(() {
|
setUp(() {
|
||||||
when(() => mockStoreRepo.insert<String>(any<StoreKey<String>>(), any())).thenAnswer((_) async => true);
|
when(() => mockStoreRepo.upsert<String>(any<StoreKey<String>>(), any())).thenAnswer((_) async => true);
|
||||||
|
when(() => mockDriftStoreRepo.upsert<String>(any<StoreKey<String>>(), any())).thenAnswer((_) async => true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Skip insert when value is not modified', () async {
|
test('Skip insert when value is not modified', () async {
|
||||||
await sut.put(StoreKey.accessToken, _kAccessToken);
|
await sut.put(StoreKey.accessToken, _kAccessToken);
|
||||||
verifyNever(() => mockStoreRepo.insert<String>(StoreKey.accessToken, any()));
|
verifyNever(() => mockStoreRepo.upsert<String>(StoreKey.accessToken, any()));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Insert value when modified', () async {
|
test('Insert value when modified', () async {
|
||||||
final newAccessToken = _kAccessToken.toUpperCase();
|
final newAccessToken = _kAccessToken.toUpperCase();
|
||||||
await sut.put(StoreKey.accessToken, newAccessToken);
|
await sut.put(StoreKey.accessToken, newAccessToken);
|
||||||
verify(() => mockStoreRepo.insert<String>(StoreKey.accessToken, newAccessToken)).called(1);
|
verify(() => mockStoreRepo.upsert<String>(StoreKey.accessToken, newAccessToken)).called(1);
|
||||||
expect(sut.tryGet(StoreKey.accessToken), newAccessToken);
|
expect(sut.tryGet(StoreKey.accessToken), newAccessToken);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -105,6 +118,7 @@ void main() {
|
||||||
setUp(() {
|
setUp(() {
|
||||||
valueController = StreamController<String?>.broadcast();
|
valueController = StreamController<String?>.broadcast();
|
||||||
when(() => mockStoreRepo.watch<String>(any<StoreKey<String>>())).thenAnswer((_) => valueController.stream);
|
when(() => mockStoreRepo.watch<String>(any<StoreKey<String>>())).thenAnswer((_) => valueController.stream);
|
||||||
|
when(() => mockDriftStoreRepo.watch<String>(any<StoreKey<String>>())).thenAnswer((_) => valueController.stream);
|
||||||
});
|
});
|
||||||
|
|
||||||
tearDown(() async {
|
tearDown(() async {
|
||||||
|
|
@ -129,6 +143,7 @@ void main() {
|
||||||
group('Store Service delete:', () {
|
group('Store Service delete:', () {
|
||||||
setUp(() {
|
setUp(() {
|
||||||
when(() => mockStoreRepo.delete<String>(any<StoreKey<String>>())).thenAnswer((_) async => true);
|
when(() => mockStoreRepo.delete<String>(any<StoreKey<String>>())).thenAnswer((_) async => true);
|
||||||
|
when(() => mockDriftStoreRepo.delete<String>(any<StoreKey<String>>())).thenAnswer((_) async => true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Removes the value from the DB', () async {
|
test('Removes the value from the DB', () async {
|
||||||
|
|
@ -145,6 +160,7 @@ void main() {
|
||||||
group('Store Service clear:', () {
|
group('Store Service clear:', () {
|
||||||
setUp(() {
|
setUp(() {
|
||||||
when(() => mockStoreRepo.deleteAll()).thenAnswer((_) async => true);
|
when(() => mockStoreRepo.deleteAll()).thenAnswer((_) async => true);
|
||||||
|
when(() => mockDriftStoreRepo.deleteAll()).thenAnswer((_) async => true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Clears all values from the store', () async {
|
test('Clears all values from the store', () async {
|
||||||
|
|
|
||||||
BIN
mobile/test/drift/main/generated/schema.dart
generated
BIN
mobile/test/drift/main/generated/schema.dart
generated
Binary file not shown.
BIN
mobile/test/drift/main/generated/schema_v8.dart
generated
Normal file
BIN
mobile/test/drift/main/generated/schema_v8.dart
generated
Normal file
Binary file not shown.
|
|
@ -44,7 +44,7 @@ void main() {
|
||||||
test('converts int', () async {
|
test('converts int', () async {
|
||||||
int? version = await sut.tryGet(StoreKey.version);
|
int? version = await sut.tryGet(StoreKey.version);
|
||||||
expect(version, isNull);
|
expect(version, isNull);
|
||||||
await sut.insert(StoreKey.version, _kTestVersion);
|
await sut.upsert(StoreKey.version, _kTestVersion);
|
||||||
version = await sut.tryGet(StoreKey.version);
|
version = await sut.tryGet(StoreKey.version);
|
||||||
expect(version, _kTestVersion);
|
expect(version, _kTestVersion);
|
||||||
});
|
});
|
||||||
|
|
@ -52,7 +52,7 @@ void main() {
|
||||||
test('converts string', () async {
|
test('converts string', () async {
|
||||||
String? accessToken = await sut.tryGet(StoreKey.accessToken);
|
String? accessToken = await sut.tryGet(StoreKey.accessToken);
|
||||||
expect(accessToken, isNull);
|
expect(accessToken, isNull);
|
||||||
await sut.insert(StoreKey.accessToken, _kTestAccessToken);
|
await sut.upsert(StoreKey.accessToken, _kTestAccessToken);
|
||||||
accessToken = await sut.tryGet(StoreKey.accessToken);
|
accessToken = await sut.tryGet(StoreKey.accessToken);
|
||||||
expect(accessToken, _kTestAccessToken);
|
expect(accessToken, _kTestAccessToken);
|
||||||
});
|
});
|
||||||
|
|
@ -60,7 +60,7 @@ void main() {
|
||||||
test('converts datetime', () async {
|
test('converts datetime', () async {
|
||||||
DateTime? backupFailedSince = await sut.tryGet(StoreKey.backupFailedSince);
|
DateTime? backupFailedSince = await sut.tryGet(StoreKey.backupFailedSince);
|
||||||
expect(backupFailedSince, isNull);
|
expect(backupFailedSince, isNull);
|
||||||
await sut.insert(StoreKey.backupFailedSince, _kTestBackupFailed);
|
await sut.upsert(StoreKey.backupFailedSince, _kTestBackupFailed);
|
||||||
backupFailedSince = await sut.tryGet(StoreKey.backupFailedSince);
|
backupFailedSince = await sut.tryGet(StoreKey.backupFailedSince);
|
||||||
expect(backupFailedSince, _kTestBackupFailed);
|
expect(backupFailedSince, _kTestBackupFailed);
|
||||||
});
|
});
|
||||||
|
|
@ -68,7 +68,7 @@ void main() {
|
||||||
test('converts bool', () async {
|
test('converts bool', () async {
|
||||||
bool? colorfulInterface = await sut.tryGet(StoreKey.colorfulInterface);
|
bool? colorfulInterface = await sut.tryGet(StoreKey.colorfulInterface);
|
||||||
expect(colorfulInterface, isNull);
|
expect(colorfulInterface, isNull);
|
||||||
await sut.insert(StoreKey.colorfulInterface, _kTestColorfulInterface);
|
await sut.upsert(StoreKey.colorfulInterface, _kTestColorfulInterface);
|
||||||
colorfulInterface = await sut.tryGet(StoreKey.colorfulInterface);
|
colorfulInterface = await sut.tryGet(StoreKey.colorfulInterface);
|
||||||
expect(colorfulInterface, _kTestColorfulInterface);
|
expect(colorfulInterface, _kTestColorfulInterface);
|
||||||
});
|
});
|
||||||
|
|
@ -76,7 +76,7 @@ void main() {
|
||||||
test('converts user', () async {
|
test('converts user', () async {
|
||||||
UserDto? user = await sut.tryGet(StoreKey.currentUser);
|
UserDto? user = await sut.tryGet(StoreKey.currentUser);
|
||||||
expect(user, isNull);
|
expect(user, isNull);
|
||||||
await sut.insert(StoreKey.currentUser, _kTestUser);
|
await sut.upsert(StoreKey.currentUser, _kTestUser);
|
||||||
user = await sut.tryGet(StoreKey.currentUser);
|
user = await sut.tryGet(StoreKey.currentUser);
|
||||||
expect(user, _kTestUser);
|
expect(user, _kTestUser);
|
||||||
});
|
});
|
||||||
|
|
@ -108,10 +108,10 @@ void main() {
|
||||||
await _populateStore(db);
|
await _populateStore(db);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('update()', () async {
|
test('upsert()', () async {
|
||||||
int? version = await sut.tryGet(StoreKey.version);
|
int? version = await sut.tryGet(StoreKey.version);
|
||||||
expect(version, _kTestVersion);
|
expect(version, _kTestVersion);
|
||||||
await sut.update(StoreKey.version, _kTestVersion + 10);
|
await sut.upsert(StoreKey.version, _kTestVersion + 10);
|
||||||
version = await sut.tryGet(StoreKey.version);
|
version = await sut.tryGet(StoreKey.version);
|
||||||
expect(version, _kTestVersion + 10);
|
expect(version, _kTestVersion + 10);
|
||||||
});
|
});
|
||||||
|
|
@ -126,22 +126,29 @@ void main() {
|
||||||
final stream = sut.watch(StoreKey.version);
|
final stream = sut.watch(StoreKey.version);
|
||||||
expectLater(stream, emitsInOrder([_kTestVersion, _kTestVersion + 10]));
|
expectLater(stream, emitsInOrder([_kTestVersion, _kTestVersion + 10]));
|
||||||
await pumpEventQueue();
|
await pumpEventQueue();
|
||||||
await sut.update(StoreKey.version, _kTestVersion + 10);
|
await sut.upsert(StoreKey.version, _kTestVersion + 10);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('watchAll()', () async {
|
test('watchAll()', () async {
|
||||||
final stream = sut.watchAll();
|
final stream = sut.watchAll();
|
||||||
expectLater(
|
expectLater(
|
||||||
stream,
|
stream,
|
||||||
emitsInAnyOrder([
|
emitsInOrder([
|
||||||
emits(const StoreDto<Object>(StoreKey.version, _kTestVersion)),
|
[
|
||||||
emits(StoreDto<Object>(StoreKey.backupFailedSince, _kTestBackupFailed)),
|
const StoreDto<Object>(StoreKey.version, _kTestVersion),
|
||||||
emits(const StoreDto<Object>(StoreKey.accessToken, _kTestAccessToken)),
|
StoreDto<Object>(StoreKey.backupFailedSince, _kTestBackupFailed),
|
||||||
emits(const StoreDto<Object>(StoreKey.colorfulInterface, _kTestColorfulInterface)),
|
const StoreDto<Object>(StoreKey.accessToken, _kTestAccessToken),
|
||||||
emits(const StoreDto<Object>(StoreKey.version, _kTestVersion + 10)),
|
const StoreDto<Object>(StoreKey.colorfulInterface, _kTestColorfulInterface),
|
||||||
|
],
|
||||||
|
[
|
||||||
|
const StoreDto<Object>(StoreKey.version, _kTestVersion + 10),
|
||||||
|
StoreDto<Object>(StoreKey.backupFailedSince, _kTestBackupFailed),
|
||||||
|
const StoreDto<Object>(StoreKey.accessToken, _kTestAccessToken),
|
||||||
|
const StoreDto<Object>(StoreKey.colorfulInterface, _kTestColorfulInterface),
|
||||||
|
],
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
await sut.update(StoreKey.version, _kTestVersion + 10);
|
await sut.upsert(StoreKey.version, _kTestVersion + 10);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ import 'package:mocktail/mocktail.dart';
|
||||||
|
|
||||||
class MockStoreRepository extends Mock implements IsarStoreRepository {}
|
class MockStoreRepository extends Mock implements IsarStoreRepository {}
|
||||||
|
|
||||||
|
class MockDriftStoreRepository extends Mock implements DriftStoreRepository {}
|
||||||
|
|
||||||
class MockLogRepository extends Mock implements LogRepository {}
|
class MockLogRepository extends Mock implements LogRepository {}
|
||||||
|
|
||||||
class MockIsarUserRepository extends Mock implements IsarUserRepository {}
|
class MockIsarUserRepository extends Mock implements IsarUserRepository {}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue