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 (#16199)
* 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:
parent
8634c59850
commit
aeb3e0a84f
33 changed files with 582 additions and 287 deletions
mobile
analysis_options.yaml
integration_test/test_utils
lib
domain
entities
infrastructure
interfaces
main.dartproviders
repositories
routing
services
widgets/common/app_bar_dialog
test
|
@ -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
|
||||
|
|
|
@ -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(),
|
||||
),
|
||||
);
|
||||
|
|
34
mobile/lib/domain/README.md
Normal file
34
mobile/lib/domain/README.md
Normal 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.
|
3
mobile/lib/domain/interfaces/db.interface.dart
Normal file
3
mobile/lib/domain/interfaces/db.interface.dart
Normal file
|
@ -0,0 +1,3 @@
|
|||
abstract interface class IDatabaseRepository {
|
||||
Future<T> transaction<T>(Future<T> Function() callback);
|
||||
}
|
17
mobile/lib/domain/interfaces/store.interface.dart
Normal file
17
mobile/lib/domain/interfaces/store.interface.dart
Normal 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();
|
||||
}
|
106
mobile/lib/domain/services/store.service.dart
Normal file
106
mobile/lib/domain/services/store.service.dart
Normal 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";
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
31
mobile/lib/infrastructure/README.md
Normal file
31
mobile/lib/infrastructure/README.md
Normal 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.
|
12
mobile/lib/infrastructure/entities/store.entity.dart
Normal file
12
mobile/lib/infrastructure/entities/store.entity.dart
Normal 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;
|
||||
}
|
|
@ -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> {
|
19
mobile/lib/infrastructure/repositories/db.repository.dart
Normal file
19
mobile/lib/infrastructure/repositories/db.repository.dart
Normal 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();
|
||||
}
|
107
mobile/lib/infrastructure/repositories/store.repository.dart
Normal file
107
mobile/lib/infrastructure/repositories/store.repository.dart
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
7
mobile/lib/providers/infrastructure/db.provider.dart
Normal file
7
mobile/lib/providers/infrastructure/db.provider.dart
Normal 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');
|
24
mobile/lib/providers/infrastructure/db.provider.g.dart
generated
Normal file
24
mobile/lib/providers/infrastructure/db.provider.g.dart
generated
Normal 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
|
10
mobile/lib/providers/infrastructure/store.provider.dart
Normal file
10
mobile/lib/providers/infrastructure/store.provider.dart
Normal 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));
|
25
mobile/lib/providers/infrastructure/store.provider.g.dart
generated
Normal file
25
mobile/lib/providers/infrastructure/store.provider.g.dart
generated
Normal 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
|
|
@ -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),
|
||||
);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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),
|
||||
);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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, '');
|
||||
|
|
|
@ -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, '');
|
||||
});
|
||||
|
|
|
@ -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, '');
|
||||
|
|
|
@ -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")),
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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),
|
||||
];
|
||||
});
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue