diff --git a/mobile/integration_test/background_sync_teardown_test.dart b/mobile/integration_test/background_sync_teardown_test.dart index aa0a8a9a8a..fb90ff4821 100644 --- a/mobile/integration_test/background_sync_teardown_test.dart +++ b/mobile/integration_test/background_sync_teardown_test.dart @@ -1,6 +1,6 @@ import 'dart:async'; -import 'package:drift/drift.dart' show Value; +import 'package:drift/drift.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:immich_mobile/domain/utils/background_sync.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'; diff --git a/mobile/integration_test/test_utils/general_helper.dart b/mobile/integration_test/test_utils/general_helper.dart index 66955364f3..1278608cc2 100644 --- a/mobile/integration_test/test_utils/general_helper.dart +++ b/mobile/integration_test/test_utils/general_helper.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/infrastructure/store.dart'; import 'package:immich_mobile/main.dart' as app; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/utils/bootstrap.dart'; diff --git a/mobile/lib/domain/models/store.model.dart b/mobile/lib/domain/models/store.model.dart deleted file mode 100644 index 787c1f4041..0000000000 --- a/mobile/lib/domain/models/store.model.dart +++ /dev/null @@ -1,67 +0,0 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'store.model.freezed.dart'; - -/// Key for each possible value in the `Store`. -/// Defines the data type for each value -enum StoreKey { - deviceId._(4), - - // Legacy keys that have been migrated to the new metadata store - legacyVersion._(0), - legacyManageLocalMediaAndroid._(137), - legacySyncMigrationStatus._(1013), - legacyAdvancedTroubleshooting._(114), - legacyEnableHapticFeedback._(126), - legacyReadonlyModeEnabled._(138), - legacyServerUrl._(10), - legacyAccessToken._(11), - legacyServerEndpoint._(12), - legacyBackupRequireCharging._(7), - legacyBackupTriggerDelay._(8), - legacySyncAlbums._(131), - legacyEnableBackup._(1003), - legacyUseWifiForUploadVideos._(1004), - legacyUseWifiForUploadPhotos._(1005), - legacySelectedAlbumSortOrder._(113), - legacySelectedAlbumSortReverse._(123), - legacyAlbumGridView._(140), - legacyAutoEndpointSwitching._(132), - legacyPreferredWifiName._(133), - legacyLocalEndpoint._(134), - legacyExternalEndpointList._(135), - legacyCustomHeaders._(127), - legacyLoopVideo._(117), - legacyLoadOriginalVideo._(136), - legacyAutoPlayVideo._(139), - legacyTapToNavigate._(141), - legacyPreferRemoteImage._(116), - legacyLoadOriginal._(101), - legacyPrimaryColor._(128), - legacyDynamicTheme._(129), - legacyColorfulInterface._(130), - legacyThemeMode._(102), - legacyCleanupKeepFavorites._(1008), - legacyCleanupKeepMediaType._(1009), - legacyCleanupKeepAlbumIds._(1010), - legacyCleanupCutoffDaysAgo._(1011), - legacyCleanupDefaultsInitialized._(1012), - legacyTilesPerRow._(103), - legacyGroupAssetsBy._(105), - legacyStorageIndicator._(109), - legacyMapRelativeDate._(119), - legacyMapShowFavoriteOnly._(118), - legacyMapIncludeArchived._(121), - legacyMapThemeMode._(124), - legacyMapwithPartners._(125), - legacyLogLevel._(115); - - const StoreKey._(this.id); - final int id; - Type get type => T; -} - -@freezed -abstract class StoreDto with _$StoreDto { - const factory StoreDto(StoreKey key, T? value) = _StoreDto; -} diff --git a/mobile/lib/domain/services/background_worker.service.dart b/mobile/lib/domain/services/background_worker.service.dart index 5a8357ac44..b5ccdf286f 100644 --- a/mobile/lib/domain/services/background_worker.service.dart +++ b/mobile/lib/domain/services/background_worker.service.dart @@ -10,11 +10,11 @@ import 'package:immich_mobile/domain/services/hash.service.dart'; import 'package:immich_mobile/domain/services/local_sync.service.dart'; import 'package:immich_mobile/domain/services/log.service.dart'; import 'package:immich_mobile/domain/services/sync_stream.service.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; +import 'package:immich_mobile/infrastructure/store.dart'; import 'package:immich_mobile/platform/background_worker_api.g.dart'; import 'package:immich_mobile/platform/background_worker_lock_api.g.dart'; import 'package:immich_mobile/providers/api.provider.dart'; @@ -373,6 +373,6 @@ Future backgroundSyncNativeEntrypoint() async { WidgetsFlutterBinding.ensureInitialized(); DartPluginRegistrant.ensureInitialized(); - final (drift, logDB) = await Bootstrap.initDomain(shouldBufferLogs: false, listenStoreUpdates: false); + final (drift, logDB) = await Bootstrap.initDomain(shouldBufferLogs: false); await BackgroundWorkerBgService(drift: drift, driftLogger: logDB).init(); } diff --git a/mobile/lib/domain/services/store.service.dart b/mobile/lib/domain/services/store.service.dart deleted file mode 100644 index ea830dc322..0000000000 --- a/mobile/lib/domain/services/store.service.dart +++ /dev/null @@ -1,110 +0,0 @@ -import 'dart:async'; - -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; - -/// Provides access to a persistent key-value store with an in-memory cache. -/// Listens for repository changes to keep the cache updated. -class StoreService { - final StoreRepository _storeRepository; - - /// In-memory cache. Keys are [StoreKey.id] - final Map _cache = {}; - StreamSubscription>? _storeUpdateSubscription; - - StoreService._({required StoreRepository isarStoreRepository}) : _storeRepository = isarStoreRepository; - - // TODO: Temporary typedef to make minimal changes. Remove this and make the presentation layer access store through a provider - static StoreService? _instance; - static StoreService get I { - if (_instance == null) { - throw UnsupportedError("StoreService not initialized. Call init() first"); - } - return _instance!; - } - - // TODO: Replace the implementation with the one from create after removing the typedef - static Future init({required StoreRepository storeRepository, bool listenUpdates = true}) async { - _instance ??= await create(storeRepository: storeRepository, listenUpdates: listenUpdates); - return _instance!; - } - - static Future create({required StoreRepository storeRepository, bool listenUpdates = true}) async { - final instance = StoreService._(isarStoreRepository: storeRepository); - await instance.populateCache(); - if (listenUpdates) { - instance._storeUpdateSubscription = instance._listenForChange(); - } - return instance; - } - - Future populateCache() async { - final storeValues = await _storeRepository.getAll(); - for (StoreDto storeValue in storeValues) { - _cache[storeValue.key.id] = storeValue.value; - } - } - - StreamSubscription> _listenForChange() => _storeRepository.watchAll().listen((events) { - for (final event in events) { - _cache[event.key.id] = event.value; - } - }); - - /// Disposes the store and cancels the subscription. To reuse the store call init() again - Future dispose() async { - await _storeUpdateSubscription?.cancel(); - _storeUpdateSubscription = null; - _cache.clear(); - // Allow a subsequent init() (e.g. when a worker isolate is reused) to - // create a fresh instance instead of returning this disposed one. - if (identical(_instance, this)) { - _instance = null; - } - } - - /// Returns the cached value for [key], or `null` - T? tryGet(StoreKey key) => _cache[key.id] as T?; - - /// Returns the stored value for [key] or [defaultValue]. - /// Throws [StoreKeyNotFoundException] if value and [defaultValue] are null. - T get(StoreKey key, [T? defaultValue]) { - final value = tryGet(key) ?? defaultValue; - if (value == null) { - throw StoreKeyNotFoundException(key); - } - return value; - } - - /// Stores the [value] for the [key]. Skips write if value hasn't changed. - Future put, T>(U key, T value) async { - if (_cache[key.id] == value) { - return; - } - await _storeRepository.upsert(key, value); - _cache[key.id] = value; - } - - /// Returns a stream that emits the value for [key] on change. - Stream watch(StoreKey key) => _storeRepository.watch(key); - - /// Removes the value for [key] - Future delete(StoreKey key) async { - await _storeRepository.deleteValue(key); - _cache.remove(key.id); - } - - /// Clears all values from the store (cache and DB) - Future clear() async { - await _storeRepository.deleteAll(); - _cache.clear(); - } -} - -class StoreKeyNotFoundException implements Exception { - final StoreKey key; - const StoreKeyNotFoundException(this.key); - - @override - String toString() => "Key - <${key.name}> not available in Store"; -} diff --git a/mobile/lib/entities/store.entity.dart b/mobile/lib/entities/store.entity.dart deleted file mode 100644 index 17ad88cee9..0000000000 --- a/mobile/lib/entities/store.entity.dart +++ /dev/null @@ -1,4 +0,0 @@ -import 'package:immich_mobile/domain/services/store.service.dart'; - -// ignore: non_constant_identifier_names -final Store = StoreService.I; diff --git a/mobile/lib/infrastructure/repositories/app_metadata.repository.dart b/mobile/lib/infrastructure/repositories/app_metadata.repository.dart index 7b213223c2..cb17b67590 100644 --- a/mobile/lib/infrastructure/repositories/app_metadata.repository.dart +++ b/mobile/lib/infrastructure/repositories/app_metadata.repository.dart @@ -22,8 +22,8 @@ class AppMetadataRepository extends DatabaseAccessor with $AppMetadataRep .insertOnConflictUpdate( AppMetadataEntityCompanion.insert( key: key.name, - value: Value(key.encode(value)), - updatedAt: Value(DateTime.now()), + value: .new(key.encode(value)), + updatedAt: .new(DateTime.now()), ), ); } diff --git a/mobile/lib/infrastructure/repositories/store.repository.dart b/mobile/lib/infrastructure/repositories/store.repository.dart deleted file mode 100644 index b4e2a7378f..0000000000 --- a/mobile/lib/infrastructure/repositories/store.repository.dart +++ /dev/null @@ -1,81 +0,0 @@ -import 'package:drift/drift.dart'; -import 'package:immich_mobile/domain/models/store.model.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/store.repository.drift.dart'; - -@DriftAccessor() -class StoreRepository extends DatabaseAccessor with $StoreRepositoryMixin { - final validStoreKeys = StoreKey.values.map((e) => e.id).toSet(); - - StoreRepository(super.attachedDatabase); - - Drift get _db => attachedDatabase; - - Future deleteAll() async { - await _db.storeEntity.deleteAll(); - return true; - } - - Future>> getAll() async { - final query = _db.storeEntity.select()..where((entity) => entity.id.isIn(validStoreKeys)); - return query.asyncMap((entity) => _toUpdateEvent(entity)).get(); - } - - Stream>> watchAll() { - final query = _db.storeEntity.select()..where((entity) => entity.id.isIn(validStoreKeys)); - - return query.asyncMap((entity) => _toUpdateEvent(entity)).watch(); - } - - Future deleteValue(StoreKey key) async { - await _db.storeEntity.deleteWhere((entity) => entity.id.equals(key.id)); - return; - } - - Future upsert(StoreKey key, T value) async { - await _db.storeEntity.insertOnConflictUpdate(await _fromValue(key, value)); - return true; - } - - Future tryGet(StoreKey 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); - } - - Stream watch(StoreKey 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> _toUpdateEvent(StoreEntityData entity) async { - final key = StoreKey.values.firstWhere((e) => e.id == entity.id) as StoreKey; - final value = await _toValue(key, entity); - return StoreDto(key, value); - } - - Future _toValue(StoreKey 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!), - _ => null, - } - as T?; - - Future _fromValue(StoreKey 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), - _ => throw UnsupportedError("Unsupported primitive type: ${key.type} for key: ${key.name}"), - }; - return StoreEntityCompanion(id: Value(key.id), intValue: Value(intValue), stringValue: Value(strValue)); - } -} diff --git a/mobile/lib/infrastructure/store.dart b/mobile/lib/infrastructure/store.dart new file mode 100644 index 0000000000..e368b8ad71 --- /dev/null +++ b/mobile/lib/infrastructure/store.dart @@ -0,0 +1,105 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; + +enum LegacyStoreKey { + deviceId(4), + legacyVersion(0), + legacyManageLocalMediaAndroid(137), + legacySyncMigrationStatus(1013), + legacyAdvancedTroubleshooting(114), + legacyEnableHapticFeedback(126), + legacyReadonlyModeEnabled(138), + legacyServerUrl(10), + legacyAccessToken(11), + legacyServerEndpoint(12), + legacyBackupRequireCharging(7), + legacyBackupTriggerDelay(8), + legacySyncAlbums(131), + legacyEnableBackup(1003), + legacyUseWifiForUploadVideos(1004), + legacyUseWifiForUploadPhotos(1005), + legacySelectedAlbumSortOrder(113), + legacySelectedAlbumSortReverse(123), + legacyAlbumGridView(140), + legacyAutoEndpointSwitching(132), + legacyPreferredWifiName(133), + legacyLocalEndpoint(134), + legacyExternalEndpointList(135), + legacyCustomHeaders(127), + legacyLoopVideo(117), + legacyLoadOriginalVideo(136), + legacyAutoPlayVideo(139), + legacyTapToNavigate(141), + legacyPreferRemoteImage(116), + legacyLoadOriginal(101), + legacyPrimaryColor(128), + legacyDynamicTheme(129), + legacyColorfulInterface(130), + legacyThemeMode(102), + legacyCleanupKeepFavorites(1008), + legacyCleanupKeepMediaType(1009), + legacyCleanupKeepAlbumIds(1010), + legacyCleanupCutoffDaysAgo(1011), + legacyCleanupDefaultsInitialized(1012), + legacyTilesPerRow(103), + legacyGroupAssetsBy(105), + legacyStorageIndicator(109), + legacyMapRelativeDate(119), + legacyMapShowFavoriteOnly(118), + legacyMapIncludeArchived(121), + legacyMapThemeMode(124), + legacyMapwithPartners(125), + legacyLogLevel(115); + + const LegacyStoreKey(this.id); + + final int id; +} + +class DeviceIdStore { + DeviceIdStore._(this._db); + + final Drift _db; + String? _deviceId; + + static DeviceIdStore? _instance; + static DeviceIdStore get I => _instance ?? (throw StateError('DeviceIdStore not initialized. Call init() first')); + + static Future init(Drift db) async { + final instance = DeviceIdStore._(db); + final row = await (db.storeEntity.select()..where((t) => t.id.equals(LegacyStoreKey.deviceId.id))) + .getSingleOrNull(); + instance._deviceId = row?.stringValue; + return _instance = instance; + } + + String? get deviceId => _deviceId; + + String get requireDeviceId => _deviceId ?? (throw StateError('deviceId not set')); + + Future setDeviceId(String value) async { + if (_deviceId == value) { + return; + } + await _db.storeEntity.insertOnConflictUpdate( + StoreEntityCompanion(id: Value(LegacyStoreKey.deviceId.id), stringValue: Value(value)), + ); + _deviceId = value; + } + + Future clear() async { + await _db.storeEntity.deleteAll(); + _deviceId = null; + } + + Future dispose() async { + _deviceId = null; + if (identical(_instance, this)) { + _instance = null; + } + } +} + +// ignore: non_constant_identifier_names +DeviceIdStore get Store => DeviceIdStore.I; diff --git a/mobile/lib/providers/auth.provider.dart b/mobile/lib/providers/auth.provider.dart index fca71e3b58..d85d2aa45c 100644 --- a/mobile/lib/providers/auth.provider.dart +++ b/mobile/lib/providers/auth.provider.dart @@ -4,10 +4,9 @@ import 'package:flutter_udid/flutter_udid.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/domain/models/session.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/services/user.service.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/infrastructure/store.dart'; import 'package:immich_mobile/models/auth/auth_state.model.dart'; import 'package:immich_mobile/models/auth/login_response.model.dart'; import 'package:immich_mobile/providers/api.provider.dart'; @@ -136,7 +135,7 @@ class AuthNotifier extends StateNotifier { await _widgetService.writeCredentials(serverEndpoint, accessToken, customHeaders); // Get the deviceid from the store if it exists, otherwise generate a new one - final String deviceId = Store.tryGet(StoreKey.deviceId) ?? await FlutterUdid.consistentUdid; + final deviceId = Store.deviceId ?? await FlutterUdid.consistentUdid; UserDto? user = await _userService.tryGetMyUser(); @@ -148,7 +147,7 @@ class AuthNotifier extends StateNotifier { // If the user information is successfully retrieved, update the store // Due to the flow of the code, this will always happen on first login user = serverUser; - await Store.put(StoreKey.deviceId, deviceId); + await Store.setDeviceId(deviceId); } } on ApiException catch (error, stackTrace) { if (error.code == 401) { diff --git a/mobile/lib/providers/infrastructure/store.provider.dart b/mobile/lib/providers/infrastructure/store.provider.dart deleted file mode 100644 index ba4d045b06..0000000000 --- a/mobile/lib/providers/infrastructure/store.provider.dart +++ /dev/null @@ -1,4 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/services/store.service.dart'; - -final storeServiceProvider = Provider((_) => StoreService.I); diff --git a/mobile/lib/services/background_upload.service.dart b/mobile/lib/services/background_upload.service.dart index 841c7fe77d..cb91dff59d 100644 --- a/mobile/lib/services/background_upload.service.dart +++ b/mobile/lib/services/background_upload.service.dart @@ -9,15 +9,14 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/domain/models/asset/asset_metadata.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/infrastructure/repositories/backup.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/session.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart'; -import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; +import 'package:immich_mobile/infrastructure/store.dart'; +import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/storage.provider.dart'; import 'package:immich_mobile/repositories/asset_media.repository.dart'; import 'package:immich_mobile/repositories/upload.repository.dart'; @@ -369,7 +368,7 @@ class BackgroundUploadService { final serverEndpoint = SessionRepository.instance.session.serverEndpoint!; final url = Uri.parse('$serverEndpoint/assets').toString(); final headers = ApiService.getRequestHeaders(); - final deviceId = Store.get(StoreKey.deviceId); + final deviceId = Store.requireDeviceId; final (baseDirectory, directory, filename) = await Task.split(filePath: file.path); final fieldsMap = { 'filename': originalFileName ?? filename, diff --git a/mobile/lib/services/foreground_upload.service.dart b/mobile/lib/services/foreground_upload.service.dart index ac42e2482f..75036496e9 100644 --- a/mobile/lib/services/foreground_upload.service.dart +++ b/mobile/lib/services/foreground_upload.service.dart @@ -6,14 +6,13 @@ import 'package:flutter/foundation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/asset_metadata.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart' hide AssetVisibility; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/network_capability_extensions.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/generated/translations.g.dart'; import 'package:immich_mobile/infrastructure/repositories/backup.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart'; +import 'package:immich_mobile/infrastructure/store.dart'; import 'package:immich_mobile/platform/connectivity_api.g.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; @@ -310,7 +309,7 @@ class ForegroundUploadService { // Some apps (e.g. DJI/Fusion) return names without an extension; fall back to the asset name for those. final extension = p.extension(file.path).isNotEmpty ? p.extension(file.path) : p.extension(asset.name); final originalFileName = p.setExtension(fileName, extension); - final deviceId = Store.get(StoreKey.deviceId); + final deviceId = Store.deviceId!; final fields = { // deviceAssetId/deviceId required by server v2.7.5 and below (drop in v4.0 per #27818). @@ -424,7 +423,7 @@ class ForegroundUploadService { final fields = { // deviceAssetId/deviceId required by server v2.7.5 and below (drop in v4.0 per #27818). 'deviceAssetId': deviceAssetId, - 'deviceId': Store.get(StoreKey.deviceId), + 'deviceId': Store.requireDeviceId, 'fileCreatedAt': fileCreatedAt.toUtc().toIso8601String(), 'fileModifiedAt': fileModifiedAt.toUtc().toIso8601String(), 'isFavorite': 'false', diff --git a/mobile/lib/utils/bootstrap.dart b/mobile/lib/utils/bootstrap.dart index 13ab6836b5..c8bea7e97a 100644 --- a/mobile/lib/utils/bootstrap.dart +++ b/mobile/lib/utils/bootstrap.dart @@ -1,15 +1,14 @@ import 'package:background_downloader/background_downloader.dart'; import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/domain/services/log.service.dart'; -import 'package:immich_mobile/domain/services/store.service.dart'; -import 'package:immich_mobile/generated/translations.g.dart'; +import 'package:immich_mobile/extensions/translate_extensions.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/logger_db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/network.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/session.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; +import 'package:immich_mobile/infrastructure/store.dart'; import 'package:immich_mobile/utils/debug_print.dart'; import 'package:logging/logging.dart'; import 'package:photo_manager/photo_manager.dart'; @@ -48,13 +47,11 @@ void configureFileDownloaderNotifications() { } abstract final class Bootstrap { - static Future<(Drift, DriftLogger)> initDomain({bool listenStoreUpdates = true, bool shouldBufferLogs = true}) async { + static Future<(Drift, DriftLogger)> initDomain({bool shouldBufferLogs = true}) async { await configureSqliteCache(); final (db, updatePool) = await openSqliteConnectionWithUpdatePool(name: 'immich'); final drift = Drift.sqlite(db, updatePool); - final StoreRepository storeRepo = StoreRepository(drift); - - await StoreService.init(storeRepository: storeRepo, listenUpdates: listenStoreUpdates); + await DeviceIdStore.init(drift); await SessionRepository.ensureInitialized(drift); final settingsRepo = await SettingsRepository.ensureInitialized(drift); diff --git a/mobile/lib/utils/isolate.dart b/mobile/lib/utils/isolate.dart index ab3b19b78f..5119e7bead 100644 --- a/mobile/lib/utils/isolate.dart +++ b/mobile/lib/utils/isolate.dart @@ -4,7 +4,7 @@ import 'dart:ui'; import 'package:flutter/services.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/services/log.service.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/infrastructure/store.dart'; import 'package:immich_mobile/providers/infrastructure/cancel.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/utils/bootstrap.dart'; @@ -34,7 +34,7 @@ Cancelable runInIsolateGentle({ DartPluginRegistrant.ensureInitialized(); final log = Logger("IsolateLogger"); - final (drift, logDb) = await Bootstrap.initDomain(shouldBufferLogs: false, listenStoreUpdates: false); + final (drift, logDb) = await Bootstrap.initDomain(shouldBufferLogs: false); final ref = ProviderContainer( overrides: [cancellationProvider.overrideWithValue(onCancel), driftProvider.overrideWith(driftOverride(drift))], ); diff --git a/mobile/lib/utils/migration.dart b/mobile/lib/utils/migration.dart index 84d31d1dbc..17cfbdad99 100644 --- a/mobile/lib/utils/migration.dart +++ b/mobile/lib/utils/migration.dart @@ -11,10 +11,8 @@ import 'package:immich_mobile/domain/models/config/app_config.dart'; import 'package:immich_mobile/domain/models/log.model.dart'; import 'package:immich_mobile/domain/models/session.model.dart'; import 'package:immich_mobile/domain/models/settings_key.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/timeline.model.dart'; import 'package:immich_mobile/domain/services/feature_message.service.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/infrastructure/entities/app_metadata.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/session.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/settings.entity.drift.dart'; @@ -23,6 +21,7 @@ import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/network.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/session.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; +import 'package:immich_mobile/infrastructure/store.dart'; import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart'; import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; @@ -34,24 +33,22 @@ Future migrateDatabaseIfNeeded(Drift drift) async { final metadata = drift.appMetadataRepository; final version = await metadata.get(.version); - if (version < 25) { - await _migrateTo25(); - } - - if (version < 26) { - await _migrateTo26(drift); - } - - if (version < 27) { - await _migrateTo27(drift); - } - - if (version < 28) { - await _migrateTo28(drift); - } - if (version < 29) { - await _migrateTo29(drift); + final legacyStore = await _readLegacyStore(drift); + + if (version < 25) { + await _migrateTo25(drift, legacyStore); + } + if (version < 26) { + await _migrateTo26(drift, legacyStore); + } + if (version < 27) { + await _migrateTo27(drift, legacyStore); + } + if (version < 28) { + await _migrateTo28(drift, legacyStore); + } + await _migrateTo29(drift, legacyStore); } if (isFreshInstall) { @@ -62,22 +59,29 @@ Future migrateDatabaseIfNeeded(Drift drift) async { return; } -Future _migrateTo25() async { - final accessToken = Store.tryGet(.legacyAccessToken); +Future> _readLegacyStore(Drift drift) async { + final rows = await drift.storeEntity.select().get(); + return {for (final row in rows) row.id: row.stringValue ?? row.intValue}; +} + +Future _migrateTo25(Drift drift, Map legacyStore) async { + final migrator = _StoreMigrator.settings(drift, legacyStore); + + final accessToken = migrator.readLegacyStoreString(.legacyAccessToken); if (accessToken == null || accessToken.isEmpty) { return; } final urls = []; - final serverEndpoint = Store.tryGet(.legacyServerEndpoint); + final serverEndpoint = migrator.readLegacyStoreString(.legacyServerEndpoint); if (serverEndpoint != null && serverEndpoint.isNotEmpty) { urls.add(serverEndpoint); } - final localEndpoint = Store.tryGet(.legacyLocalEndpoint); + final localEndpoint = migrator.readLegacyStoreString(.legacyLocalEndpoint); if (localEndpoint != null && localEndpoint.isNotEmpty) { urls.add(localEndpoint); } - final externalJson = Store.tryGet(.legacyExternalEndpointList); + final externalJson = migrator.readLegacyStoreString(.legacyExternalEndpointList); if (externalJson != null) { final List list = jsonDecode(externalJson); for (final entry in list) { @@ -91,7 +95,7 @@ Future _migrateTo25() async { return; } - final customHeadersStr = Store.get(.legacyCustomHeaders, ""); + final customHeadersStr = migrator.readLegacyStoreString(.legacyCustomHeaders) ?? ""; final headers = customHeadersStr.isEmpty ? const {} : (jsonDecode(customHeadersStr) as Map).cast(); @@ -117,7 +121,7 @@ Future _migrateTo26(Drift drift) async { await migrator.migrateBool(.legacyDynamicTheme, .themeDynamic); await migrator.migrateBool(.legacyColorfulInterface, .themeColorfulInterface); // Cleanup - final cleanupKeepAlbumIds = await migrator.readLegacyStoreString(.legacyCleanupKeepAlbumIds); + final cleanupKeepAlbumIds = migrator.readLegacyStoreString(.legacyCleanupKeepAlbumIds); if (cleanupKeepAlbumIds != null) { final ids = cleanupKeepAlbumIds.split(',').where((id) => id.isNotEmpty).toList(); migrator.stage(.legacyCleanupKeepAlbumIds, .cleanupKeepAlbumIds, ids); @@ -146,9 +150,9 @@ Future _migrateTo26(Drift drift) async { await migrator.migrateBool(.legacyTapToNavigate, .viewerTapToNavigate); // Network await migrator.migrateBool(.legacyAutoEndpointSwitching, .networkAutoEndpointSwitching); - final preferredWifiName = await migrator.readLegacyStoreString(.legacyPreferredWifiName); + final preferredWifiName = migrator.readLegacyStoreString(.legacyPreferredWifiName); migrator.stage(.legacyPreferredWifiName, .networkPreferredWifiName, preferredWifiName); - final localEndpoint = await migrator.readLegacyStoreString(.legacyLocalEndpoint); + final localEndpoint = migrator.readLegacyStoreString(.legacyLocalEndpoint); migrator.stage(.legacyLocalEndpoint, .networkLocalEndpoint, localEndpoint); await _migrateExternalEndpointList(migrator); await _migrateCustomHeaders(migrator); @@ -216,7 +220,7 @@ Future _migrateTo29(Drift drift) async { ), ); - final rawStatus = await migrator.readLegacyStoreString(.legacySyncMigrationStatus); + final rawStatus = migrator.readLegacyStoreString(.legacySyncMigrationStatus); if (rawStatus != null) { final decoded = jsonDecode(rawStatus); final migrations = decoded is List ? decoded.whereType().toList() : []; @@ -228,7 +232,7 @@ Future _migrateTo29(Drift drift) async { } Future _migrateAlbumSortMode(_StoreMigrator migrator) async { - final raw = await migrator.readLegacyStoreInt(.legacySelectedAlbumSortOrder); + final raw = migrator.readLegacyStoreInt(.legacySelectedAlbumSortOrder); final mode = AlbumSortMode.values.firstWhereOrNull((e) => raw != null && e.storeIndex == raw); if (mode == null) { return; @@ -238,7 +242,7 @@ Future _migrateAlbumSortMode(_StoreMigrator migrator) async { } Future _migrateExternalEndpointList(_StoreMigrator migrator) async { - final raw = await migrator.readLegacyStoreString(.legacyExternalEndpointList); + final raw = migrator.readLegacyStoreString(.legacyExternalEndpointList); if (raw == null) { return; } @@ -258,11 +262,11 @@ Future _migrateExternalEndpointList(_StoreMigrator migrator) // ignore invalid entries } - migrator.stage(StoreKey.legacyExternalEndpointList, SettingsKey.networkExternalEndpointList, urls); + migrator.stage(.legacyExternalEndpointList, SettingsKey.networkExternalEndpointList, urls); } Future _migrateCustomHeaders(_StoreMigrator migrator) async { - final raw = await migrator.readLegacyStoreString(.legacyCustomHeaders); + final raw = migrator.readLegacyStoreString(.legacyCustomHeaders); if (raw == null) { return; } @@ -281,21 +285,28 @@ Future _migrateCustomHeaders(_StoreMigrator migrator) async { // ignore invalid entries } - migrator.stage(StoreKey.legacyCustomHeaders, SettingsKey.networkCustomHeaders, headers); + migrator.stage(.legacyCustomHeaders, SettingsKey.networkCustomHeaders, headers); } class _StoreMigrator { - _StoreMigrator._(this._db, {required this.encode, required this.readDefault, required this.insertRow}); + _StoreMigrator._( + this._db, + this._legacyStore, { + required this.encode, + required this.readDefault, + required this.insertRow, + }); final Drift _db; + final Map _legacyStore; final String Function(K key, Object value) encode; final Object? Function(K key) readDefault; final void Function(Batch batch, String name, String? value) insertRow; final Map _cache = {}; final List _migratedStoreIds = []; - Future migrateEnumIndex(StoreKey legacyKey, K newKey, List values) async { - final index = await readLegacyStoreInt(legacyKey); + Future migrateEnumIndex(LegacyStoreKey legacyKey, K newKey, List values) async { + final index = readLegacyStoreInt(legacyKey); if (index == null) { return; } @@ -309,8 +320,8 @@ class _StoreMigrator { _migratedStoreIds.add(legacyKey.id); } - Future migrateEnumName(StoreKey legacyKey, K newKey, List values) async { - final name = await readLegacyStoreString(legacyKey); + Future migrateEnumName(LegacyStoreKey legacyKey, K newKey, List values) async { + final name = readLegacyStoreString(legacyKey); if (name == null) { return; } @@ -324,8 +335,8 @@ class _StoreMigrator { _migratedStoreIds.add(legacyKey.id); } - Future migrateBool(StoreKey legacyKey, K newKey) async { - final intValue = await readLegacyStoreInt(legacyKey); + Future migrateBool(LegacyStoreKey legacyKey, K newKey) async { + final intValue = readLegacyStoreInt(legacyKey); if (intValue == null) { return; } @@ -334,8 +345,8 @@ class _StoreMigrator { _migratedStoreIds.add(legacyKey.id); } - Future migrateInt(StoreKey legacyKey, K newKey) async { - final intValue = await readLegacyStoreInt(legacyKey); + Future migrateInt(LegacyStoreKey legacyKey, K newKey) async { + final intValue = readLegacyStoreInt(legacyKey); if (intValue == null) { return; } @@ -344,8 +355,8 @@ class _StoreMigrator { _migratedStoreIds.add(legacyKey.id); } - Future migrateString(StoreKey legacyKey, K newKey) async { - final value = await readLegacyStoreString(legacyKey); + Future migrateString(LegacyStoreKey legacyKey, K newKey) async { + final value = readLegacyStoreString(legacyKey); if (value == null || value.isEmpty) { return; } @@ -354,12 +365,12 @@ class _StoreMigrator { _migratedStoreIds.add(legacyKey.id); } - Future migrateNullableString(StoreKey legacyKey, K newKey) async { - _cache[newKey] = await readLegacyStoreString(legacyKey); + Future migrateNullableString(LegacyStoreKey legacyKey, K newKey) async { + _cache[newKey] = readLegacyStoreString(legacyKey); _migratedStoreIds.add(legacyKey.id); } - void stage(StoreKey legacyKey, K newKey, Object? value) { + void stage(LegacyStoreKey legacyKey, K newKey, Object? value) { _cache[newKey] = value; _migratedStoreIds.add(legacyKey.id); } @@ -378,15 +389,9 @@ class _StoreMigrator { await deleteLegacyStoreRows(_migratedStoreIds); } - Future readLegacyStoreString(StoreKey key) async { - final row = await (_db.storeEntity.select()..where((t) => t.id.equals(key.id))).getSingleOrNull(); - return row?.stringValue; - } + String? readLegacyStoreString(LegacyStoreKey key) => _legacyStore[key.id] as String?; - Future readLegacyStoreInt(StoreKey key) async { - final row = await (_db.storeEntity.select()..where((t) => t.id.equals(key.id))).getSingleOrNull(); - return row?.intValue; - } + int? readLegacyStoreInt(LegacyStoreKey key) => _legacyStore[key.id] as int?; Future deleteLegacyStoreRows(List ids) async { if (ids.isEmpty) { diff --git a/mobile/test/domain/services/local_sync_service_test.dart b/mobile/test/domain/services/local_sync_service_test.dart index 17c657aaf6..9be24fb566 100644 --- a/mobile/test/domain/services/local_sync_service_test.dart +++ b/mobile/test/domain/services/local_sync_service_test.dart @@ -5,13 +5,11 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:immich_mobile/domain/models/app_metadata_key.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/services/local_sync.service.dart'; -import 'package:immich_mobile/domain/services/store.service.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart'; +import 'package:immich_mobile/infrastructure/store.dart'; import 'package:immich_mobile/platform/native_sync_api.g.dart'; import 'package:immich_mobile/repositories/asset_media.repository.dart'; import 'package:mocktail/mocktail.dart'; @@ -37,7 +35,7 @@ void main() { debugDefaultTargetPlatformOverride = TargetPlatform.android; db = Drift(drift.DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); - await StoreService.init(storeRepository: StoreRepository(db)); + await DeviceIdStore.init(db); }); tearDownAll(() async { diff --git a/mobile/test/domain/services/store_service_test.dart b/mobile/test/domain/services/store_service_test.dart deleted file mode 100644 index 8fab8fae9e..0000000000 --- a/mobile/test/domain/services/store_service_test.dart +++ /dev/null @@ -1,154 +0,0 @@ -import 'dart:async'; - -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/domain/services/store.service.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; -import 'package:mocktail/mocktail.dart'; - -import '../../infrastructure/repository.mock.dart'; - -const _kAccessToken = '#ThisIsAToken'; -const _kAdvancedTroubleshooting = false; -const _kVersion = 2; - -void main() { - late StoreService sut; - late StoreRepository mockDriftStoreRepo; - late StreamController>> controller; - - setUp(() async { - controller = StreamController>>.broadcast(); - mockDriftStoreRepo = MockStoreRepository(); - // For generics, we need to provide fallback to each concrete type to avoid runtime errors - registerFallbackValue(StoreKey.legacyAccessToken); - registerFallbackValue(StoreKey.legacyVersion); - registerFallbackValue(StoreKey.legacyAdvancedTroubleshooting); - - when(() => mockDriftStoreRepo.getAll()).thenAnswer( - (_) async => [ - const StoreDto(StoreKey.legacyAccessToken, _kAccessToken), - const StoreDto(StoreKey.legacyAdvancedTroubleshooting, _kAdvancedTroubleshooting), - const StoreDto(StoreKey.legacyVersion, _kVersion), - ], - ); - when(() => mockDriftStoreRepo.watchAll()).thenAnswer((_) => controller.stream); - - sut = await StoreService.create(storeRepository: mockDriftStoreRepo); - }); - - tearDown(() async { - unawaited(sut.dispose()); - await controller.close(); - }); - - group("Store Service Init:", () { - test('Populates the internal cache on init', () { - verify(() => mockDriftStoreRepo.getAll()).called(1); - expect(sut.tryGet(StoreKey.legacyAccessToken), _kAccessToken); - expect(sut.tryGet(StoreKey.legacyAdvancedTroubleshooting), _kAdvancedTroubleshooting); - expect(sut.tryGet(StoreKey.legacyVersion), _kVersion); - // Other keys should be null - expect(sut.tryGet(StoreKey.deviceId), isNull); - }); - - test('Listens to stream of store updates', () async { - final event = StoreDto(StoreKey.legacyAccessToken, _kAccessToken.toUpperCase()); - controller.add([event]); - - await pumpEventQueue(); - - verify(() => mockDriftStoreRepo.watchAll()).called(1); - expect(sut.tryGet(StoreKey.legacyAccessToken), _kAccessToken.toUpperCase()); - }); - }); - - group('Store Service get:', () { - test('Returns the stored value for the given key', () { - expect(sut.get(StoreKey.legacyAccessToken), _kAccessToken); - }); - - test('Throws StoreKeyNotFoundException for nonexistent keys', () { - expect(() => sut.get(StoreKey.deviceId), throwsA(isA())); - }); - - test('Returns the stored value for the given key or the defaultValue', () { - expect(sut.get(StoreKey.legacyBackupTriggerDelay, 5), 5); - }); - }); - - group('Store Service put:', () { - setUp(() { - when(() => mockDriftStoreRepo.upsert(any>(), any())).thenAnswer((_) async => true); - }); - - test('Skip insert when value is not modified', () async { - await sut.put(StoreKey.legacyAccessToken, _kAccessToken); - verifyNever(() => mockDriftStoreRepo.upsert(StoreKey.legacyAccessToken, any())); - }); - - test('Insert value when modified', () async { - final newAccessToken = _kAccessToken.toUpperCase(); - await sut.put(StoreKey.legacyAccessToken, newAccessToken); - verify(() => mockDriftStoreRepo.upsert(StoreKey.legacyAccessToken, newAccessToken)).called(1); - expect(sut.tryGet(StoreKey.legacyAccessToken), newAccessToken); - }); - }); - - group('Store Service watch:', () { - late StreamController valueController; - - setUp(() { - valueController = StreamController.broadcast(); - when(() => mockDriftStoreRepo.watch(any>())).thenAnswer((_) => valueController.stream); - }); - - tearDown(() async { - await valueController.close(); - }); - - test('Watches a specific key for changes', () async { - final stream = sut.watch(StoreKey.legacyAccessToken); - final events = [_kAccessToken, _kAccessToken.toUpperCase(), null, _kAccessToken.toLowerCase()]; - - unawaited(expectLater(stream, emitsInOrder(events))); - - for (final event in events) { - valueController.add(event); - } - - await pumpEventQueue(); - verify(() => mockDriftStoreRepo.watch(StoreKey.legacyAccessToken)).called(1); - }); - }); - - group('Store Service delete:', () { - setUp(() { - when(() => mockDriftStoreRepo.deleteValue(any>())).thenAnswer((_) async => true); - }); - - test('Removes the value from the DB', () async { - await sut.delete(StoreKey.legacyAccessToken); - verify(() => mockDriftStoreRepo.deleteValue(StoreKey.legacyAccessToken)).called(1); - }); - - test('Removes the value from the cache', () async { - await sut.delete(StoreKey.legacyAccessToken); - expect(sut.tryGet(StoreKey.legacyAccessToken), isNull); - }); - }); - - group('Store Service clear:', () { - setUp(() { - when(() => mockDriftStoreRepo.deleteAll()).thenAnswer((_) async => true); - }); - - test('Clears all values from the store', () async { - await sut.clear(); - verify(() => mockDriftStoreRepo.deleteAll()).called(1); - expect(sut.tryGet(StoreKey.legacyAccessToken), isNull); - expect(sut.tryGet(StoreKey.legacyAdvancedTroubleshooting), isNull); - expect(sut.tryGet(StoreKey.legacyVersion), isNull); - }); - }); -} diff --git a/mobile/test/domain/services/sync_stream_service_test.dart b/mobile/test/domain/services/sync_stream_service_test.dart index f6e6045e9c..c2fa467c12 100644 --- a/mobile/test/domain/services/sync_stream_service_test.dart +++ b/mobile/test/domain/services/sync_stream_service_test.dart @@ -7,15 +7,13 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:immich_mobile/domain/models/app_metadata_key.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/sync_event.model.dart'; -import 'package:immich_mobile/domain/services/store.service.dart'; import 'package:immich_mobile/domain/services/sync_stream.service.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart'; +import 'package:immich_mobile/infrastructure/store.dart'; import 'package:immich_mobile/repositories/asset_media.repository.dart'; import 'package:immich_mobile/utils/semver.dart'; import 'package:mocktail/mocktail.dart'; @@ -63,7 +61,7 @@ void main() { registerFallbackValue(const []); db = Drift(drift.DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); - await StoreService.init(storeRepository: StoreRepository(db)); + await DeviceIdStore.init(db); }); tearDownAll(() async { diff --git a/mobile/test/infrastructure/repositories/store_repository_test.dart b/mobile/test/infrastructure/repositories/store_repository_test.dart deleted file mode 100644 index 60492fd2d8..0000000000 --- a/mobile/test/infrastructure/repositories/store_repository_test.dart +++ /dev/null @@ -1,152 +0,0 @@ -import 'dart:async'; - -import 'package:drift/drift.dart' hide isNull; -import 'package:drift/native.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/domain/models/store.model.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/store.repository.dart'; - -const _kTestAccessToken = "#TestToken"; -const _kTestVersion = 10; -const _kTestAdvancedTroubleshooting = false; - -Future _populateStore(Drift db) async { - await db.batch((batch) async { - batch.insert( - db.storeEntity, - StoreEntityCompanion( - id: Value(StoreKey.legacyAdvancedTroubleshooting.id), - intValue: const Value(_kTestAdvancedTroubleshooting ? 1 : 0), - stringValue: const Value(null), - ), - ); - batch.insert( - db.storeEntity, - StoreEntityCompanion( - id: Value(StoreKey.legacyAccessToken.id), - intValue: const Value(null), - stringValue: const Value(_kTestAccessToken), - ), - ); - batch.insert( - db.storeEntity, - StoreEntityCompanion( - id: Value(StoreKey.legacyVersion.id), - intValue: const Value(_kTestVersion), - stringValue: const Value(null), - ), - ); - }); -} - -void main() { - late Drift db; - late StoreRepository sut; - - setUp(() async { - db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); - sut = StoreRepository(db); - }); - - tearDown(() async { - await db.close(); - }); - - group('Store Repository converters:', () { - test('converts int', () async { - int? version = await sut.tryGet(StoreKey.legacyVersion); - expect(version, isNull); - await sut.upsert(StoreKey.legacyVersion, _kTestVersion); - version = await sut.tryGet(StoreKey.legacyVersion); - expect(version, _kTestVersion); - }); - - test('converts string', () async { - String? accessToken = await sut.tryGet(StoreKey.legacyAccessToken); - expect(accessToken, isNull); - await sut.upsert(StoreKey.legacyAccessToken, _kTestAccessToken); - accessToken = await sut.tryGet(StoreKey.legacyAccessToken); - expect(accessToken, _kTestAccessToken); - }); - - test('converts bool', () async { - bool? advancedTroubleshooting = await sut.tryGet(StoreKey.legacyAdvancedTroubleshooting); - expect(advancedTroubleshooting, isNull); - await sut.upsert(StoreKey.legacyAdvancedTroubleshooting, _kTestAdvancedTroubleshooting); - advancedTroubleshooting = await sut.tryGet(StoreKey.legacyAdvancedTroubleshooting); - expect(advancedTroubleshooting, _kTestAdvancedTroubleshooting); - }); - }); - - group('Store Repository Deletes:', () { - setUp(() async { - await _populateStore(db); - }); - - test('delete()', () async { - bool? advancedTroubleshooting = await sut.tryGet(StoreKey.legacyAdvancedTroubleshooting); - expect(advancedTroubleshooting, isFalse); - await sut.deleteValue(StoreKey.legacyAdvancedTroubleshooting); - advancedTroubleshooting = await sut.tryGet(StoreKey.legacyAdvancedTroubleshooting); - expect(advancedTroubleshooting, isNull); - }); - - test('deleteAll()', () async { - final count = await db.storeEntity.count().getSingle(); - expect(count, isNot(isZero)); - await sut.deleteAll(); - unawaited(expectLater(await db.storeEntity.count().getSingle(), isZero)); - }); - }); - - group('Store Repository Updates:', () { - setUp(() async { - await _populateStore(db); - }); - - test('upsert()', () async { - int? version = await sut.tryGet(StoreKey.legacyVersion); - expect(version, _kTestVersion); - await sut.upsert(StoreKey.legacyVersion, _kTestVersion + 10); - version = await sut.tryGet(StoreKey.legacyVersion); - expect(version, _kTestVersion + 10); - }); - }); - - group('Store Repository Watchers:', () { - setUp(() async { - await _populateStore(db); - }); - - test('watch()', () async { - final stream = sut.watch(StoreKey.legacyVersion); - unawaited(expectLater(stream, emitsInOrder([_kTestVersion, _kTestVersion + 10]))); - await pumpEventQueue(); - await sut.upsert(StoreKey.legacyVersion, _kTestVersion + 10); - }); - - test('watchAll()', () async { - final stream = sut.watchAll(); - unawaited( - expectLater( - stream, - emitsInOrder([ - [ - const StoreDto(StoreKey.legacyVersion, _kTestVersion), - const StoreDto(StoreKey.legacyAccessToken, _kTestAccessToken), - const StoreDto(StoreKey.legacyAdvancedTroubleshooting, _kTestAdvancedTroubleshooting), - ], - [ - const StoreDto(StoreKey.legacyVersion, _kTestVersion + 10), - const StoreDto(StoreKey.legacyAccessToken, _kTestAccessToken), - const StoreDto(StoreKey.legacyAdvancedTroubleshooting, _kTestAdvancedTroubleshooting), - ], - ]), - ), - ); - await sut.upsert(StoreKey.legacyVersion, _kTestVersion + 10); - }); - }); -} diff --git a/mobile/test/infrastructure/repositories/sync_api_repository_test.dart b/mobile/test/infrastructure/repositories/sync_api_repository_test.dart index 5253838fef..9d13999933 100644 --- a/mobile/test/infrastructure/repositories/sync_api_repository_test.dart +++ b/mobile/test/infrastructure/repositories/sync_api_repository_test.dart @@ -6,10 +6,9 @@ import 'package:drift/native.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; import 'package:immich_mobile/domain/models/sync_event.model.dart'; -import 'package:immich_mobile/domain/services/store.service.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart'; +import 'package:immich_mobile/infrastructure/store.dart'; import 'package:immich_mobile/utils/semver.dart'; import 'package:mocktail/mocktail.dart'; import 'package:openapi/api.dart'; @@ -41,7 +40,7 @@ void main() { setUpAll(() async { final db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); - await StoreService.init(storeRepository: StoreRepository(db)); + await DeviceIdStore.init(db); }); setUp(() { diff --git a/mobile/test/infrastructure/repository.mock.dart b/mobile/test/infrastructure/repository.mock.dart index f6529bd5d9..c741a36a43 100644 --- a/mobile/test/infrastructure/repository.mock.dart +++ b/mobile/test/infrastructure/repository.mock.dart @@ -10,7 +10,6 @@ import 'package:immich_mobile/infrastructure/repositories/remote_album.repositor import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_migration.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart'; @@ -21,12 +20,6 @@ import 'package:immich_mobile/repositories/drift_album_api_repository.dart'; import 'package:immich_mobile/repositories/upload.repository.dart'; import 'package:mocktail/mocktail.dart'; -class MockDrift extends Mock implements Drift {} - -class MockMemoryRepository extends Mock implements MemoryRepository {} - -class MockStoreRepository extends Mock implements StoreRepository {} - class MockSettingsRepository extends Mock implements SettingsRepository {} class MockAppMetadataRepository extends Mock implements AppMetadataRepository {} diff --git a/mobile/test/modules/map/map_theme_override_test.dart b/mobile/test/modules/map/map_theme_override_test.dart index 3ea54bc9b5..b74274b44f 100644 --- a/mobile/test/modules/map/map_theme_override_test.dart +++ b/mobile/test/modules/map/map_theme_override_test.dart @@ -6,9 +6,8 @@ import 'package:drift/native.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/services/store.service.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; +import 'package:immich_mobile/infrastructure/store.dart'; import 'package:immich_mobile/models/map/map_state.model.dart'; import 'package:immich_mobile/providers/locale_provider.dart'; import 'package:immich_mobile/providers/map/map_state.provider.dart'; @@ -32,7 +31,7 @@ void main() { setUp(() async { mapState = const MapState(themeMode: ThemeMode.dark); mapStateNotifier = MockMapStateNotifier(mapState); - await StoreService.init(storeRepository: StoreRepository(db)); + await DeviceIdStore.init(db); overrides = [ mapStateNotifierProvider.overrideWith(() => mapStateNotifier), localeProvider.overrideWithValue(const Locale("en")), diff --git a/mobile/test/service.mocks.dart b/mobile/test/service.mocks.dart index 7e67d4d250..b340020319 100644 --- a/mobile/test/service.mocks.dart +++ b/mobile/test/service.mocks.dart @@ -2,7 +2,6 @@ import 'package:immich_mobile/domain/services/asset.service.dart'; import 'package:immich_mobile/domain/services/background_worker.service.dart'; import 'package:immich_mobile/domain/services/partner.service.dart'; import 'package:immich_mobile/domain/services/remote_album.service.dart'; -import 'package:immich_mobile/domain/services/store.service.dart'; import 'package:immich_mobile/domain/services/user.service.dart'; import 'package:immich_mobile/domain/utils/background_sync.dart'; import 'package:immich_mobile/platform/native_sync_api.g.dart'; @@ -23,8 +22,6 @@ class MockApiService extends Mock implements ApiService {} class MockNetworkService extends Mock implements NetworkService {} -class MockStoreService extends Mock implements StoreService {} - class MockNativeSyncApi extends Mock implements NativeSyncApi {} class MockPartnerService extends Mock implements PartnerService {} diff --git a/mobile/test/services/action.service_test.dart b/mobile/test/services/action.service_test.dart index b008ff9bfc..b74e3fb8e3 100644 --- a/mobile/test/services/action.service_test.dart +++ b/mobile/test/services/action.service_test.dart @@ -2,10 +2,8 @@ import 'package:drift/drift.dart' as drift; import 'package:drift/native.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/domain/services/store.service.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; +import 'package:immich_mobile/infrastructure/store.dart'; import 'package:immich_mobile/services/action.service.dart'; import 'package:mocktail/mocktail.dart'; @@ -25,7 +23,7 @@ void main() { debugDefaultTargetPlatformOverride = TargetPlatform.android; db = Drift(drift.DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); - await StoreService.init(storeRepository: StoreRepository(db)); + await DeviceIdStore.init(db); }); tearDownAll(() async { diff --git a/mobile/test/services/auth.service_test.dart b/mobile/test/services/auth.service_test.dart index cdde557443..d298ad56cf 100644 --- a/mobile/test/services/auth.service_test.dart +++ b/mobile/test/services/auth.service_test.dart @@ -2,10 +2,9 @@ import 'package:drift/drift.dart' hide isNull; import 'package:drift/native.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/domain/services/store.service.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/session.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; +import 'package:immich_mobile/infrastructure/store.dart'; import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart'; import 'package:immich_mobile/services/auth.service.dart'; import 'package:mocktail/mocktail.dart'; @@ -37,7 +36,7 @@ void main() { setUpAll(() async { WidgetsFlutterBinding.ensureInitialized(); db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); - await StoreService.init(storeRepository: StoreRepository(db)); + await DeviceIdStore.init(db); await SessionRepository.ensureInitialized(db); }); diff --git a/mobile/test/services/background_upload.service_test.dart b/mobile/test/services/background_upload.service_test.dart index a926724f73..12af726bdd 100644 --- a/mobile/test/services/background_upload.service_test.dart +++ b/mobile/test/services/background_upload.service_test.dart @@ -8,13 +8,10 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/session.model.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/domain/services/store.service.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/session.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; +import 'package:immich_mobile/infrastructure/store.dart'; import 'package:immich_mobile/services/background_upload.service.dart'; import 'package:mocktail/mocktail.dart'; @@ -39,11 +36,11 @@ void main() { (MethodCall methodCall) async => 'test', ); db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); - await StoreService.init(storeRepository: StoreRepository(db)); + await DeviceIdStore.init(db); await SettingsRepository.ensureInitialized(db); await SessionRepository.ensureInitialized(db); await SessionRepository.instance.write(SessionKey.serverEndpoint, 'https://demo.immich.app'); - await Store.put(StoreKey.deviceId, 'test-device-id'); + await Store.setDeviceId('test-device-id'); }); setUp(() { diff --git a/mobile/test/services/foreground_upload.service_test.dart b/mobile/test/services/foreground_upload.service_test.dart index bb2360f702..f869665845 100644 --- a/mobile/test/services/foreground_upload.service_test.dart +++ b/mobile/test/services/foreground_upload.service_test.dart @@ -4,13 +4,10 @@ import 'package:drift/drift.dart' hide isNotNull, isNull; import 'package:drift/native.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/domain/services/store.service.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/session.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; +import 'package:immich_mobile/infrastructure/store.dart'; import 'package:immich_mobile/repositories/upload.repository.dart'; import 'package:immich_mobile/services/foreground_upload.service.dart'; import 'package:mocktail/mocktail.dart'; @@ -37,12 +34,11 @@ void main() { (MethodCall methodCall) async => 'test', ); db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); - await StoreService.init(storeRepository: StoreRepository(db)); + await DeviceIdStore.init(db); await SettingsRepository.ensureInitialized(db); await SessionRepository.ensureInitialized(db); await SessionRepository.instance.write(.serverEndpoint, 'http://demo.immich.app'); - await Store.put(StoreKey.deviceId, 'device-id'); - + await DeviceIdStore.I.setDeviceId('device-id'); registerFallbackValue(File('file')); registerFallbackValue({}); }); diff --git a/mobile/test/unit/presentation/presentation_context.dart b/mobile/test/unit/presentation/presentation_context.dart index c1ab323ef9..19acc5c8e7 100644 --- a/mobile/test/unit/presentation/presentation_context.dart +++ b/mobile/test/unit/presentation/presentation_context.dart @@ -8,12 +8,10 @@ import 'package:immich_mobile/constants/locales.dart'; import 'package:immich_mobile/domain/models/app_metadata_key.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/domain/services/store.service.dart'; import 'package:immich_mobile/generated/codegen_loader.g.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/session.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:immich_mobile/presentation/actions/action.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; @@ -85,7 +83,6 @@ class PresentationContext { TestUtils.init(); if (_db == null) { final db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); - await StoreService.init(storeRepository: StoreRepository(db), listenUpdates: false); await SessionRepository.ensureInitialized(db); await SessionRepository.instance.write(.serverUrl, serverEndpoint); await SessionRepository.instance.write(.accessToken, 'access-token');