add proper logging

This commit is contained in:
shenlong-tanwen
2024-08-21 23:43:48 +05:30
parent 1631df70e9
commit 75448ce56b
37 changed files with 923 additions and 224 deletions
@@ -4,7 +4,7 @@ class LocalAlbum extends Table {
const LocalAlbum();
IntColumn get id => integer().autoIncrement()();
TextColumn get localId => text()();
TextColumn get localId => text().unique()();
TextColumn get name => text()();
DateTimeColumn get modifiedTime =>
dateTime().withDefault(currentDateAndTime)();
@@ -5,9 +5,9 @@ class LocalAsset extends Table {
const LocalAsset();
IntColumn get id => integer().autoIncrement()();
TextColumn get localId => text()();
TextColumn get localId => text().unique()();
TextColumn get name => text()();
TextColumn get checksum => text()();
TextColumn get checksum => text().unique()();
IntColumn get height => integer()();
IntColumn get width => integer()();
IntColumn get type => intEnum<AssetType>()();
@@ -6,7 +6,10 @@ class Store extends Table {
@override
String get tableName => 'store';
IntColumn get id => integer().autoIncrement()();
IntColumn get id => integer()();
IntColumn get intValue => integer().nullable()();
TextColumn get stringValue => text().nullable()();
@override
Set<Column> get primaryKey => {id};
}
@@ -0,0 +1,24 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
class User extends Table {
const User();
TextColumn get id => text()();
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
TextColumn get name => text()();
TextColumn get email => text()();
BoolColumn get isAdmin => boolean().withDefault(const Constant(false))();
// Quota
IntColumn get quotaSizeInBytes => integer().withDefault(const Constant(0))();
IntColumn get quotaUsageInBytes => integer().withDefault(const Constant(0))();
// Sharing
BoolColumn get inTimeline => boolean().withDefault(const Constant(false))();
// User prefs
TextColumn get profileImagePath => text()();
BoolColumn get memoryEnabled => boolean().withDefault(const Constant(true))();
IntColumn get avatarColor => intEnum<UserAvatarColor>()();
@override
Set<Column> get primaryKey => {id};
}
@@ -6,6 +6,15 @@ abstract class ILogRepository {
/// Fetches all logs
FutureOr<List<LogMessage>> fetchLogs();
/// Inserts a new log into the DB
FutureOr<bool> add(LogMessage log);
/// Bulk insert logs into DB
FutureOr<bool> addAll(List<LogMessage> log);
/// Clears all logs
FutureOr<bool> clear();
/// Truncates the logs to the most recent [limit]. Defaults to recent 250 logs
FutureOr<void> truncateLogs({int limit = 250});
}
@@ -9,19 +9,19 @@ abstract class IStoreConverter<T, U> {
U toPrimitive(T value);
/// Converts the value back to T? from the primitive type U from the Store
T? fromPrimitive(U value);
FutureOr<T?> fromPrimitive(U value);
}
abstract class IStoreRepository {
FutureOr<T?> getValue<T, U>(StoreKey<T, U> key);
FutureOr<T?> tryGet<T, U>(StoreKey<T, U> key);
FutureOr<bool> setValue<T, U>(StoreKey<T, U> key, T value);
FutureOr<T> get<T, U>(StoreKey<T, U> key);
FutureOr<void> deleteValue(StoreKey key);
FutureOr<bool> set<T, U>(StoreKey<T, U> key, T value);
Stream<T?> watchValue<T, U>(StoreKey<T, U> key);
FutureOr<void> delete(StoreKey key);
Stream<List<StoreValue>> watchStore();
Stream<T?> watch<T, U>(StoreKey<T, U> key);
FutureOr<void> clearStore();
}
@@ -0,0 +1,11 @@
import 'dart:async';
import 'package:immich_mobile/domain/models/user.model.dart';
abstract class IUserRepository {
/// Fetches user
FutureOr<User?> getUser(String userId);
/// Insert user
FutureOr<bool> insertUser(User user);
}
+1 -4
View File
@@ -24,7 +24,6 @@ extension LevelExtension on Level {
@immutable
class LogMessage {
final int id;
final String content;
final LogLevel level;
final DateTime createdAt;
@@ -33,7 +32,6 @@ class LogMessage {
final String? stack;
const LogMessage({
required this.id,
required this.content,
required this.level,
required this.createdAt,
@@ -51,8 +49,7 @@ class LogMessage {
@override
int get hashCode {
return id.hashCode ^
content.hashCode ^
return content.hashCode ^
level.hashCode ^
createdAt.hashCode ^
logger.hashCode ^
+22 -2
View File
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:immich_mobile/domain/interfaces/store.interface.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/domain/utils/store_converters.dart';
import 'package:immich_mobile/presentation/modules/theme/models/app_theme.model.dart';
@@ -21,14 +22,33 @@ class StoreValue<T> {
int get hashCode => id.hashCode ^ value.hashCode;
}
class StoreKeyNotFoundException implements Exception {
final StoreKey key;
const StoreKeyNotFoundException(this.key);
@override
String toString() => "Key '${key.name}' not found in Store";
}
/// Key for each possible value in the `Store`.
/// Also stores the converter to convert the value to and from the store and the type of value stored in the Store
enum StoreKey<T, U> {
serverEndpoint<String, String>(
0,
converter: StorePrimitiveConverter(),
converter: StoreStringConverter(),
type: String,
),
accessToken<String, String>(
1,
converter: StoreStringConverter(),
type: String,
),
currentUser<User, String>(
2,
converter: StoreUserConverter(),
type: String,
),
// App settings
appTheme<AppTheme, int>(
1000,
converter: StoreEnumConverter(AppTheme.values),
@@ -44,7 +64,7 @@ enum StoreKey<T, U> {
const StoreKey(this.id, {required this.converter, required this.type});
final int id;
/// Type is also stored here easily fetch it during runtime
/// Primitive Type is also stored here to easily fetch it during runtime
final Type type;
final IStoreConverter<T, U> converter;
}
+187
View File
@@ -0,0 +1,187 @@
import 'dart:ui';
import 'package:openapi/openapi.dart' as api;
class User {
const User({
required this.id,
required this.updatedAt,
required this.name,
required this.email,
required this.isAdmin,
required this.quotaSizeInBytes,
required this.quotaUsageInBytes,
required this.inTimeline,
required this.profileImagePath,
required this.memoryEnabled,
required this.avatarColor,
});
final String id;
final DateTime updatedAt;
final String name;
final String email;
final bool isAdmin;
// Quota
final int quotaSizeInBytes;
final int quotaUsageInBytes;
// Sharing
final bool inTimeline;
// User prefs
final String profileImagePath;
final bool memoryEnabled;
final UserAvatarColor avatarColor;
User copyWith({
String? id,
DateTime? updatedAt,
String? name,
String? email,
bool? isAdmin,
int? quotaSizeInBytes,
int? quotaUsageInBytes,
bool? inTimeline,
String? profileImagePath,
bool? memoryEnabled,
UserAvatarColor? avatarColor,
}) {
return User(
id: id ?? this.id,
updatedAt: updatedAt ?? this.updatedAt,
name: name ?? this.name,
email: email ?? this.email,
isAdmin: isAdmin ?? this.isAdmin,
quotaSizeInBytes: quotaSizeInBytes ?? this.quotaSizeInBytes,
quotaUsageInBytes: quotaUsageInBytes ?? this.quotaUsageInBytes,
inTimeline: inTimeline ?? this.inTimeline,
profileImagePath: profileImagePath ?? this.profileImagePath,
memoryEnabled: memoryEnabled ?? this.memoryEnabled,
avatarColor: avatarColor ?? this.avatarColor,
);
}
@override
String toString() {
return 'User(id: $id, updatedAt: $updatedAt, name: $name, email: $email, isAdmin: $isAdmin, quotaSizeInBytes: $quotaSizeInBytes, quotaUsageInBytes: $quotaUsageInBytes, inTimeline: $inTimeline, profileImagePath: $profileImagePath, memoryEnabled: $memoryEnabled, avatarColor: $avatarColor)';
}
@override
bool operator ==(covariant User other) {
if (identical(this, other)) return true;
return other.id == id &&
other.updatedAt == updatedAt &&
other.name == name &&
other.email == email &&
other.isAdmin == isAdmin &&
other.quotaSizeInBytes == quotaSizeInBytes &&
other.quotaUsageInBytes == quotaUsageInBytes &&
other.inTimeline == inTimeline &&
other.profileImagePath == profileImagePath &&
other.memoryEnabled == memoryEnabled &&
other.avatarColor == avatarColor;
}
@override
int get hashCode {
return id.hashCode ^
updatedAt.hashCode ^
name.hashCode ^
email.hashCode ^
isAdmin.hashCode ^
quotaSizeInBytes.hashCode ^
quotaUsageInBytes.hashCode ^
inTimeline.hashCode ^
profileImagePath.hashCode ^
memoryEnabled.hashCode ^
avatarColor.hashCode;
}
factory User.fromAdminDto(
api.UserAdminResponseDto userDto, [
api.UserPreferencesResponseDto? userPreferences,
]) {
return User(
id: userDto.id,
updatedAt: DateTime.now(),
name: userDto.name,
email: userDto.email,
isAdmin: userDto.isAdmin,
quotaSizeInBytes: userDto.quotaSizeInBytes ?? 0,
quotaUsageInBytes: userDto.quotaUsageInBytes ?? 0,
inTimeline: true,
profileImagePath: userDto.profileImagePath,
memoryEnabled: userPreferences?.memories.enabled ?? true,
avatarColor: userDto.avatarColor.toEnum(),
);
}
}
enum UserAvatarColor {
// do not change this order or reuse indices for other purposes, adding is OK
primary,
pink,
red,
yellow,
blue,
green,
purple,
orange,
gray,
amber,
}
extension AvatarColorEnumHelper on api.UserAvatarColor {
UserAvatarColor toEnum() {
switch (this) {
case api.UserAvatarColor.primary:
return UserAvatarColor.primary;
case api.UserAvatarColor.pink:
return UserAvatarColor.pink;
case api.UserAvatarColor.red:
return UserAvatarColor.red;
case api.UserAvatarColor.yellow:
return UserAvatarColor.yellow;
case api.UserAvatarColor.blue:
return UserAvatarColor.blue;
case api.UserAvatarColor.green:
return UserAvatarColor.green;
case api.UserAvatarColor.purple:
return UserAvatarColor.purple;
case api.UserAvatarColor.orange:
return UserAvatarColor.orange;
case api.UserAvatarColor.gray:
return UserAvatarColor.gray;
case api.UserAvatarColor.amber:
return UserAvatarColor.amber;
}
return UserAvatarColor.primary;
}
}
extension AvatarColorToColorHelper on UserAvatarColor {
Color toColor([bool isDarkTheme = false]) {
switch (this) {
case UserAvatarColor.primary:
return isDarkTheme ? const Color(0xFFABCBFA) : const Color(0xFF4250AF);
case UserAvatarColor.pink:
return const Color.fromARGB(255, 244, 114, 182);
case UserAvatarColor.red:
return const Color.fromARGB(255, 239, 68, 68);
case UserAvatarColor.yellow:
return const Color.fromARGB(255, 234, 179, 8);
case UserAvatarColor.blue:
return const Color.fromARGB(255, 59, 130, 246);
case UserAvatarColor.green:
return const Color.fromARGB(255, 22, 163, 74);
case UserAvatarColor.purple:
return const Color.fromARGB(255, 147, 51, 234);
case UserAvatarColor.orange:
return const Color.fromARGB(255, 234, 88, 12);
case UserAvatarColor.gray:
return const Color.fromARGB(255, 75, 85, 99);
case UserAvatarColor.amber:
return const Color.fromARGB(255, 217, 119, 6);
}
}
}
@@ -2,10 +2,14 @@ import 'dart:io';
import 'package:drift/drift.dart';
import 'package:drift/native.dart';
// ignore: depend_on_referenced_packages
import 'package:drift_dev/api/migrations.dart';
import 'package:flutter/foundation.dart';
import 'package:immich_mobile/domain/entities/album.entity.dart';
import 'package:immich_mobile/domain/entities/asset.entity.dart';
import 'package:immich_mobile/domain/entities/log.entity.dart';
import 'package:immich_mobile/domain/entities/store.entity.dart';
import 'package:immich_mobile/domain/entities/user.entity.dart';
import 'package:immich_mobile/domain/interfaces/database.interface.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
@@ -14,7 +18,7 @@ import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart';
import 'database.repository.drift.dart';
@DriftDatabase(tables: [Logs, Store, LocalAlbum, LocalAsset])
@DriftDatabase(tables: [Logs, Store, LocalAlbum, LocalAsset, User])
class DriftDatabaseRepository extends $DriftDatabaseRepository
implements IDatabaseRepository<GeneratedDatabase> {
DriftDatabaseRepository() : super(_openConnection());
@@ -51,6 +55,18 @@ class DriftDatabaseRepository extends $DriftDatabaseRepository
@override
// ignore: no-empty-block
void migrateDB() {
// No migrations yet
// Migrations are handled automatically using the migrator field
}
@override
MigrationStrategy get migration => MigrationStrategy(
onCreate: (m) => m.createAll(),
beforeOpen: (details) async {
if (kDebugMode) {
await validateDatabaseSchema();
}
},
// ignore: no-empty-block
onUpgrade: (m, from, to) async {},
);
}
@@ -1,3 +1,7 @@
import 'dart:async';
import 'package:drift/drift.dart';
import 'package:flutter/foundation.dart';
import 'package:immich_mobile/domain/entities/log.entity.drift.dart';
import 'package:immich_mobile/domain/interfaces/log.interface.dart';
import 'package:immich_mobile/domain/models/log.model.dart';
@@ -10,7 +14,7 @@ class LogDriftRepository implements ILogRepository {
@override
Future<List<LogMessage>> fetchLogs() async {
return await db.select(db.logs).map((l) => l.toModel()).get();
return await db.managers.logs.map((l) => l.toModel()).get();
}
@override
@@ -26,12 +30,65 @@ class LogDriftRepository implements ILogRepository {
}
});
}
@override
FutureOr<bool> add(LogMessage log) async {
try {
await db.transaction(() async {
await db.into(db.logs).insert(LogsCompanion.insert(
content: log.content,
level: log.level,
createdAt: Value(log.createdAt),
error: Value(log.error),
logger: Value(log.logger),
stack: Value(log.stack),
));
});
return true;
} catch (e) {
debugPrint("Error while adding a log to the DB - $e");
return false;
}
}
@override
FutureOr<bool> addAll(List<LogMessage> logs) async {
try {
await db.batch((b) {
b.insertAll(
db.logs,
logs.map((log) => LogsCompanion.insert(
content: log.content,
level: log.level,
createdAt: Value(log.createdAt),
error: Value(log.error),
logger: Value(log.logger),
stack: Value(log.stack),
)),
);
});
return true;
} catch (e) {
debugPrint("Error while adding a log to the DB - $e");
return false;
}
}
@override
FutureOr<bool> clear() async {
try {
await db.managers.logs.delete();
return true;
} catch (e) {
debugPrint("Error while clearning the logs in DB - $e");
return false;
}
}
}
extension _LogToLogMessage on Log {
LogMessage toModel() {
return LogMessage(
id: id,
content: content,
createdAt: createdAt,
level: level,
@@ -1,6 +1,5 @@
import 'dart:async';
import 'package:collection/collection.dart';
import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/entities/store.entity.drift.dart';
import 'package:immich_mobile/domain/interfaces/store.interface.dart';
@@ -14,7 +13,7 @@ class StoreDriftRepository with LogContext implements IStoreRepository {
const StoreDriftRepository(this.db);
@override
FutureOr<T?> getValue<T, U>(StoreKey<T, U> key) async {
FutureOr<T?> tryGet<T, U>(StoreKey<T, U> key) async {
final storeData = await db.managers.store
.filter((s) => s.id.equals(key.id))
.getSingleOrNull();
@@ -22,7 +21,16 @@ class StoreDriftRepository with LogContext implements IStoreRepository {
}
@override
FutureOr<bool> setValue<T, U>(StoreKey<T, U> key, T value) async {
FutureOr<T> get<T, U>(StoreKey<T, U> key) async {
final value = await tryGet(key);
if (value == null) {
throw StoreKeyNotFoundException(key);
}
return value;
}
@override
FutureOr<bool> set<T, U>(StoreKey<T, U> key, T value) async {
try {
await db.transaction(() async {
final storeValue = key.converter.toPrimitive(value);
@@ -42,30 +50,18 @@ class StoreDriftRepository with LogContext implements IStoreRepository {
}
@override
FutureOr<void> deleteValue(StoreKey key) async {
FutureOr<void> delete(StoreKey key) async {
return await db.transaction(() async {
await db.managers.store.filter((s) => s.id.equals(key.id)).delete();
});
}
@override
Stream<List<StoreValue>> watchStore() {
return (db.select(db.store).map((s) {
final key = StoreKey.values.firstWhereOrNull((e) => e.id == s.id);
if (key != null) {
final value = _getValueFromStoreData(key, s);
return StoreValue(id: s.id, value: value);
}
return StoreValue(id: s.id, value: null);
})).watch();
}
@override
Stream<T?> watchValue<T, U>(StoreKey<T, U> key) {
Stream<T?> watch<T, U>(StoreKey<T, U> key) {
return db.managers.store
.filter((s) => s.id.equals(key.id))
.watchSingleOrNull()
.map((e) => _getValueFromStoreData(key, e));
.asyncMap((e) async => await _getValueFromStoreData(key, e));
}
@override
@@ -75,7 +71,10 @@ class StoreDriftRepository with LogContext implements IStoreRepository {
});
}
T? _getValueFromStoreData<T, U>(StoreKey<T, U> key, StoreData? data) {
FutureOr<T?> _getValueFromStoreData<T, U>(
StoreKey<T, U> key,
StoreData? data,
) async {
final primitive = switch (key.type) {
const (int) => data?.intValue,
const (String) => data?.stringValue,
@@ -0,0 +1,65 @@
import 'dart:async';
import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/entities/user.entity.drift.dart';
import 'package:immich_mobile/domain/interfaces/user.interface.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/domain/repositories/database.repository.dart';
import 'package:immich_mobile/utils/mixins/log_context.mixin.dart';
class UserDriftRepository with LogContext implements IUserRepository {
final DriftDatabaseRepository db;
const UserDriftRepository(this.db);
@override
FutureOr<User?> getUser(String userId) async {
return await db.managers.user
.filter((f) => f.id.equals(userId))
.map((u) => u.toModel())
.getSingleOrNull();
}
@override
FutureOr<bool> insertUser(User user) async {
try {
return await db.transaction(() async {
await db.into(db.user).insertOnConflictUpdate(UserCompanion.insert(
id: user.id,
name: user.name,
email: user.email,
profileImagePath: user.profileImagePath,
avatarColor: user.avatarColor,
inTimeline: Value(user.inTimeline),
isAdmin: Value(user.isAdmin),
memoryEnabled: Value(user.memoryEnabled),
quotaSizeInBytes: Value(user.quotaSizeInBytes),
quotaUsageInBytes: Value(user.quotaSizeInBytes),
updatedAt: Value(user.updatedAt),
));
return true;
});
} catch (e, s) {
log.severe("Cannot insert User into table - $user", e, s);
return false;
}
}
}
extension _UserDataToUser on UserData {
User toModel() {
return User(
id: id,
email: email,
avatarColor: avatarColor,
inTimeline: inTimeline,
isAdmin: isAdmin,
memoryEnabled: memoryEnabled,
name: name,
profileImagePath: profileImagePath,
quotaSizeInBytes: quotaSizeInBytes,
quotaUsageInBytes: quotaUsageInBytes,
updatedAt: updatedAt,
);
}
}
@@ -1,17 +1,18 @@
import 'package:immich_mobile/domain/interfaces/store.interface.dart';
import 'package:immich_mobile/domain/models/app_setting.model.dart';
import 'package:immich_mobile/domain/store_manager.dart';
class AppSettingService {
final StoreManager store;
final IStoreRepository store;
const AppSettingService(this.store);
T getSetting<T>(AppSetting<T> setting) {
return store.get(setting.storeKey, setting.defaultValue);
Future<T> getSetting<T>(AppSetting<T> setting) async {
final value = await store.tryGet(setting.storeKey);
return value ?? setting.defaultValue;
}
Future<bool> setSetting<T>(AppSetting<T> setting, T value) async {
return await store.put(setting.storeKey, value);
return await store.set(setting.storeKey, value);
}
Stream<T> watchSetting<T>(AppSetting<T> setting) {
@@ -1,6 +1,8 @@
import 'dart:convert';
import 'package:dio/dio.dart';
import 'package:flutter_web_auth_2/flutter_web_auth_2.dart';
import 'package:immich_mobile/service_locator.dart';
import 'package:immich_mobile/utils/mixins/log_context.mixin.dart';
import 'package:openapi/openapi.dart';
@@ -52,4 +54,58 @@ class LoginService with LogContext {
// No well-known, return the baseUrl
return baseUrl;
}
Future<String?> passwordLogin(String email, String password) async {
try {
final loginResponse = await di<Openapi>().getAuthenticationApi().login(
loginCredentialDto: LoginCredentialDto((builder) {
builder.email = email;
builder.password = password;
}),
);
return loginResponse.data?.accessToken;
} catch (e, s) {
log.severe("Exception occured while performing password login", e, s);
}
return null;
}
Future<String?> oAuthLogin() async {
const String oAuthCallbackSchema = 'app.immich';
final oAuthApi = di<Openapi>().getOAuthApi();
try {
final oAuthUrl = await oAuthApi.startOAuth(
oAuthConfigDto: OAuthConfigDto((builder) {
builder.redirectUri = "$oAuthCallbackSchema:/";
}),
);
final oAuthUrlRes = oAuthUrl.data?.url;
if (oAuthUrlRes == null) {
log.severe(
"oAuth Server URL not available. Kindly ensure oAuth login is enabled in the server",
);
return null;
}
final oAuthCallbackUrl = await FlutterWebAuth2.authenticate(
url: oAuthUrlRes,
callbackUrlScheme: oAuthCallbackSchema,
);
final loginResponse = await oAuthApi.finishOAuth(
oAuthCallbackDto: OAuthCallbackDto((builder) {
builder.url = oAuthCallbackUrl;
}),
);
return loginResponse.data?.accessToken;
} catch (e) {
log.severe("Exception occured while performing oauth login", e);
}
return null;
}
}
@@ -0,0 +1,28 @@
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/utils/mixins/log_context.mixin.dart';
import 'package:openapi/openapi.dart';
class UserService with LogContext {
final Openapi _api;
UsersApi get _userApi => _api.getUsersApi();
UserService(this._api);
Future<User?> getMyUser() async {
try {
final response = await _userApi.getMyUser();
final dto = response.data;
if (dto == null) {
log.severe("Cannot fetch my user.");
return null;
}
final preferences = await _userApi.getMyPreferences();
return User.fromAdminDto(dto, preferences.data);
} catch (e, s) {
log.severe("Error while fetching server features", e, s);
}
return null;
}
}
-90
View File
@@ -1,90 +0,0 @@
import 'dart:async';
import 'package:immich_mobile/domain/interfaces/store.interface.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/service_locator.dart';
import 'package:immich_mobile/utils/mixins/log_context.mixin.dart';
class StoreKeyNotFoundException implements Exception {
final StoreKey key;
const StoreKeyNotFoundException(this.key);
@override
String toString() => "Key '${key.name}' not found in Store";
}
/// Key-value cache for individual items enumerated in StoreKey.
class StoreManager with LogContext {
late final IStoreRepository _db;
late final StreamSubscription _subscription;
final Map<int, dynamic> _cache = {};
StoreManager(IStoreRepository db) {
_db = db;
_subscription = _db.watchStore().listen(_onChangeListener);
_populateCache();
}
void dispose() {
_subscription.cancel();
}
FutureOr<void> _populateCache() async {
for (StoreKey key in StoreKey.values) {
final value = await _db.getValue(key);
if (value != null) {
_cache[key.id] = value;
}
}
/// Signal ready once the cache is populated
di.signalReady(this);
}
/// clears all values from this store (cache and DB), only for testing!
Future<void> clear() async {
_cache.clear();
return await _db.clearStore();
}
/// Returns the stored value for the given key (possibly null)
T? tryGet<T, U>(StoreKey<T, U> key) => _cache[key.id] as T?;
/// Returns the stored value for the given key or if null the [defaultValue]
/// Throws a [StoreKeyNotFoundException] if both are null
T get<T, U>(StoreKey<T, U> key, [T? defaultValue]) {
final value = _cache[key.id] ?? defaultValue;
if (value == null) {
throw StoreKeyNotFoundException(key);
}
return value;
}
/// Watches a specific key for changes
Stream<T?> watch<T, U>(StoreKey<T, U> key) => _db.watchValue(key);
/// Stores the value synchronously in the cache and asynchronously in the DB
FutureOr<bool> put<T, U>(StoreKey<T, U> key, T value) async {
if (_cache[key.id] == value) return Future.value(true);
_cache[key.id] = value;
return await _db.setValue(key, value);
}
/// Removes the value synchronously from the cache and asynchronously from the DB
Future<void> delete<T, U>(StoreKey<T, U> key) async {
if (_cache[key.id] == null) return Future.value();
_cache.remove(key.id);
return await _db.deleteValue(key);
}
/// Updates the state in cache if a value is updated in any isolate
void _onChangeListener(List<StoreValue>? data) {
if (data != null) {
for (StoreValue storeValue in data) {
if (storeValue.value != null) {
_cache[storeValue.id] = storeValue.value;
}
}
}
}
}
@@ -1,4 +1,9 @@
import 'dart:async';
import 'package:immich_mobile/domain/interfaces/store.interface.dart';
import 'package:immich_mobile/domain/interfaces/user.interface.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/service_locator.dart';
class StoreEnumConverter<T extends Enum> extends IStoreConverter<T, int> {
const StoreEnumConverter(this.values);
@@ -22,8 +27,8 @@ class StoreBooleanConverter extends IStoreConverter<bool, int> {
int toPrimitive(bool value) => value ? 1 : 0;
}
class StorePrimitiveConverter<T> extends IStoreConverter<T, T> {
const StorePrimitiveConverter();
class _StorePrimitiveConverter<T> extends IStoreConverter<T, T> {
const _StorePrimitiveConverter();
@override
T fromPrimitive(T value) => value;
@@ -31,3 +36,23 @@ class StorePrimitiveConverter<T> extends IStoreConverter<T, T> {
@override
T toPrimitive(T value) => value;
}
class StoreStringConverter extends _StorePrimitiveConverter<String> {
const StoreStringConverter();
}
class StoreIntConverter extends _StorePrimitiveConverter<int> {
const StoreIntConverter();
}
class StoreUserConverter extends IStoreConverter<User, String> {
const StoreUserConverter();
@override
Future<User?> fromPrimitive(String value) async {
return await di<IUserRepository>().getUser(value);
}
@override
String toPrimitive(User value) => value.id;
}