1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-04-16 13:06:24 +02:00

refactor(mobile): split store into repo and service ()

* refactor(mobile): migrate store

* refactor(mobile): expand abbreviations

* chore(mobile): fix lint

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
shenlong 2025-02-20 00:35:24 +05:30 committed by GitHub
parent 8634c59850
commit aeb3e0a84f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 582 additions and 287 deletions

View file

@ -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

View file

@ -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(),
),
);

View file

@ -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.

View file

@ -0,0 +1,3 @@
abstract interface class IDatabaseRepository {
Future<T> transaction<T>(Future<T> Function() callback);
}

View file

@ -0,0 +1,17 @@
import 'package:immich_mobile/entities/store.entity.dart';
abstract interface class IStoreRepository {
Future<bool> insert<T>(StoreKey<T> key, T value);
Future<T?> tryGet<T>(StoreKey<T> key);
Stream<T?> watch<T>(StoreKey<T> key);
Stream<StoreUpdateEvent> watchAll();
Future<bool> update<T>(StoreKey<T> key, T value);
Future<void> delete<T>(StoreKey<T> key);
Future<void> deleteAll();
}

View file

@ -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<int, dynamic> _cache = {};
late final StreamSubscription<StoreUpdateEvent> _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<StoreService> init({
required IStoreRepository storeRepository,
}) async {
_instance ??= await create(storeRepository: storeRepository);
return _instance!;
}
/// Initializes the store with the given [storeRepository]
static Future<StoreService> 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<void> _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<StoreUpdateEvent> _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<T>(StoreKey<T> 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<T>(StoreKey<T> 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<void> put<T>(StoreKey<T> 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<T?> watch<T>(StoreKey<T> key) => _storeRepository.watch(key);
/// Removes the value asynchronously from the DB and synchronously from the cache
Future<void> delete<T>(StoreKey<T> key) async {
await _storeRepository.delete(key);
_cache.remove(key.id);
}
/// Clears all values from this store (cache and DB)
Future<void> 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";
}

View file

@ -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<dynamic> _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<void> 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<T>(StoreKey<T> 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<T?> watch<T>(StoreKey<T> key) =>
_db.storeValues.watchObject(key.id).map((e) => e?._extract(key));
/// Returns the stored value for the given key (possibly null)
static T? tryGet<T>(StoreKey<T> key) => _cache[key.id];
/// Stores the value synchronously in the cache and asynchronously in the DB
static Future<void> put<T>(StoreKey<T> 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<void> delete<T>(StoreKey<T> 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<StoreValue>? 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<T>(StoreKey<T> 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<StoreValue> _of<T>(T? value, StoreKey<T> 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<T> {
version<int>(0, type: int),
assetETag<String>(1, type: String),
currentUser<User>(2, type: User, fromDb: _getUser, toDb: _toUser),
deviceIdHash<int>(3, type: int),
deviceId<String>(4, type: String),
backupFailedSince<DateTime>(5, type: DateTime),
backupRequireWifi<bool>(6, type: bool),
backupRequireCharging<bool>(7, type: bool),
backupTriggerDelay<int>(8, type: int),
serverUrl<String>(10, type: String),
accessToken<String>(11, type: String),
serverEndpoint<String>(12, type: String),
autoBackup<bool>(13, type: bool),
backgroundBackup<bool>(14, type: bool),
sslClientCertData<String>(15, type: String),
sslClientPasswd<String>(16, type: String),
version<int>._(0),
assetETag<String>._(1),
currentUser<User>._(2),
deviceIdHash<int>._(3),
deviceId<String>._(4),
backupFailedSince<DateTime>._(5),
backupRequireWifi<bool>._(6),
backupRequireCharging<bool>._(7),
backupTriggerDelay<int>._(8),
serverUrl<String>._(10),
accessToken<String>._(11),
serverEndpoint<String>._(12),
autoBackup<bool>._(13),
backgroundBackup<bool>._(14),
sslClientCertData<String>._(15),
sslClientPasswd<String>._(16),
// user settings from [AppSettingsEnum] below:
loadPreview<bool>(100, type: bool),
loadOriginal<bool>(101, type: bool),
themeMode<String>(102, type: String),
tilesPerRow<int>(103, type: int),
dynamicLayout<bool>(104, type: bool),
groupAssetsBy<int>(105, type: int),
uploadErrorNotificationGracePeriod<int>(106, type: int),
backgroundBackupTotalProgress<bool>(107, type: bool),
backgroundBackupSingleProgress<bool>(108, type: bool),
storageIndicator<bool>(109, type: bool),
thumbnailCacheSize<int>(110, type: int),
imageCacheSize<int>(111, type: int),
albumThumbnailCacheSize<int>(112, type: int),
selectedAlbumSortOrder<int>(113, type: int),
advancedTroubleshooting<bool>(114, type: bool),
logLevel<int>(115, type: int),
preferRemoteImage<bool>(116, type: bool),
loopVideo<bool>(117, type: bool),
loadPreview<bool>._(100),
loadOriginal<bool>._(101),
themeMode<String>._(102),
tilesPerRow<int>._(103),
dynamicLayout<bool>._(104),
groupAssetsBy<int>._(105),
uploadErrorNotificationGracePeriod<int>._(106),
backgroundBackupTotalProgress<bool>._(107),
backgroundBackupSingleProgress<bool>._(108),
storageIndicator<bool>._(109),
thumbnailCacheSize<int>._(110),
imageCacheSize<int>._(111),
albumThumbnailCacheSize<int>._(112),
selectedAlbumSortOrder<int>._(113),
advancedTroubleshooting<bool>._(114),
logLevel<int>._(115),
preferRemoteImage<bool>._(116),
loopVideo<bool>._(117),
// map related settings
mapShowFavoriteOnly<bool>(118, type: bool),
mapRelativeDate<int>(119, type: int),
selfSignedCert<bool>(120, type: bool),
mapIncludeArchived<bool>(121, type: bool),
ignoreIcloudAssets<bool>(122, type: bool),
selectedAlbumSortReverse<bool>(123, type: bool),
mapThemeMode<int>(124, type: int),
mapwithPartners<bool>(125, type: bool),
enableHapticFeedback<bool>(126, type: bool),
customHeaders<String>(127, type: String),
mapShowFavoriteOnly<bool>._(118),
mapRelativeDate<int>._(119),
selfSignedCert<bool>._(120),
mapIncludeArchived<bool>._(121),
ignoreIcloudAssets<bool>._(122),
selectedAlbumSortReverse<bool>._(123),
mapThemeMode<int>._(124),
mapwithPartners<bool>._(125),
enableHapticFeedback<bool>._(126),
customHeaders<String>._(127),
// theme settings
primaryColor<String>(128, type: String),
dynamicTheme<bool>(129, type: bool),
colorfulInterface<bool>(130, type: bool),
primaryColor<String>._(128),
dynamicTheme<bool>._(129),
colorfulInterface<bool>._(130),
syncAlbums<bool>(131, type: bool),
syncAlbums<bool>._(131),
// Auto endpoint switching
autoEndpointSwitching<bool>(132, type: bool),
preferredWifiName<String>(133, type: String),
localEndpoint<String>(134, type: String),
externalEndpointList<String>(135, type: String),
autoEndpointSwitching<bool>._(132),
preferredWifiName<String>._(133),
localEndpoint<String>._(134),
externalEndpointList<String>._(135),
// Video settings
loadOriginalVideo<bool>(136, type: bool),
loadOriginalVideo<bool>._(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<T>(Isar, int)? fromDb;
final Future<int> Function<T>(Isar, T)? toDb;
Type get type => T;
}
T? _getUser<T>(Isar db, int i) {
final User? u = db.users.getSync(i);
return u as T?;
}
class StoreUpdateEvent<T> {
final StoreKey<T> key;
final T? value;
Future<int> _toUser<T>(Isar db, T u) {
if (u is User) {
return db.users.put(u);
}
throw TypeError();
const StoreUpdateEvent(this.key, this.value);
}

View file

@ -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.

View file

@ -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;
}

View file

@ -105,9 +105,7 @@ List<IsarLinkBase<dynamic>> _storeValueGetLinks(StoreValue object) {
return [];
}
void _storeValueAttach(IsarCollection<dynamic> col, Id id, StoreValue object) {
object.id = id;
}
void _storeValueAttach(IsarCollection<dynamic> col, Id id, StoreValue object) {}
extension StoreValueQueryWhereSort
on QueryBuilder<StoreValue, StoreValue, QWhere> {

View file

@ -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<T> transaction<T>(Future<T> Function() callback) =>
Zone.current[_kzoneTxn] == null ? _db.writeTxn(callback) : callback();
}

View file

@ -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<bool> deleteAll() async {
return await transaction(() async {
await _db.storeValues.clear();
return true;
});
}
@override
Stream<StoreUpdateEvent> watchAll() {
return _db.storeValues.where().watch().asyncExpand(
(entities) =>
Stream.fromFutures(entities.map((e) async => _toUpdateEvent(e))),
);
}
@override
Future<void> delete<T>(StoreKey<T> key) async {
return await transaction(() async => await _db.storeValues.delete(key.id));
}
@override
Future<bool> insert<T>(StoreKey<T> key, T value) async {
return await transaction(() async {
await _db.storeValues.put(await _fromValue(key, value));
return true;
});
}
@override
Future<T?> tryGet<T>(StoreKey<T> key) async {
final entity = (await _db.storeValues.get(key.id));
if (entity == null) {
return null;
}
return await _toValue(key, entity);
}
@override
Future<bool> update<T>(StoreKey<T> key, T value) async {
return await transaction(() async {
await _db.storeValues.put(await _fromValue(key, value));
return true;
});
}
@override
Stream<T?> watch<T>(StoreKey<T> key) async* {
yield* _db.storeValues
.watchObject(key.id, fireImmediately: true)
.asyncMap((e) async => e == null ? null : await _toValue(key, e));
}
Future<StoreUpdateEvent> _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<T?> _toValue<T>(StoreKey<T> 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<StoreValue> _fromValue<T>(StoreKey<T> key, T value) async {
final (int? intValue, String? strValue) = switch (key.type) {
const (int) => (value as int, null),
const (String) => (null, value as String),
const (bool) => (
(value as bool) ? 1 : 0,
null,
),
const (DateTime) => (
(value as DateTime).millisecondsSinceEpoch,
null,
),
const (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);
}
}

View file

@ -4,6 +4,8 @@ import 'package:immich_mobile/interfaces/database.interface.dart';
abstract interface class IUserRepository implements IDatabaseRepository {
Future<User?> get(String id);
Future<User?> getByDbId(int id);
Future<List<User>> getByIds(List<String> ids);
Future<List<User>> getAll({bool self = true, UserSort? sortBy});

View file

@ -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<Isar> loadDb() async {
directory: dir.path,
maxSizeMiB: 1024,
);
Store.init(db);
await StoreService.init(storeRepository: IsarStoreRepository(db));
return db;
}

View file

@ -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<AuthState> {
Future<bool> 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<AuthState> {
// 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<AuthState> {
return true;
}
Future<void> saveWifiName(String wifiName) {
return Store.put(StoreKey.preferredWifiName, wifiName);
Future<void> saveWifiName(String wifiName) async {
await Store.put(StoreKey.preferredWifiName, wifiName);
}
Future<void> saveLocalEndpoint(String url) {
return Store.put(StoreKey.localEndpoint, url);
Future<void> saveLocalEndpoint(String url) async {
await Store.put(StoreKey.localEndpoint, url);
}
String? getSavedWifiName() {

View file

@ -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');

View file

@ -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<Isar>.internal(
isar,
name: r'isarProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$isarHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef IsarRef = ProviderRef<Isar>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member

View file

@ -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));

View file

@ -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<IStoreRepository>.internal(
storeRepository,
name: r'storeRepositoryProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$storeRepositoryHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef StoreRepositoryRef = AutoDisposeProviderRef<IStoreRepository>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member

View file

@ -23,7 +23,7 @@ class CurrentUserProvider extends StateNotifier<User?> {
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),
);

View file

@ -58,6 +58,11 @@ class UserRepository extends DatabaseRepository implements IUserRepository {
.isarIdEqualTo(Store.get(StoreKey.currentUser).isarId)
.findAll();
@override
Future<User?> getByDbId(int id) async {
return await db.users.get(id);
}
@override
Future<void> clearTable() async {
await txn(() async {

View file

@ -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';

View file

@ -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),
);

View file

@ -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<void> setAccessToken(String accessToken) async {
_accessToken = accessToken;
Store.put(StoreKey.accessToken, accessToken);
await Store.put(StoreKey.accessToken, accessToken);
}
Future<void> setDeviceInfoHeader() async {

View file

@ -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();
}
}

View file

@ -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, '');

View file

@ -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, '');
});

View file

@ -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, '');

View file

@ -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<Override> 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")),

View file

@ -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<Asset> initialAssets = [
makeAsset(checksum: "a", remoteId: "0-1"),

View file

@ -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),
];
});

View file

@ -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 {

View file

@ -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';