🚀
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:immich_mobile/domain/models/log.model.dart';
|
||||
|
||||
class Logs extends Table {
|
||||
const Logs();
|
||||
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
TextColumn get content => text()();
|
||||
IntColumn get level => intEnum<LogLevel>()();
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||
TextColumn get logger => text().nullable()();
|
||||
TextColumn get error => text().nullable()();
|
||||
TextColumn get stack => text().nullable()();
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
class Store extends Table {
|
||||
const Store();
|
||||
|
||||
@override
|
||||
String get tableName => 'store';
|
||||
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
IntColumn get intValue => integer().nullable()();
|
||||
TextColumn get stringValue => text().nullable()();
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
abstract class IDatabaseRepository<T> {
|
||||
/// Current version of the DB to aid with migration
|
||||
int get schemaVersion;
|
||||
|
||||
/// Initializes the DB and returns the corresponding object
|
||||
T init();
|
||||
|
||||
/// Check and migrate the DB to the latest schema
|
||||
void migrateDB();
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:immich_mobile/domain/models/log.model.dart';
|
||||
|
||||
abstract class ILogRepository {
|
||||
/// Fetches all logs
|
||||
FutureOr<List<LogMessage>> fetchLogs();
|
||||
|
||||
/// Truncates the logs to the most recent [limit]. Defaults to recent 250 logs
|
||||
FutureOr<void> truncateLogs({int limit = 250});
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
|
||||
abstract class IStoreRepository {
|
||||
FutureOr<T?> getValue<T>(StoreKey key);
|
||||
|
||||
FutureOr<void> setValue<T>(StoreKey<T> key, T value);
|
||||
|
||||
FutureOr<void> deleteValue(StoreKey key);
|
||||
|
||||
Stream<T?> watchValue<T>(StoreKey key);
|
||||
|
||||
Stream<List<StoreValue>> watchStore();
|
||||
|
||||
FutureOr<void> clearStore();
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
/// Log levels according to dart logging [Level]
|
||||
enum LogLevel {
|
||||
all,
|
||||
finest,
|
||||
finer,
|
||||
fine,
|
||||
config,
|
||||
info,
|
||||
warning,
|
||||
severe,
|
||||
shout,
|
||||
off,
|
||||
}
|
||||
|
||||
extension LevelExtension on Level {
|
||||
LogLevel toLogLevel() =>
|
||||
LogLevel.values.elementAtOrNull(Level.LEVELS.indexOf(this)) ??
|
||||
LogLevel.info;
|
||||
}
|
||||
|
||||
class LogMessage {
|
||||
final int id;
|
||||
final String content;
|
||||
final LogLevel level;
|
||||
final DateTime createdAt;
|
||||
final String? logger;
|
||||
final String? error;
|
||||
final String? stack;
|
||||
|
||||
const LogMessage({
|
||||
required this.id,
|
||||
required this.content,
|
||||
required this.level,
|
||||
required this.createdAt,
|
||||
this.logger,
|
||||
this.error,
|
||||
this.stack,
|
||||
});
|
||||
|
||||
@override
|
||||
bool operator ==(covariant LogMessage other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other.hashCode == hashCode;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return id.hashCode ^
|
||||
content.hashCode ^
|
||||
level.hashCode ^
|
||||
createdAt.hashCode ^
|
||||
logger.hashCode ^
|
||||
error.hashCode ^
|
||||
stack.hashCode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/// Key for each possible value in the `Store`.
|
||||
/// Defines the data type for each value
|
||||
enum StoreKey<T> {
|
||||
// Server endpoint related stores
|
||||
accessToken<String>(0, type: String),
|
||||
serverEndpoint<String>(1, type: String),
|
||||
;
|
||||
|
||||
const StoreKey(this.id, {required this.type});
|
||||
final int id;
|
||||
final Type type;
|
||||
}
|
||||
|
||||
class StoreValue {
|
||||
final int id;
|
||||
final int? intValue;
|
||||
final String? stringValue;
|
||||
|
||||
const StoreValue({required this.id, this.intValue, this.stringValue});
|
||||
|
||||
@override
|
||||
bool operator ==(covariant StoreValue other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other.hashCode == hashCode;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => id.hashCode ^ intValue.hashCode ^ stringValue.hashCode;
|
||||
|
||||
T? extract<T>(Type type) {
|
||||
switch (type) {
|
||||
case const (int):
|
||||
return intValue as T?;
|
||||
case const (bool):
|
||||
return intValue == null ? null : (intValue! == 1) as T;
|
||||
case const (DateTime):
|
||||
return intValue == null
|
||||
? null
|
||||
: DateTime.fromMicrosecondsSinceEpoch(intValue!) as T;
|
||||
case const (String):
|
||||
return stringValue as T?;
|
||||
default:
|
||||
throw UnsupportedError("Unknown Store Key type");
|
||||
}
|
||||
}
|
||||
|
||||
static StoreValue of<T>(StoreKey<T> key, T? value) {
|
||||
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:
|
||||
throw UnsupportedError("Unknown Store Key type");
|
||||
}
|
||||
return StoreValue(id: key.id, intValue: i, stringValue: s);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:immich_mobile/domain/entities/log.entity.dart';
|
||||
import 'package:immich_mobile/domain/entities/store.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';
|
||||
import 'package:sqlite3/sqlite3.dart';
|
||||
import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart';
|
||||
|
||||
import 'database.repository.drift.dart';
|
||||
|
||||
@DriftDatabase(tables: [Logs, Store])
|
||||
class DriftDatabaseRepository extends $DriftDatabaseRepository
|
||||
implements IDatabaseRepository<GeneratedDatabase> {
|
||||
DriftDatabaseRepository() : super(_openConnection());
|
||||
|
||||
static LazyDatabase _openConnection() {
|
||||
return LazyDatabase(() async {
|
||||
final dbFolder = await getApplicationDocumentsDirectory();
|
||||
final file = File(p.join(dbFolder.path, 'db.sqlite'));
|
||||
|
||||
// Work around limitations on old Android versions
|
||||
// https://github.com/simolus3/sqlite3.dart/tree/main/sqlite3_flutter_libs#problems-on-android-6
|
||||
if (Platform.isAndroid) {
|
||||
await applyWorkaroundToOpenSqlite3OnOldAndroidVersions();
|
||||
}
|
||||
|
||||
// Make sqlite3 pick a more suitable location for temporary files - the
|
||||
// one from the system may be inaccessible due to sandboxing.
|
||||
// https://github.com/simolus3/moor/issues/876#issuecomment-710013503
|
||||
final cachebase = (await getTemporaryDirectory()).path;
|
||||
// We can't access /tmp on Android, which sqlite3 would try by default.
|
||||
// Explicitly tell it about the correct temporary directory.
|
||||
sqlite3.tempDirectory = cachebase;
|
||||
|
||||
return NativeDatabase.createInBackground(file);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
GeneratedDatabase init() => this;
|
||||
|
||||
@override
|
||||
int get schemaVersion => 1;
|
||||
|
||||
@override
|
||||
// ignore: no-empty-block
|
||||
void migrateDB() {
|
||||
// No migrations yet
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
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';
|
||||
import 'package:immich_mobile/domain/repositories/database.repository.dart';
|
||||
|
||||
class LogDriftRepository implements ILogRepository {
|
||||
final DriftDatabaseRepository db;
|
||||
|
||||
const LogDriftRepository(this.db);
|
||||
|
||||
@override
|
||||
Future<List<LogMessage>> fetchLogs() async {
|
||||
return await db.select(db.logs).map((l) => l.toModel()).get();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> truncateLogs({int limit = 250}) {
|
||||
return db.transaction(() async {
|
||||
final totalCount = await db.managers.logs.count();
|
||||
if (totalCount > limit) {
|
||||
final rowsToDelete = totalCount - limit;
|
||||
await db.managers.logs
|
||||
.orderBy((o) => o.createdAt.desc())
|
||||
.limit(rowsToDelete)
|
||||
.delete();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension _LogToLogMessage on Log {
|
||||
LogMessage toModel() {
|
||||
return LogMessage(
|
||||
id: id,
|
||||
content: content,
|
||||
createdAt: createdAt,
|
||||
level: level,
|
||||
error: error,
|
||||
logger: logger,
|
||||
stack: stack,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:immich_mobile/domain/entities/store.entity.drift.dart';
|
||||
import 'package:immich_mobile/domain/interfaces/store.interface.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/repositories/database.repository.dart';
|
||||
|
||||
class StoreDriftRepository implements IStoreRepository {
|
||||
final DriftDatabaseRepository db;
|
||||
|
||||
const StoreDriftRepository(this.db);
|
||||
|
||||
@override
|
||||
FutureOr<T?> getValue<T>(StoreKey key) async {
|
||||
final value = await db.managers.store
|
||||
.filter((s) => s.id.equals(key.id))
|
||||
.getSingleOrNull();
|
||||
return value?.toModel().extract(key.type);
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<void> setValue<T>(StoreKey<T> key, T value) {
|
||||
return db.transaction(() async {
|
||||
final storeValue = StoreValue.of(key, value);
|
||||
await db.into(db.store).insertOnConflictUpdate(StoreCompanion.insert(
|
||||
id: Value(storeValue.id),
|
||||
intValue: Value(storeValue.intValue),
|
||||
stringValue: Value(storeValue.stringValue),
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<void> deleteValue(StoreKey key) {
|
||||
return 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) => s.toModel())).watch();
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<T?> watchValue<T>(StoreKey key) {
|
||||
return db.managers.store
|
||||
.filter((s) => s.id.equals(key.id))
|
||||
.watchSingleOrNull()
|
||||
.map((e) => e?.toModel().extract(key.type));
|
||||
}
|
||||
|
||||
@override
|
||||
FutureOr<void> clearStore() {
|
||||
return db.transaction(() async {
|
||||
await db.managers.store.delete();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension _StoreDataToStoreValue on StoreData {
|
||||
StoreValue toModel() {
|
||||
return StoreValue(id: id, intValue: intValue, stringValue: stringValue);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:immich_mobile/domain/interfaces/log.interface.dart';
|
||||
import 'package:immich_mobile/domain/interfaces/store.interface.dart';
|
||||
import 'package:immich_mobile/domain/repositories/database.repository.dart';
|
||||
import 'package:immich_mobile/domain/repositories/log.repository.dart';
|
||||
import 'package:immich_mobile/domain/repositories/store.repository.dart';
|
||||
import 'package:immich_mobile/domain/store_manager.dart';
|
||||
|
||||
/// Ambient instance
|
||||
final getIt = GetIt.instance;
|
||||
|
||||
class ServiceLocator {
|
||||
const ServiceLocator._internal();
|
||||
|
||||
static void configureServices() {
|
||||
// Register DB
|
||||
getIt.registerSingleton<DriftDatabaseRepository>(DriftDatabaseRepository());
|
||||
_registerCoreServices();
|
||||
}
|
||||
|
||||
static void _registerCoreServices() {
|
||||
// Init store
|
||||
getIt
|
||||
.registerFactory<IStoreRepository>(() => StoreDriftRepository(getIt()));
|
||||
getIt.registerSingleton<StoreManager>(StoreManager(getIt()));
|
||||
// Logs
|
||||
getIt.registerFactory<ILogRepository>(() => LogDriftRepository(getIt()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:immich_mobile/domain/interfaces/store.interface.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.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 store for individual items enumerated in StoreKey.
|
||||
/// Supports String, int and JSON-serializable Objects
|
||||
/// Can be used concurrently from multiple isolates
|
||||
class StoreManager with LogContext {
|
||||
late final IStoreRepository _db;
|
||||
StreamSubscription? _subscription;
|
||||
final Map<int, dynamic> _cache = {};
|
||||
|
||||
StoreManager._internal();
|
||||
static final StoreManager _instance = StoreManager._internal();
|
||||
|
||||
factory StoreManager(IStoreRepository db) {
|
||||
if (_instance._subscription == null) {
|
||||
_instance._db = db;
|
||||
_instance._populateCache();
|
||||
_instance._subscription =
|
||||
_instance._db.watchStore().listen(_instance._onChangeListener);
|
||||
}
|
||||
return _instance;
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
_subscription?.cancel();
|
||||
}
|
||||
|
||||
FutureOr<void> _populateCache() async {
|
||||
for (StoreKey key in StoreKey.values) {
|
||||
final StoreValue? value = await _db.getValue(key);
|
||||
if (value != null) {
|
||||
_cache[key.id] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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>(StoreKey<T> 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>(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
|
||||
Stream<T?> watch<T>(StoreKey<T> key) => _db.watchValue(key);
|
||||
|
||||
/// Stores the value synchronously in the cache and asynchronously in the DB
|
||||
FutureOr<void> put<T>(StoreKey<T> key, T value) async {
|
||||
if (_cache[key.id] == value) return Future.value();
|
||||
_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>(StoreKey<T> 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 value in data) {
|
||||
final key = StoreKey.values.firstWhereOrNull((e) => e.id == value.id);
|
||||
if (key != null) {
|
||||
_cache[value.id] = value.extract(key.type);
|
||||
} else {
|
||||
log.warning("No key available for value Id - ${value.id}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user