diff --git a/mobile/analysis_options.yaml b/mobile/analysis_options.yaml index 619ba284c1..ffeccbdd50 100644 --- a/mobile/analysis_options.yaml +++ b/mobile/analysis_options.yaml @@ -66,6 +66,9 @@ custom_lint: # required / wanted - lib/entities/*.entity.dart - lib/repositories/{album,asset,backup,database,etag,exif_info,user}.repository.dart + - lib/infrastructure/entities/*.entity.dart + - lib/infrastructure/repositories/{store,db}.repository.dart + - lib/providers/infrastructure/db.provider.dart # acceptable exceptions for the time being (until Isar is fully replaced) - integration_test/test_utils/general_helper.dart - lib/main.dart diff --git a/mobile/integration_test/test_utils/general_helper.dart b/mobile/integration_test/test_utils/general_helper.dart index bf2b252b2b..a3db2d49a8 100644 --- a/mobile/integration_test/test_utils/general_helper.dart +++ b/mobile/integration_test/test_utils/general_helper.dart @@ -4,12 +4,13 @@ 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/main.dart' as app; import 'package:immich_mobile/providers/db.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:integration_test/integration_test.dart'; import 'package:isar/isar.dart'; // ignore: depend_on_referenced_packages import 'package:meta/meta.dart'; -import 'package:immich_mobile/main.dart' as app; import 'login_helper.dart'; @@ -44,7 +45,10 @@ class ImmichTestHelper { // Load main Widget await tester.pumpWidget( ProviderScope( - overrides: [dbProvider.overrideWithValue(db)], + overrides: [ + dbProvider.overrideWithValue(db), + isarProvider.overrideWithValue(db), + ], child: const app.MainWidget(), ), ); diff --git a/mobile/lib/domain/README.md b/mobile/lib/domain/README.md new file mode 100644 index 0000000000..f9bb2ee561 --- /dev/null +++ b/mobile/lib/domain/README.md @@ -0,0 +1,34 @@ +# Domain Layer + +This directory contains the domain layer of Immich. The domain layer is responsible for the business logic of the app. It includes interfaces for repositories, models, services and utilities. This layer should never depend on anything from the presentation layer or from the infrastructure layer. + +## Structure + +- **[Interfaces](./interfaces/)**: These are the interfaces that define the contract for data operations. +- **[Models](./models/)**: These are the core data classes that represent the business models. +- **[Services](./services/)**: These are the classes that contain the business logic and interact with the repositories. +- **[Utils](./utils/)**: These are utility classes and functions that provide common functionalities used across the domain layer. + +``` +domain/ +├── interfaces/ +│ └── user.interface.dart +├── models/ +│ └── user.model.dart +├── services/ +│ └── user.service.dart +└── utils/ + └── date_utils.dart +``` + +## Usage + +The domain layer provides services that implement the business logic by consuming repositories through dependency injection. Services are exposed through Riverpod providers in the root `providers` directory. + +```dart +// In presentation layer +final userService = ref.watch(userServiceProvider); +final user = await userService.getUser(userId); +``` + +The presentation layer should never directly use repositories, but instead interact with the domain layer through services. \ No newline at end of file diff --git a/mobile/lib/domain/interfaces/db.interface.dart b/mobile/lib/domain/interfaces/db.interface.dart new file mode 100644 index 0000000000..5645d15c47 --- /dev/null +++ b/mobile/lib/domain/interfaces/db.interface.dart @@ -0,0 +1,3 @@ +abstract interface class IDatabaseRepository { + Future transaction(Future Function() callback); +} diff --git a/mobile/lib/domain/interfaces/store.interface.dart b/mobile/lib/domain/interfaces/store.interface.dart new file mode 100644 index 0000000000..cbcce1159c --- /dev/null +++ b/mobile/lib/domain/interfaces/store.interface.dart @@ -0,0 +1,17 @@ +import 'package:immich_mobile/entities/store.entity.dart'; + +abstract interface class IStoreRepository { + Future insert(StoreKey key, T value); + + Future tryGet(StoreKey key); + + Stream watch(StoreKey key); + + Stream watchAll(); + + Future update(StoreKey key, T value); + + Future delete(StoreKey key); + + Future deleteAll(); +} diff --git a/mobile/lib/domain/services/store.service.dart b/mobile/lib/domain/services/store.service.dart new file mode 100644 index 0000000000..ac127b8ee6 --- /dev/null +++ b/mobile/lib/domain/services/store.service.dart @@ -0,0 +1,106 @@ +import 'dart:async'; + +import 'package:immich_mobile/domain/interfaces/store.interface.dart'; +import 'package:immich_mobile/entities/store.entity.dart'; + +class StoreService { + final IStoreRepository _storeRepository; + + final Map _cache = {}; + late final StreamSubscription _storeUpdateSubscription; + + StoreService._({ + required IStoreRepository storeRepository, + }) : _storeRepository = storeRepository; + + // 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 + /// Initializes the store with the given [storeRepository] + static Future init({ + required IStoreRepository storeRepository, + }) async { + _instance ??= await create(storeRepository: storeRepository); + return _instance!; + } + + /// Initializes the store with the given [storeRepository] + static Future create({ + required IStoreRepository storeRepository, + }) async { + final instance = StoreService._(storeRepository: storeRepository); + await instance._populateCache(); + instance._storeUpdateSubscription = instance._listenForChange(); + return instance; + } + + /// Fills the cache with the values from the DB + Future _populateCache() async { + for (StoreKey key in StoreKey.values) { + final storeValue = await _storeRepository.tryGet(key); + _cache[key.id] = storeValue; + } + } + + /// Listens for changes in the DB and updates the cache + StreamSubscription _listenForChange() => + _storeRepository.watchAll().listen((event) { + _cache[event.key.id] = event.value; + }); + + /// Disposes the store and cancels the subscription. To reuse the store call init() again + void dispose() async { + await _storeUpdateSubscription.cancel(); + _cache.clear(); + } + + /// Returns the stored value for the given key (possibly null) + T? tryGet(StoreKey key) => _cache[key.id]; + + /// Returns the stored value for the given key or if null the [defaultValue] + /// Throws a [StoreKeyNotFoundException] if both are null + T get(StoreKey key, [T? defaultValue]) { + final value = tryGet(key) ?? defaultValue; + if (value == null) { + throw StoreKeyNotFoundException(key); + } + return value; + } + + /// Asynchronously stores the value in the DB and synchronously in the cache + Future put(StoreKey key, T value) async { + if (_cache[key.id] == value) return; + await _storeRepository.insert(key, value); + _cache[key.id] = value; + } + + /// Watches a specific key for changes + Stream watch(StoreKey key) => _storeRepository.watch(key); + + /// Removes the value asynchronously from the DB and synchronously from the cache + Future delete(StoreKey key) async { + await _storeRepository.delete(key); + _cache.remove(key.id); + } + + /// Clears all values from this 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 index a6ebe77c4f..f6f78d9d9c 100644 --- a/mobile/lib/entities/store.entity.dart +++ b/mobile/lib/entities/store.entity.dart @@ -1,138 +1,11 @@ import 'dart:convert'; import 'dart:typed_data'; -import 'package:collection/collection.dart'; +import 'package:immich_mobile/domain/services/store.service.dart'; import 'package:immich_mobile/entities/user.entity.dart'; -import 'package:isar/isar.dart'; -import 'package:logging/logging.dart'; -part 'store.entity.g.dart'; - -/// Key-value store for individual items enumerated in StoreKey. -/// Supports String, int and JSON-serializable Objects -/// Can be used concurrently from multiple isolates -class Store { - static final Logger _log = Logger("Store"); - static late final Isar _db; - static final List _cache = - List.filled(StoreKey.values.map((e) => e.id).max + 1, null); - - /// Initializes the store (call exactly once per app start) - static void init(Isar db) { - _db = db; - _populateCache(); - _db.storeValues.where().build().watch().listen(_onChangeListener); - } - - /// clears all values from this store (cache and DB), only for testing! - static Future clear() { - _cache.fillRange(0, _cache.length, null); - return _db.writeTxn(() => _db.storeValues.clear()); - } - - /// Returns the stored value for the given key or if null the [defaultValue] - /// Throws a [StoreKeyNotFoundException] if both are null - static T get(StoreKey key, [T? defaultValue]) { - final value = _cache[key.id] ?? defaultValue; - if (value == null) { - throw StoreKeyNotFoundException(key); - } - return value; - } - - /// Watches a specific key for changes - static Stream watch(StoreKey key) => - _db.storeValues.watchObject(key.id).map((e) => e?._extract(key)); - - /// Returns the stored value for the given key (possibly null) - static T? tryGet(StoreKey key) => _cache[key.id]; - - /// Stores the value synchronously in the cache and asynchronously in the DB - static Future put(StoreKey key, T value) { - if (_cache[key.id] == value) return Future.value(); - _cache[key.id] = value; - return _db.writeTxn( - () async => _db.storeValues.put(await StoreValue._of(value, key)), - ); - } - - /// Removes the value synchronously from the cache and asynchronously from the DB - static Future delete(StoreKey key) { - if (_cache[key.id] == null) return Future.value(); - _cache[key.id] = null; - return _db.writeTxn(() => _db.storeValues.delete(key.id)); - } - - /// Fills the cache with the values from the DB - static _populateCache() { - for (StoreKey key in StoreKey.values) { - final StoreValue? value = _db.storeValues.getSync(key.id); - if (value != null) { - _cache[key.id] = value._extract(key); - } - } - } - - /// updates the state if a value is updated in any isolate - static void _onChangeListener(List? data) { - if (data != null) { - for (StoreValue value in data) { - final key = StoreKey.values.firstWhereOrNull((e) => e.id == value.id); - if (key != null) { - _cache[value.id] = value._extract(key); - } else { - _log.warning("No key available for value id - ${value.id}"); - } - } - } - } -} - -/// Internal class for `Store`, do not use elsewhere. -@Collection(inheritance: false) -class StoreValue { - StoreValue(this.id, {this.intValue, this.strValue}); - Id id; - int? intValue; - String? strValue; - - T? _extract(StoreKey key) => switch (key.type) { - const (int) => intValue as T?, - const (bool) => intValue == null ? null : (intValue! == 1) as T, - const (DateTime) => intValue == null - ? null - : DateTime.fromMicrosecondsSinceEpoch(intValue!) as T, - const (String) => strValue as T?, - _ when key.fromDb != null => key.fromDb!.call(Store._db, intValue!), - _ => throw TypeError(), - }; - - static Future _of(T? value, StoreKey key) async { - int? i; - String? s; - switch (key.type) { - case const (int): - i = value as int?; - break; - case const (bool): - i = value == null ? null : (value == true ? 1 : 0); - break; - case const (DateTime): - i = value == null ? null : (value as DateTime).microsecondsSinceEpoch; - break; - case const (String): - s = value as String?; - break; - default: - if (key.toDb != null) { - i = await key.toDb!.call(Store._db, value); - break; - } - throw TypeError(); - } - return StoreValue(key.id, intValue: i, strValue: s); - } -} +// ignore: non_constant_identifier_names +final Store = StoreService.I; class SSLClientCertStoreVal { final Uint8List data; @@ -164,100 +37,81 @@ class SSLClientCertStoreVal { } } -class StoreKeyNotFoundException implements Exception { - final StoreKey key; - StoreKeyNotFoundException(this.key); - @override - String toString() => "Key '${key.name}' not found in Store"; -} - /// Key for each possible value in the `Store`. /// Defines the data type for each value enum StoreKey { - version(0, type: int), - assetETag(1, type: String), - currentUser(2, type: User, fromDb: _getUser, toDb: _toUser), - deviceIdHash(3, type: int), - deviceId(4, type: String), - backupFailedSince(5, type: DateTime), - backupRequireWifi(6, type: bool), - backupRequireCharging(7, type: bool), - backupTriggerDelay(8, type: int), - serverUrl(10, type: String), - accessToken(11, type: String), - serverEndpoint(12, type: String), - autoBackup(13, type: bool), - backgroundBackup(14, type: bool), - sslClientCertData(15, type: String), - sslClientPasswd(16, type: String), + version._(0), + assetETag._(1), + currentUser._(2), + deviceIdHash._(3), + deviceId._(4), + backupFailedSince._(5), + backupRequireWifi._(6), + backupRequireCharging._(7), + backupTriggerDelay._(8), + serverUrl._(10), + accessToken._(11), + serverEndpoint._(12), + autoBackup._(13), + backgroundBackup._(14), + sslClientCertData._(15), + sslClientPasswd._(16), // user settings from [AppSettingsEnum] below: - loadPreview(100, type: bool), - loadOriginal(101, type: bool), - themeMode(102, type: String), - tilesPerRow(103, type: int), - dynamicLayout(104, type: bool), - groupAssetsBy(105, type: int), - uploadErrorNotificationGracePeriod(106, type: int), - backgroundBackupTotalProgress(107, type: bool), - backgroundBackupSingleProgress(108, type: bool), - storageIndicator(109, type: bool), - thumbnailCacheSize(110, type: int), - imageCacheSize(111, type: int), - albumThumbnailCacheSize(112, type: int), - selectedAlbumSortOrder(113, type: int), - advancedTroubleshooting(114, type: bool), - logLevel(115, type: int), - preferRemoteImage(116, type: bool), - loopVideo(117, type: bool), + loadPreview._(100), + loadOriginal._(101), + themeMode._(102), + tilesPerRow._(103), + dynamicLayout._(104), + groupAssetsBy._(105), + uploadErrorNotificationGracePeriod._(106), + backgroundBackupTotalProgress._(107), + backgroundBackupSingleProgress._(108), + storageIndicator._(109), + thumbnailCacheSize._(110), + imageCacheSize._(111), + albumThumbnailCacheSize._(112), + selectedAlbumSortOrder._(113), + advancedTroubleshooting._(114), + logLevel._(115), + preferRemoteImage._(116), + loopVideo._(117), // map related settings - mapShowFavoriteOnly(118, type: bool), - mapRelativeDate(119, type: int), - selfSignedCert(120, type: bool), - mapIncludeArchived(121, type: bool), - ignoreIcloudAssets(122, type: bool), - selectedAlbumSortReverse(123, type: bool), - mapThemeMode(124, type: int), - mapwithPartners(125, type: bool), - enableHapticFeedback(126, type: bool), - customHeaders(127, type: String), + mapShowFavoriteOnly._(118), + mapRelativeDate._(119), + selfSignedCert._(120), + mapIncludeArchived._(121), + ignoreIcloudAssets._(122), + selectedAlbumSortReverse._(123), + mapThemeMode._(124), + mapwithPartners._(125), + enableHapticFeedback._(126), + customHeaders._(127), // theme settings - primaryColor(128, type: String), - dynamicTheme(129, type: bool), - colorfulInterface(130, type: bool), + primaryColor._(128), + dynamicTheme._(129), + colorfulInterface._(130), - syncAlbums(131, type: bool), + syncAlbums._(131), // Auto endpoint switching - autoEndpointSwitching(132, type: bool), - preferredWifiName(133, type: String), - localEndpoint(134, type: String), - externalEndpointList(135, type: String), + autoEndpointSwitching._(132), + preferredWifiName._(133), + localEndpoint._(134), + externalEndpointList._(135), // Video settings - loadOriginalVideo(136, type: bool), + loadOriginalVideo._(136), ; - const StoreKey( - this.id, { - required this.type, - this.fromDb, - this.toDb, - }); + const StoreKey._(this.id); final int id; - final Type type; - final T? Function(Isar, int)? fromDb; - final Future Function(Isar, T)? toDb; + Type get type => T; } -T? _getUser(Isar db, int i) { - final User? u = db.users.getSync(i); - return u as T?; -} +class StoreUpdateEvent { + final StoreKey key; + final T? value; -Future _toUser(Isar db, T u) { - if (u is User) { - return db.users.put(u); - } - throw TypeError(); + const StoreUpdateEvent(this.key, this.value); } diff --git a/mobile/lib/infrastructure/README.md b/mobile/lib/infrastructure/README.md new file mode 100644 index 0000000000..8959704270 --- /dev/null +++ b/mobile/lib/infrastructure/README.md @@ -0,0 +1,31 @@ +# Infrastructure Layer + +This directory contains the infrastructure layer of Immich. The infrastructure layer is responsible for the implementation details of the app. It includes data sources, APIs, and other external dependencies. + +## Structure + +- **[Entities](./entities/)**: These are the classes that define the database schema for the domain models. +- **[Repositories](./repositories/)**: These are the actual implementation of the domain interfaces. A single interface might have multiple implementations. +- **[Utils](./utils/)**: These are utility classes and functions specific to infrastructure implementations. + +``` +infrastructure/ +├── entities/ +│ └── user.entity.dart +├── repositories/ +│ └── user.repository.dart +└── utils/ + └── database_utils.dart +``` + +## Usage + +The infrastructure layer provides concrete implementations of repository interfaces defined in the domain layer. These implementations are exposed through Riverpod providers in the root `providers` directory. + +```dart +// In domain/services/user.service.dart +final userRepository = ref.watch(userRepositoryProvider); +final user = await userRepository.getUser(userId); +``` + +The domain layer should never directly instantiate repository implementations, but instead receive them through dependency injection. \ No newline at end of file diff --git a/mobile/lib/infrastructure/entities/store.entity.dart b/mobile/lib/infrastructure/entities/store.entity.dart new file mode 100644 index 0000000000..ef47af8f52 --- /dev/null +++ b/mobile/lib/infrastructure/entities/store.entity.dart @@ -0,0 +1,12 @@ +import 'package:isar/isar.dart'; + +part 'store.entity.g.dart'; + +/// Internal class for `Store`, do not use elsewhere. +@Collection(inheritance: false) +class StoreValue { + const StoreValue(this.id, {this.intValue, this.strValue}); + final Id id; + final int? intValue; + final String? strValue; +} diff --git a/mobile/lib/entities/store.entity.g.dart b/mobile/lib/infrastructure/entities/store.entity.g.dart similarity index 99% rename from mobile/lib/entities/store.entity.g.dart rename to mobile/lib/infrastructure/entities/store.entity.g.dart index 7d3210ff85..b97b5b0a28 100644 --- a/mobile/lib/entities/store.entity.g.dart +++ b/mobile/lib/infrastructure/entities/store.entity.g.dart @@ -105,9 +105,7 @@ List> _storeValueGetLinks(StoreValue object) { return []; } -void _storeValueAttach(IsarCollection col, Id id, StoreValue object) { - object.id = id; -} +void _storeValueAttach(IsarCollection col, Id id, StoreValue object) {} extension StoreValueQueryWhereSort on QueryBuilder { diff --git a/mobile/lib/infrastructure/repositories/db.repository.dart b/mobile/lib/infrastructure/repositories/db.repository.dart new file mode 100644 index 0000000000..74e182bdee --- /dev/null +++ b/mobile/lib/infrastructure/repositories/db.repository.dart @@ -0,0 +1,19 @@ +import 'dart:async'; + +import 'package:immich_mobile/domain/interfaces/db.interface.dart'; +import 'package:isar/isar.dart'; + +// #zoneTxn is the symbol used by Isar to mark a transaction within the current zone +// ref: isar/isar_common.dart +const Symbol _kzoneTxn = #zoneTxn; + +class IsarDatabaseRepository implements IDatabaseRepository { + final Isar _db; + const IsarDatabaseRepository(Isar db) : _db = db; + + // Isar do not support nested transactions. This is a workaround to prevent us from making nested transactions + // Reuse the current transaction if it is already active, else start a new transaction + @override + Future transaction(Future Function() callback) => + Zone.current[_kzoneTxn] == null ? _db.writeTxn(callback) : callback(); +} diff --git a/mobile/lib/infrastructure/repositories/store.repository.dart b/mobile/lib/infrastructure/repositories/store.repository.dart new file mode 100644 index 0000000000..a160a3b48f --- /dev/null +++ b/mobile/lib/infrastructure/repositories/store.repository.dart @@ -0,0 +1,107 @@ +import 'package:immich_mobile/domain/interfaces/store.interface.dart'; +import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/entities/user.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/store.entity.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; +import 'package:immich_mobile/repositories/user.repository.dart'; +import 'package:isar/isar.dart'; + +class IsarStoreRepository extends IsarDatabaseRepository + implements IStoreRepository { + final Isar _db; + const IsarStoreRepository(super.db) : _db = db; + + @override + Future deleteAll() async { + return await transaction(() async { + await _db.storeValues.clear(); + return true; + }); + } + + @override + Stream watchAll() { + return _db.storeValues.where().watch().asyncExpand( + (entities) => + Stream.fromFutures(entities.map((e) async => _toUpdateEvent(e))), + ); + } + + @override + Future delete(StoreKey key) async { + return await transaction(() async => await _db.storeValues.delete(key.id)); + } + + @override + Future insert(StoreKey key, T value) async { + return await transaction(() async { + await _db.storeValues.put(await _fromValue(key, value)); + return true; + }); + } + + @override + Future tryGet(StoreKey key) async { + final entity = (await _db.storeValues.get(key.id)); + if (entity == null) { + return null; + } + return await _toValue(key, entity); + } + + @override + Future update(StoreKey key, T value) async { + return await transaction(() async { + await _db.storeValues.put(await _fromValue(key, value)); + return true; + }); + } + + @override + Stream watch(StoreKey key) async* { + yield* _db.storeValues + .watchObject(key.id, fireImmediately: true) + .asyncMap((e) async => e == null ? null : await _toValue(key, e)); + } + + Future _toUpdateEvent(StoreValue entity) async { + final key = StoreKey.values.firstWhere((e) => e.id == entity.id); + final value = await _toValue(key, entity); + return StoreUpdateEvent(key, value); + } + + Future _toValue(StoreKey key, StoreValue entity) async => + switch (key.type) { + const (int) => entity.intValue, + const (String) => entity.strValue, + const (bool) => entity.intValue == 1, + const (DateTime) => entity.intValue == null + ? null + : DateTime.fromMillisecondsSinceEpoch(entity.intValue!), + const (User) => await UserRepository(_db).getByDbId(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, + ), + const (User) => ( + (await UserRepository(_db).update(value as User)).isarId, + null + ), + _ => throw UnsupportedError( + "Unsupported primitive type: ${key.type} for key: ${key.name}", + ), + }; + return StoreValue(key.id, intValue: intValue, strValue: strValue); + } +} diff --git a/mobile/lib/interfaces/user.interface.dart b/mobile/lib/interfaces/user.interface.dart index 601730f3f8..d099e0e50b 100644 --- a/mobile/lib/interfaces/user.interface.dart +++ b/mobile/lib/interfaces/user.interface.dart @@ -4,6 +4,8 @@ import 'package:immich_mobile/interfaces/database.interface.dart'; abstract interface class IUserRepository implements IDatabaseRepository { Future get(String id); + Future getByDbId(int id); + Future> getByIds(List ids); Future> getAll({bool self = true, UserSort? sortBy}); diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 139366b359..822d772278 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -4,45 +4,48 @@ import 'dart:io'; import 'package:background_downloader/background_downloader.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart'; -import 'package:intl/date_symbol_data_local.dart'; -import 'package:timezone/data/latest.dart'; -import 'package:isar/isar.dart'; -import 'package:logging/logging.dart'; -import 'package:path_provider/path_provider.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_displaymode/flutter_displaymode.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/constants/locales.dart'; -import 'package:immich_mobile/providers/locale_provider.dart'; -import 'package:immich_mobile/providers/theme.provider.dart'; -import 'package:immich_mobile/providers/app_life_cycle.provider.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/routing/tab_navigation_observer.dart'; -import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/entities/duplicated_asset.entity.dart'; +import 'package:immich_mobile/domain/services/store.service.dart'; import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/android_device_asset.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/entities/backup_album.entity.dart'; +import 'package:immich_mobile/entities/duplicated_asset.entity.dart'; import 'package:immich_mobile/entities/etag.entity.dart'; import 'package:immich_mobile/entities/exif_info.entity.dart'; import 'package:immich_mobile/entities/ios_device_asset.entity.dart'; import 'package:immich_mobile/entities/logger_message.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/user.entity.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/infrastructure/entities/store.entity.dart'; +import 'package:immich_mobile/infrastructure/repositories/store.repository.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/db.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; +import 'package:immich_mobile/providers/locale_provider.dart'; +import 'package:immich_mobile/providers/theme.provider.dart'; +import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/routing/tab_navigation_observer.dart'; import 'package:immich_mobile/services/background.service.dart'; import 'package:immich_mobile/services/immich_logger.service.dart'; import 'package:immich_mobile/services/local_notification.service.dart'; -import 'package:immich_mobile/utils/migration.dart'; -import 'package:immich_mobile/utils/download.dart'; -import 'package:immich_mobile/utils/cache/widgets_binding.dart'; -import 'package:immich_mobile/utils/http_ssl_cert_override.dart'; -import 'package:immich_mobile/theme/theme_data.dart'; import 'package:immich_mobile/theme/dynamic_theme.dart'; +import 'package:immich_mobile/theme/theme_data.dart'; +import 'package:immich_mobile/utils/cache/widgets_binding.dart'; +import 'package:immich_mobile/utils/download.dart'; +import 'package:immich_mobile/utils/http_ssl_cert_override.dart'; +import 'package:immich_mobile/utils/migration.dart'; +import 'package:intl/date_symbol_data_local.dart'; +import 'package:isar/isar.dart'; +import 'package:logging/logging.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:timezone/data/latest.dart'; void main() async { ImmichWidgetsBinding(); @@ -53,7 +56,10 @@ void main() async { runApp( ProviderScope( - overrides: [dbProvider.overrideWithValue(db)], + overrides: [ + dbProvider.overrideWithValue(db), + isarProvider.overrideWithValue(db), + ], child: const MainWidget(), ), ); @@ -135,7 +141,7 @@ Future loadDb() async { directory: dir.path, maxSizeMiB: 1024, ); - Store.init(db); + await StoreService.init(storeRepository: IsarStoreRepository(db)); return db; } diff --git a/mobile/lib/providers/auth.provider.dart b/mobile/lib/providers/auth.provider.dart index a23ffd3d68..573c490e7c 100644 --- a/mobile/lib/providers/auth.provider.dart +++ b/mobile/lib/providers/auth.provider.dart @@ -2,9 +2,9 @@ import 'package:flutter/foundation.dart'; import 'package:flutter_udid/flutter_udid.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/models/auth/login_response.model.dart'; -import 'package:immich_mobile/models/auth/auth_state.model.dart'; import 'package:immich_mobile/entities/user.entity.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'; import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/auth.service.dart'; @@ -98,7 +98,7 @@ class AuthNotifier extends StateNotifier { Future saveAuthInfo({ required String accessToken, }) async { - _apiService.setAccessToken(accessToken); + await _apiService.setAccessToken(accessToken); // Get the deviceid from the store if it exists, otherwise generate a new one String deviceId = @@ -141,13 +141,13 @@ 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 if (userResponse != null) { - Store.put(StoreKey.deviceId, deviceId); - Store.put(StoreKey.deviceIdHash, fastHash(deviceId)); - Store.put( + await Store.put(StoreKey.deviceId, deviceId); + await Store.put(StoreKey.deviceIdHash, fastHash(deviceId)); + await Store.put( StoreKey.currentUser, User.fromUserDto(userResponse, userPreferences), ); - Store.put(StoreKey.accessToken, accessToken); + await Store.put(StoreKey.accessToken, accessToken); user = User.fromUserDto(userResponse, userPreferences); } else { @@ -173,12 +173,12 @@ class AuthNotifier extends StateNotifier { return true; } - Future saveWifiName(String wifiName) { - return Store.put(StoreKey.preferredWifiName, wifiName); + Future saveWifiName(String wifiName) async { + await Store.put(StoreKey.preferredWifiName, wifiName); } - Future saveLocalEndpoint(String url) { - return Store.put(StoreKey.localEndpoint, url); + Future saveLocalEndpoint(String url) async { + await Store.put(StoreKey.localEndpoint, url); } String? getSavedWifiName() { diff --git a/mobile/lib/providers/infrastructure/db.provider.dart b/mobile/lib/providers/infrastructure/db.provider.dart new file mode 100644 index 0000000000..447039478e --- /dev/null +++ b/mobile/lib/providers/infrastructure/db.provider.dart @@ -0,0 +1,7 @@ +import 'package:isar/isar.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'db.provider.g.dart'; + +@Riverpod(keepAlive: true) +Isar isar(IsarRef ref) => throw UnimplementedError('isar'); diff --git a/mobile/lib/providers/infrastructure/db.provider.g.dart b/mobile/lib/providers/infrastructure/db.provider.g.dart new file mode 100644 index 0000000000..a6122394ea --- /dev/null +++ b/mobile/lib/providers/infrastructure/db.provider.g.dart @@ -0,0 +1,24 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'db.provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$isarHash() => r'69d3a06aa7e69a4381478e03f7956eb07d7f7feb'; + +/// See also [isar]. +@ProviderFor(isar) +final isarProvider = Provider.internal( + isar, + name: r'isarProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$isarHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef IsarRef = ProviderRef; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/mobile/lib/providers/infrastructure/store.provider.dart b/mobile/lib/providers/infrastructure/store.provider.dart new file mode 100644 index 0000000000..cb7024ad51 --- /dev/null +++ b/mobile/lib/providers/infrastructure/store.provider.dart @@ -0,0 +1,10 @@ +import 'package:immich_mobile/domain/interfaces/store.interface.dart'; +import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; +import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'store.provider.g.dart'; + +@riverpod +IStoreRepository storeRepository(StoreRepositoryRef ref) => + IsarStoreRepository(ref.watch(isarProvider)); diff --git a/mobile/lib/providers/infrastructure/store.provider.g.dart b/mobile/lib/providers/infrastructure/store.provider.g.dart new file mode 100644 index 0000000000..69523af272 --- /dev/null +++ b/mobile/lib/providers/infrastructure/store.provider.g.dart @@ -0,0 +1,25 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'store.provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$storeRepositoryHash() => r'9f378b96e552151fa14a8c8ce2c30a5f38f436ed'; + +/// See also [storeRepository]. +@ProviderFor(storeRepository) +final storeRepositoryProvider = AutoDisposeProvider.internal( + storeRepository, + name: r'storeRepositoryProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$storeRepositoryHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef StoreRepositoryRef = AutoDisposeProviderRef; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/mobile/lib/providers/user.provider.dart b/mobile/lib/providers/user.provider.dart index 971cfd5103..73fc283109 100644 --- a/mobile/lib/providers/user.provider.dart +++ b/mobile/lib/providers/user.provider.dart @@ -23,7 +23,7 @@ class CurrentUserProvider extends StateNotifier { final user = await _apiService.usersApi.getMyUser(); final userPreferences = await _apiService.usersApi.getMyPreferences(); if (user != null) { - Store.put( + await Store.put( StoreKey.currentUser, User.fromUserDto(user, userPreferences), ); diff --git a/mobile/lib/repositories/user.repository.dart b/mobile/lib/repositories/user.repository.dart index e490c7d8c1..4f1a9e7267 100644 --- a/mobile/lib/repositories/user.repository.dart +++ b/mobile/lib/repositories/user.repository.dart @@ -58,6 +58,11 @@ class UserRepository extends DatabaseRepository implements IUserRepository { .isarIdEqualTo(Store.get(StoreKey.currentUser).isarId) .findAll(); + @override + Future getByDbId(int id) async { + return await db.users.get(id); + } + @override Future clearTable() async { await txn(() async { diff --git a/mobile/lib/routing/auth_guard.dart b/mobile/lib/routing/auth_guard.dart index c99a890fc8..65631911ec 100644 --- a/mobile/lib/routing/auth_guard.dart +++ b/mobile/lib/routing/auth_guard.dart @@ -1,8 +1,9 @@ import 'dart:io'; import 'package:auto_route/auto_route.dart'; -import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/domain/services/store.service.dart'; import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; diff --git a/mobile/lib/routing/tab_navigation_observer.dart b/mobile/lib/routing/tab_navigation_observer.dart index 7d96b83d02..f48e35c813 100644 --- a/mobile/lib/routing/tab_navigation_observer.dart +++ b/mobile/lib/routing/tab_navigation_observer.dart @@ -1,12 +1,11 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/foundation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/providers/memory.provider.dart'; - import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/user.entity.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/asset.provider.dart'; +import 'package:immich_mobile/providers/memory.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; class TabNavigationObserver extends AutoRouterObserver { @@ -37,7 +36,7 @@ class TabNavigationObserver extends AutoRouterObserver { return; } - Store.put( + await Store.put( StoreKey.currentUser, User.fromUserDto(userResponseDto, userPreferences), ); diff --git a/mobile/lib/services/api.service.dart b/mobile/lib/services/api.service.dart index c72a4bf1bc..17e90cd009 100644 --- a/mobile/lib/services/api.service.dart +++ b/mobile/lib/services/api.service.dart @@ -4,11 +4,11 @@ import 'dart:io'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/material.dart'; +import 'package:http/http.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/utils/url_helper.dart'; import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; -import 'package:http/http.dart'; class ApiService implements Authentication { late ApiClient _apiClient; @@ -147,9 +147,9 @@ class ApiService implements Authentication { return ""; } - void setAccessToken(String accessToken) { + Future setAccessToken(String accessToken) async { _accessToken = accessToken; - Store.put(StoreKey.accessToken, accessToken); + await Store.put(StoreKey.accessToken, accessToken); } Future setDeviceInfoHeader() async { diff --git a/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart b/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart index f0006d1ada..bf01d8643a 100644 --- a/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart +++ b/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart @@ -1,14 +1,14 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:image_picker/image_picker.dart'; +import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/providers/upload_profile_image.provider.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; import 'package:immich_mobile/providers/auth.provider.dart'; +import 'package:immich_mobile/providers/upload_profile_image.provider.dart'; +import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart'; +import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; class AppBarProfileInfoBox extends HookConsumerWidget { const AppBarProfileInfoBox({ @@ -67,7 +67,7 @@ class AppBarProfileInfoBox extends HookConsumerWidget { ); if (user != null) { user.profileImagePath = profileImagePath; - Store.put(StoreKey.currentUser, user); + await Store.put(StoreKey.currentUser, user); ref.read(currentUserProvider.notifier).refresh(); } } diff --git a/mobile/test/modules/activity/activities_page_test.dart b/mobile/test/modules/activity/activities_page_test.dart index a5dda5dc44..38070966c8 100644 --- a/mobile/test/modules/activity/activities_page_test.dart +++ b/mobile/test/modules/activity/activities_page_test.dart @@ -4,18 +4,20 @@ library; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/models/activities/activity.model.dart'; -import 'package:immich_mobile/providers/activity.provider.dart'; -import 'package:immich_mobile/pages/common/activities.page.dart'; -import 'package:immich_mobile/widgets/activities/activity_text_field.dart'; -import 'package:immich_mobile/widgets/activities/dismissible_activity.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; +import 'package:immich_mobile/domain/services/store.service.dart'; import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/user.entity.dart'; +import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; +import 'package:immich_mobile/models/activities/activity.model.dart'; +import 'package:immich_mobile/pages/common/activities.page.dart'; +import 'package:immich_mobile/providers/activity.provider.dart'; +import 'package:immich_mobile/providers/album/current_album.provider.dart'; +import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/widgets/activities/activity_text_field.dart'; +import 'package:immich_mobile/widgets/activities/dismissible_activity.dart'; import 'package:isar/isar.dart'; import 'package:mocktail/mocktail.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -25,8 +27,8 @@ import '../../fixtures/asset.stub.dart'; import '../../fixtures/user.stub.dart'; import '../../test_utils.dart'; import '../../widget_tester_extensions.dart'; -import '../asset_viewer/asset_viewer_mocks.dart'; import '../album/album_mocks.dart'; +import '../asset_viewer/asset_viewer_mocks.dart'; import '../shared/shared_mocks.dart'; import 'activity_mocks.dart'; @@ -71,7 +73,7 @@ void main() { setUpAll(() async { TestUtils.init(); db = await TestUtils.initIsar(); - Store.init(db); + await StoreService.init(storeRepository: IsarStoreRepository(db)); Store.put(StoreKey.currentUser, UserStub.admin); Store.put(StoreKey.serverEndpoint, ''); Store.put(StoreKey.accessToken, ''); diff --git a/mobile/test/modules/activity/activity_text_field_test.dart b/mobile/test/modules/activity/activity_text_field_test.dart index caa742873a..1e94f1ddeb 100644 --- a/mobile/test/modules/activity/activity_text_field_test.dart +++ b/mobile/test/modules/activity/activity_text_field_test.dart @@ -4,11 +4,13 @@ library; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/providers/activity.provider.dart'; -import 'package:immich_mobile/widgets/activities/activity_text_field.dart'; -import 'package:immich_mobile/providers/album/current_album.provider.dart'; +import 'package:immich_mobile/domain/services/store.service.dart'; import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; +import 'package:immich_mobile/providers/activity.provider.dart'; +import 'package:immich_mobile/providers/album/current_album.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; +import 'package:immich_mobile/widgets/activities/activity_text_field.dart'; import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; import 'package:isar/isar.dart'; import 'package:mocktail/mocktail.dart'; @@ -31,7 +33,7 @@ void main() { setUpAll(() async { TestUtils.init(); db = await TestUtils.initIsar(); - Store.init(db); + await StoreService.init(storeRepository: IsarStoreRepository(db)); Store.put(StoreKey.currentUser, UserStub.admin); Store.put(StoreKey.serverEndpoint, ''); }); diff --git a/mobile/test/modules/activity/activity_tile_test.dart b/mobile/test/modules/activity/activity_tile_test.dart index f64eea851a..fb48359dd5 100644 --- a/mobile/test/modules/activity/activity_tile_test.dart +++ b/mobile/test/modules/activity/activity_tile_test.dart @@ -5,10 +5,12 @@ library; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/models/activities/activity.model.dart'; -import 'package:immich_mobile/widgets/activities/activity_tile.dart'; -import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; +import 'package:immich_mobile/domain/services/store.service.dart'; import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; +import 'package:immich_mobile/models/activities/activity.model.dart'; +import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; +import 'package:immich_mobile/widgets/activities/activity_tile.dart'; import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; import 'package:isar/isar.dart'; @@ -27,7 +29,7 @@ void main() { TestUtils.init(); db = await TestUtils.initIsar(); // For UserCircleAvatar - Store.init(db); + await StoreService.init(storeRepository: IsarStoreRepository(db)); Store.put(StoreKey.currentUser, UserStub.admin); Store.put(StoreKey.serverEndpoint, ''); Store.put(StoreKey.accessToken, ''); diff --git a/mobile/test/modules/map/map_theme_override_test.dart b/mobile/test/modules/map/map_theme_override_test.dart index bd000c8715..5a6b163c04 100644 --- a/mobile/test/modules/map/map_theme_override_test.dart +++ b/mobile/test/modules/map/map_theme_override_test.dart @@ -4,10 +4,13 @@ library; 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/store.repository.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'; import 'package:immich_mobile/widgets/map/map_theme_override.dart'; +import 'package:isar/isar.dart'; import '../../test_utils.dart'; import '../../widget_tester_extensions.dart'; @@ -17,14 +20,17 @@ void main() { late MockMapStateNotifier mapStateNotifier; late List overrides; late MapState mapState; + late Isar db; setUpAll(() async { TestUtils.init(); + db = await TestUtils.initIsar(); }); - setUp(() { + setUp(() async { mapState = MapState(themeMode: ThemeMode.dark); mapStateNotifier = MockMapStateNotifier(mapState); + await StoreService.init(storeRepository: IsarStoreRepository(db)); overrides = [ mapStateNotifierProvider.overrideWith(() => mapStateNotifier), localeProvider.overrideWithValue(const Locale("en")), diff --git a/mobile/test/modules/shared/sync_service_test.dart b/mobile/test/modules/shared/sync_service_test.dart index 5eca5016fd..a87d422b40 100644 --- a/mobile/test/modules/shared/sync_service_test.dart +++ b/mobile/test/modules/shared/sync_service_test.dart @@ -1,9 +1,11 @@ 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/entities/asset.entity.dart'; import 'package:immich_mobile/entities/etag.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/user.entity.dart'; +import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:immich_mobile/interfaces/asset.interface.dart'; import 'package:immich_mobile/interfaces/user.interface.dart'; import 'package:immich_mobile/services/immich_logger.service.dart'; @@ -63,10 +65,11 @@ void main() { setUpAll(() async { WidgetsFlutterBinding.ensureInitialized(); final db = await TestUtils.initIsar(); - ImmichLogger(); + db.writeTxnSync(() => db.clearSync()); - Store.init(db); + await StoreService.init(storeRepository: IsarStoreRepository(db)); await Store.put(StoreKey.currentUser, owner); + ImmichLogger(); }); final List initialAssets = [ makeAsset(checksum: "a", remoteId: "0-1"), diff --git a/mobile/test/pages/search/search.page_test.dart b/mobile/test/pages/search/search.page_test.dart index 32b56e9ad3..de19acc5a3 100644 --- a/mobile/test/pages/search/search.page_test.dart +++ b/mobile/test/pages/search/search.page_test.dart @@ -5,10 +5,12 @@ library; import 'package:flutter/material.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/domain/services/store.service.dart'; +import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:immich_mobile/pages/search/search.page.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/db.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/providers/search/paginated_search.provider.dart'; import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; import 'package:isar/isar.dart'; @@ -29,7 +31,7 @@ void main() { setUpAll(() async { TestUtils.init(); db = await TestUtils.initIsar(); - Store.init(db); + await StoreService.init(storeRepository: IsarStoreRepository(db)); mockApiService = MockApiService(); mockSearchApi = MockSearchApi(); when(() => mockApiService.searchApi).thenReturn(mockSearchApi); @@ -39,6 +41,7 @@ void main() { paginatedSearchRenderListProvider .overrideWithValue(AsyncValue.data(RenderList.empty())), dbProvider.overrideWithValue(db), + isarProvider.overrideWithValue(db), apiServiceProvider.overrideWithValue(mockApiService), ]; }); diff --git a/mobile/test/services/auth.service_test.dart b/mobile/test/services/auth.service_test.dart index edbf6495e3..e4f011d940 100644 --- a/mobile/test/services/auth.service_test.dart +++ b/mobile/test/services/auth.service_test.dart @@ -1,10 +1,13 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/domain/services/store.service.dart'; +import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart'; import 'package:immich_mobile/services/auth.service.dart'; +import 'package:isar/isar.dart'; import 'package:mocktail/mocktail.dart'; import 'package:openapi/api.dart'; + import '../repository.mocks.dart'; import '../service.mocks.dart'; import '../test_utils.dart'; @@ -15,6 +18,7 @@ void main() { late MockAuthRepository authRepository; late MockApiService apiService; late MockNetworkService networkService; + late Isar db; setUp(() async { authApiRepository = MockAuthApiRepository(); @@ -32,12 +36,18 @@ void main() { registerFallbackValue(Uri()); }); + setUpAll(() async { + db = await TestUtils.initIsar(); + db.writeTxnSync(() => db.clearSync()); + await StoreService.init(storeRepository: IsarStoreRepository(db)); + }); + group('validateServerUrl', () { setUpAll(() async { WidgetsFlutterBinding.ensureInitialized(); final db = await TestUtils.initIsar(); db.writeTxnSync(() => db.clearSync()); - Store.init(db); + await StoreService.init(storeRepository: IsarStoreRepository(db)); }); test('Should resolve HTTP endpoint', () async { diff --git a/mobile/test/test_utils.dart b/mobile/test/test_utils.dart index 7afd209f10..39837b6e56 100644 --- a/mobile/test/test_utils.dart +++ b/mobile/test/test_utils.dart @@ -3,17 +3,17 @@ import 'dart:io'; 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/backup_album.entity.dart'; -import 'package:immich_mobile/entities/duplicated_asset.entity.dart'; import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/android_device_asset.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/entities/backup_album.entity.dart'; +import 'package:immich_mobile/entities/duplicated_asset.entity.dart'; import 'package:immich_mobile/entities/etag.entity.dart'; import 'package:immich_mobile/entities/exif_info.entity.dart'; import 'package:immich_mobile/entities/ios_device_asset.entity.dart'; import 'package:immich_mobile/entities/logger_message.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/user.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/store.entity.dart'; import 'package:isar/isar.dart'; import 'package:mocktail/mocktail.dart';