mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
refactor to per row store
This commit is contained in:
@@ -1,33 +1,11 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:immich_mobile/domain/models/config/theme_config.dart';
|
import 'package:immich_mobile/domain/models/config/theme_config.dart';
|
||||||
import 'package:immich_mobile/domain/models/metadata_value.dart';
|
|
||||||
import 'package:immich_mobile/extensions/json_extensions.dart';
|
|
||||||
|
|
||||||
class AppConfig implements MetadataValue {
|
|
||||||
static const String name = 'app-config';
|
|
||||||
|
|
||||||
|
class AppConfig {
|
||||||
final ThemeConfig theme;
|
final ThemeConfig theme;
|
||||||
|
|
||||||
const AppConfig({this.theme = const ThemeConfig()});
|
const AppConfig({this.theme = const ThemeConfig()});
|
||||||
|
|
||||||
factory AppConfig.fromJson(Map<String, Object?> json) {
|
AppConfig copyWith({ThemeConfig? theme}) => .new(theme: theme ?? this.theme);
|
||||||
final themeJson = json.nested(ThemeConfig.name);
|
|
||||||
return AppConfig(
|
|
||||||
theme: ThemeConfig(
|
|
||||||
mode: ThemeMode.values.firstWhere(
|
|
||||||
(e) => e.name == themeJson[ThemeConfig.keys.mode],
|
|
||||||
orElse: () => ThemeMode.system,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Map<String, Object?> toJson() => {
|
|
||||||
ThemeConfig.name: {ThemeConfig.keys.mode: theme.mode.name},
|
|
||||||
};
|
|
||||||
|
|
||||||
AppConfig copyWith({ThemeMode? themeMode}) => AppConfig(theme: ThemeConfig(mode: themeMode ?? theme.mode));
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || (other is AppConfig && other.theme == theme);
|
bool operator ==(Object other) => identical(this, other) || (other is AppConfig && other.theme == theme);
|
||||||
@@ -36,5 +14,5 @@ class AppConfig implements MetadataValue {
|
|||||||
int get hashCode => theme.hashCode;
|
int get hashCode => theme.hashCode;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => '$name: { $theme }';
|
String toString() => 'AppConfig(theme: $theme)';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,11 @@
|
|||||||
import 'package:immich_mobile/domain/models/log.model.dart';
|
import 'package:immich_mobile/domain/models/log.model.dart';
|
||||||
|
|
||||||
class _Keys {
|
|
||||||
const _Keys();
|
|
||||||
|
|
||||||
final level = 'level';
|
|
||||||
}
|
|
||||||
|
|
||||||
class LogConfig {
|
class LogConfig {
|
||||||
static const String name = 'log';
|
|
||||||
|
|
||||||
// ignore: library_private_types_in_public_api
|
|
||||||
static const _Keys keys = _Keys();
|
|
||||||
|
|
||||||
final LogLevel level;
|
final LogLevel level;
|
||||||
|
|
||||||
const LogConfig({this.level = LogLevel.info});
|
const LogConfig({this.level = .info});
|
||||||
|
|
||||||
|
LogConfig copyWith({LogLevel? level}) => .new(level: level ?? this.level);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || (other is LogConfig && other.level == level);
|
bool operator ==(Object other) => identical(this, other) || (other is LogConfig && other.level == level);
|
||||||
@@ -23,5 +14,5 @@ class LogConfig {
|
|||||||
int get hashCode => level.hashCode;
|
int get hashCode => level.hashCode;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => '$name: {${keys.level}: $level}';
|
String toString() => 'LogConfig(level: $level)';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +1,11 @@
|
|||||||
import 'package:immich_mobile/domain/models/config/log_config.dart';
|
import 'package:immich_mobile/domain/models/config/log_config.dart';
|
||||||
import 'package:immich_mobile/domain/models/log.model.dart';
|
|
||||||
import 'package:immich_mobile/domain/models/metadata_value.dart';
|
|
||||||
import 'package:immich_mobile/extensions/json_extensions.dart';
|
|
||||||
|
|
||||||
class SystemConfig implements MetadataValue {
|
|
||||||
static const String name = 'system-config';
|
|
||||||
|
|
||||||
|
class SystemConfig {
|
||||||
final LogConfig log;
|
final LogConfig log;
|
||||||
|
|
||||||
const SystemConfig({this.log = const LogConfig()});
|
const SystemConfig({this.log = const .new()});
|
||||||
|
|
||||||
factory SystemConfig.fromJson(Map<String, Object?> json) {
|
SystemConfig copyWith({LogConfig? log}) => .new(log: log ?? this.log);
|
||||||
final logJson = json.nested(LogConfig.name);
|
|
||||||
return SystemConfig(
|
|
||||||
log: LogConfig(
|
|
||||||
level: LogLevel.values.firstWhere((e) => e.name == logJson[LogConfig.keys.level], orElse: () => LogLevel.info),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Map<String, Object?> toJson() => {
|
|
||||||
LogConfig.name: {LogConfig.keys.level: log.level.name},
|
|
||||||
};
|
|
||||||
|
|
||||||
SystemConfig copyWith({LogLevel? logLevel}) => SystemConfig(log: LogConfig(level: logLevel ?? log.level));
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || (other is SystemConfig && other.log == log);
|
bool operator ==(Object other) => identical(this, other) || (other is SystemConfig && other.log == log);
|
||||||
@@ -33,5 +14,5 @@ class SystemConfig implements MetadataValue {
|
|||||||
int get hashCode => log.hashCode;
|
int get hashCode => log.hashCode;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => '$name: { $log }';
|
String toString() => 'SystemConfig(log: $log)';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,11 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class _Keys {
|
|
||||||
const _Keys();
|
|
||||||
|
|
||||||
final mode = 'mode';
|
|
||||||
}
|
|
||||||
|
|
||||||
class ThemeConfig {
|
class ThemeConfig {
|
||||||
static const String name = 'theme';
|
|
||||||
// ignore: library_private_types_in_public_api
|
|
||||||
static const _Keys keys = _Keys();
|
|
||||||
|
|
||||||
final ThemeMode mode;
|
final ThemeMode mode;
|
||||||
|
|
||||||
const ThemeConfig({this.mode = ThemeMode.system});
|
const ThemeConfig({this.mode = .system});
|
||||||
|
|
||||||
|
ThemeConfig copyWith({ThemeMode? mode}) => .new(mode: mode ?? this.mode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || (other is ThemeConfig && other.mode == mode);
|
bool operator ==(Object other) => identical(this, other) || (other is ThemeConfig && other.mode == mode);
|
||||||
@@ -22,5 +14,5 @@ class ThemeConfig {
|
|||||||
int get hashCode => mode.hashCode;
|
int get hashCode => mode.hashCode;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => '$name: {${keys.mode}: $mode}';
|
String toString() => 'ThemeConfig(mode: $mode)';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
import 'package:immich_mobile/domain/models/metadata_value.dart';
|
|
||||||
|
|
||||||
class SystemMetadata implements MetadataValue {
|
|
||||||
static const String name = 'system-metadata';
|
|
||||||
|
|
||||||
const SystemMetadata();
|
|
||||||
|
|
||||||
factory SystemMetadata.fromJson(Map<String, Object?> json) => const SystemMetadata();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Map<String, Object?> toJson() => const {};
|
|
||||||
|
|
||||||
SystemMetadata copyWith() => this;
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) => other is SystemMetadata;
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => 0;
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() => '$name: {}';
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/log.model.dart';
|
||||||
|
|
||||||
|
enum MetadataDomain {
|
||||||
|
appConfig('app-config'),
|
||||||
|
systemConfig('system-config');
|
||||||
|
|
||||||
|
final String prefix;
|
||||||
|
const MetadataDomain(this.prefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
enum MetadataKey<T extends Object> {
|
||||||
|
themeMode<ThemeMode>(.appConfig, 'theme.mode', .system, ThemeMode.values),
|
||||||
|
logLevel<LogLevel>(.systemConfig, 'log.level', .info, LogLevel.values);
|
||||||
|
|
||||||
|
final MetadataDomain domain;
|
||||||
|
final String name;
|
||||||
|
final T defaultValue;
|
||||||
|
final List<T>? enumValues;
|
||||||
|
|
||||||
|
const MetadataKey(this.domain, this.name, this.defaultValue, [this.enumValues]);
|
||||||
|
|
||||||
|
String get key => '${domain.prefix}.$name';
|
||||||
|
|
||||||
|
static MetadataKey<Object>? fromKey(String key) {
|
||||||
|
for (final m in MetadataKey.values) {
|
||||||
|
if (m.key == key) return m;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import 'package:immich_mobile/domain/models/config/app_config.dart';
|
|
||||||
import 'package:immich_mobile/domain/models/config/system_config.dart';
|
|
||||||
import 'package:immich_mobile/domain/models/metadata/system_metadata.dart';
|
|
||||||
import 'package:immich_mobile/domain/models/metadata_value.dart';
|
|
||||||
|
|
||||||
enum MetadataKind<T extends MetadataValue> {
|
|
||||||
appConfig<AppConfig>(AppConfig.name, AppConfig.fromJson, AppConfig()),
|
|
||||||
systemConfig<SystemConfig>(SystemConfig.name, SystemConfig.fromJson, SystemConfig()),
|
|
||||||
systemMetadata<SystemMetadata>(SystemMetadata.name, SystemMetadata.fromJson, SystemMetadata());
|
|
||||||
|
|
||||||
final String key;
|
|
||||||
final T Function(Map<String, Object?>) fromJson;
|
|
||||||
final T defaultValue;
|
|
||||||
|
|
||||||
const MetadataKind(this.key, this.fromJson, this.defaultValue);
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
abstract class MetadataValue {
|
|
||||||
const MetadataValue();
|
|
||||||
|
|
||||||
Map<String, Object?> toJson();
|
|
||||||
}
|
|
||||||
@@ -2,9 +2,9 @@ import 'dart:async';
|
|||||||
|
|
||||||
import 'package:immich_mobile/constants/constants.dart';
|
import 'package:immich_mobile/constants/constants.dart';
|
||||||
import 'package:immich_mobile/domain/models/log.model.dart';
|
import 'package:immich_mobile/domain/models/log.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/metadata_kind.dart';
|
import 'package:immich_mobile/domain/models/metadata_key.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/cached_metadata.repository.dart';
|
|
||||||
import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart';
|
||||||
import 'package:immich_mobile/utils/debug_print.dart';
|
import 'package:immich_mobile/utils/debug_print.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
@@ -12,10 +12,10 @@ import 'package:logging/logging.dart';
|
|||||||
///
|
///
|
||||||
/// It listens to Dart's [Logger.root], buffers logs in memory (optionally),
|
/// It listens to Dart's [Logger.root], buffers logs in memory (optionally),
|
||||||
/// writes them to a persistent [LogRepository], and manages log levels via
|
/// writes them to a persistent [LogRepository], and manages log levels via
|
||||||
/// [CachedMetadataRepository].
|
/// [MetadataRepository].
|
||||||
class LogService {
|
class LogService {
|
||||||
final LogRepository _logRepository;
|
final LogRepository _logRepository;
|
||||||
final CachedMetadataRepository _metadataRepository;
|
final MetadataRepository _metadataRepository;
|
||||||
|
|
||||||
final List<LogMessage> _msgBuffer = [];
|
final List<LogMessage> _msgBuffer = [];
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ class LogService {
|
|||||||
|
|
||||||
static Future<LogService> init({
|
static Future<LogService> init({
|
||||||
required LogRepository logRepository,
|
required LogRepository logRepository,
|
||||||
required CachedMetadataRepository metadataRepository,
|
required MetadataRepository metadataRepository,
|
||||||
bool shouldBuffer = true,
|
bool shouldBuffer = true,
|
||||||
}) async {
|
}) async {
|
||||||
_instance ??= await create(
|
_instance ??= await create(
|
||||||
@@ -51,12 +51,12 @@ class LogService {
|
|||||||
|
|
||||||
static Future<LogService> create({
|
static Future<LogService> create({
|
||||||
required LogRepository logRepository,
|
required LogRepository logRepository,
|
||||||
required CachedMetadataRepository metadataRepository,
|
required MetadataRepository metadataRepository,
|
||||||
bool shouldBuffer = true,
|
bool shouldBuffer = true,
|
||||||
}) async {
|
}) async {
|
||||||
final instance = LogService._(logRepository, metadataRepository, shouldBuffer);
|
final instance = LogService._(logRepository, metadataRepository, shouldBuffer);
|
||||||
await logRepository.truncate(limit: kLogTruncateLimit);
|
await logRepository.truncate(limit: kLogTruncateLimit);
|
||||||
final level = instance._metadataRepository.read(MetadataKind.systemConfig).log.level;
|
final level = instance._metadataRepository.systemConfig.log.level;
|
||||||
Logger.root.level = Level.LEVELS.elementAtOrNull(level.index) ?? Level.INFO;
|
Logger.root.level = Level.LEVELS.elementAtOrNull(level.index) ?? Level.INFO;
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
@@ -91,7 +91,7 @@ class LogService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> setLogLevel(LogLevel level) async {
|
Future<void> setLogLevel(LogLevel level) async {
|
||||||
await _metadataRepository.update(MetadataKind.systemConfig, (current) => current.copyWith(logLevel: level));
|
await _metadataRepository.write(MetadataKey.logLevel, level);
|
||||||
Logger.root.level = level.toLevel();
|
Logger.root.level = level.toLevel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
extension JsonHelper on Map<String, Object?> {
|
|
||||||
/// Returns the nested map under [key] or an empty const map if the key is absent or the value is not a map
|
|
||||||
Map<String, Object?> nested(String key) => (this[key] as Map<String, Object?>?) ?? const {};
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
import 'package:immich_mobile/domain/models/config/app_config.dart';
|
|
||||||
import 'package:immich_mobile/domain/models/metadata_kind.dart';
|
|
||||||
import 'package:immich_mobile/domain/models/metadata_value.dart';
|
|
||||||
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart';
|
|
||||||
|
|
||||||
class CachedMetadataRepository {
|
|
||||||
final MetadataRepository _repository;
|
|
||||||
final Map<MetadataKind, MetadataValue> _cache = {};
|
|
||||||
|
|
||||||
CachedMetadataRepository._(this._repository);
|
|
||||||
|
|
||||||
static CachedMetadataRepository? _instance;
|
|
||||||
static CachedMetadataRepository get instance {
|
|
||||||
if (_instance == null) {
|
|
||||||
throw UnsupportedError('CachedMetadataRepository not initialized. Call ensureInitialized() first');
|
|
||||||
}
|
|
||||||
return _instance!;
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<CachedMetadataRepository> ensureInitialized({required MetadataRepository repository}) async {
|
|
||||||
if (_instance == null) {
|
|
||||||
final instance = CachedMetadataRepository._(repository);
|
|
||||||
await instance._hydrate();
|
|
||||||
_instance = instance;
|
|
||||||
}
|
|
||||||
return _instance!;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _hydrate() async {
|
|
||||||
for (final kind in MetadataKind.values) {
|
|
||||||
_cache[kind] = await _repository.get(kind);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
T read<T extends MetadataValue>(MetadataKind<T> kind) => (_cache[kind] as T?) ?? kind.defaultValue;
|
|
||||||
|
|
||||||
Future<void> update<T extends MetadataValue>(MetadataKind<T> kind, T Function(T current) mutator) async {
|
|
||||||
final current = read(kind);
|
|
||||||
final updated = mutator(current);
|
|
||||||
if (_cache[kind] == updated) return;
|
|
||||||
await _repository.set(kind, updated);
|
|
||||||
_cache[kind] = updated;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> setAppConfig(AppConfig Function(AppConfig current) mutator) {
|
|
||||||
return update(MetadataKind.appConfig, (c) => mutator.call(c));
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> clear<T extends MetadataValue>(MetadataKind<T> kind) async {
|
|
||||||
await _repository.delete(kind);
|
|
||||||
_cache[kind] = kind.defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
Stream<T> watch<T extends MetadataValue>(MetadataKind<T> kind) => _repository.watch(kind);
|
|
||||||
}
|
|
||||||
@@ -1,43 +1,110 @@
|
|||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:immich_mobile/domain/models/metadata_kind.dart';
|
import 'package:immich_mobile/domain/models/config/app_config.dart';
|
||||||
import 'package:immich_mobile/domain/models/metadata_value.dart';
|
import 'package:immich_mobile/domain/models/config/log_config.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/config/system_config.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/config/theme_config.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/metadata_key.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/metadata.entity.drift.dart';
|
import 'package:immich_mobile/infrastructure/entities/metadata.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
|
|
||||||
class MetadataRepository extends DriftDatabaseRepository {
|
class MetadataRepository extends DriftDatabaseRepository {
|
||||||
final Drift _db;
|
final Drift _db;
|
||||||
|
final Map<MetadataKey, Object> _cache = {};
|
||||||
|
|
||||||
const MetadataRepository(this._db) : super(_db);
|
MetadataRepository._(this._db) : super(_db);
|
||||||
|
|
||||||
Future<T> get<T extends MetadataValue>(MetadataKind<T> kind) async {
|
static MetadataRepository? _instance;
|
||||||
final row = await (_db.select(_db.metadataEntity)..where((t) => t.key.equals(kind.key))).getSingleOrNull();
|
|
||||||
return _toValue(kind, row) as T;
|
static MetadataRepository get instance {
|
||||||
|
final instance = _instance;
|
||||||
|
if (instance == null) {
|
||||||
|
throw StateError('MetadataRepository not initialized. Call ensureInitialized() first');
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> set<T extends MetadataValue>(MetadataKind<T> kind, T value) async {
|
static Future<MetadataRepository> ensureInitialized(Drift db) async {
|
||||||
|
if (_instance == null) {
|
||||||
|
final instance = MetadataRepository._(db);
|
||||||
|
await instance._hydrate();
|
||||||
|
_instance = instance;
|
||||||
|
}
|
||||||
|
return _instance!;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> refresh() async {
|
||||||
|
instance._cache.clear();
|
||||||
|
await instance._hydrate();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _hydrate() async {
|
||||||
|
final rows = await _db.select(_db.metadataEntity).get();
|
||||||
|
for (final row in rows) {
|
||||||
|
final key = MetadataKey.fromKey(row.key);
|
||||||
|
if (key != null) _cache[key] = _decode(key, row.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
T _read<T extends Object>(MetadataKey<T> key) => (_cache[key] as T?) ?? key.defaultValue;
|
||||||
|
|
||||||
|
Future<void> write<T extends Object>(MetadataKey<T> key, T value) async {
|
||||||
|
if (_read(key) == value) return;
|
||||||
|
|
||||||
await _db
|
await _db
|
||||||
.into(_db.metadataEntity)
|
.into(_db.metadataEntity)
|
||||||
.insertOnConflictUpdate(
|
.insertOnConflictUpdate(
|
||||||
MetadataEntityCompanion.insert(
|
MetadataEntityCompanion.insert(key: key.key, value: _encode(value), updatedAt: Value(DateTime.now())),
|
||||||
key: kind.key,
|
|
||||||
value: jsonEncode(value.toJson()),
|
|
||||||
updatedAt: Value(DateTime.now()),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
_cache[key] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> delete<T extends MetadataValue>(MetadataKind<T> kind) async {
|
String _encode<T extends Object>(T value) => switch (value) {
|
||||||
await (_db.delete(_db.metadataEntity)..where((t) => t.key.equals(kind.key))).go();
|
Enum() => value.name,
|
||||||
|
DateTime() => value.toIso8601String(),
|
||||||
|
_ => throw ArgumentError('Unsupported metadata value type: ${value.runtimeType}'),
|
||||||
|
};
|
||||||
|
|
||||||
|
T _decode<T extends Object>(MetadataKey<T> key, String raw) {
|
||||||
|
final enumValues = key.enumValues;
|
||||||
|
if (enumValues != null) {
|
||||||
|
return enumValues.where((v) => (v as Enum).name == raw).firstOrNull ?? key.defaultValue;
|
||||||
|
}
|
||||||
|
return switch (key.defaultValue) {
|
||||||
|
DateTime() => (DateTime.tryParse(raw) ?? key.defaultValue) as T,
|
||||||
|
_ => throw ArgumentError('Unsupported metadata value type: ${key.defaultValue.runtimeType}'),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream<T> watch<T extends MetadataValue>(MetadataKind<T> kind) {
|
Future<void> delete<T extends Object>(MetadataKey<T> key) async {
|
||||||
return (_db.select(
|
_cache[key] = key.defaultValue;
|
||||||
_db.metadataEntity,
|
await (_db.delete(_db.metadataEntity)..where((t) => t.key.equals(key.key))).go();
|
||||||
)..where((t) => t.key.equals(kind.key))).watchSingleOrNull().map((row) => _toValue(kind, row) as T);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MetadataValue _toValue(MetadataKind kind, MetadataEntityData? row) =>
|
Future<void> clearDomain(MetadataDomain domain) async {
|
||||||
row == null ? kind.defaultValue : kind.fromJson(jsonDecode(row.value) as Map<String, Object?>);
|
for (final k in MetadataKey.values.where((k) => k.domain == domain)) {
|
||||||
|
_cache[k] = k.defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
await (_db.delete(_db.metadataEntity)..where((t) => t.key.like('${domain.prefix}.%'))).go();
|
||||||
|
}
|
||||||
|
|
||||||
|
AppConfig get appConfig => AppConfig(theme: ThemeConfig(mode: _read(MetadataKey.themeMode)));
|
||||||
|
|
||||||
|
SystemConfig get systemConfig => SystemConfig(log: LogConfig(level: _read(MetadataKey.logLevel)));
|
||||||
|
|
||||||
|
Stream<AppConfig> watchAppConfig() => _watchDomain(MetadataDomain.appConfig).map((_) => appConfig).distinct();
|
||||||
|
|
||||||
|
Stream<SystemConfig> watchSystemConfig() =>
|
||||||
|
_watchDomain(MetadataDomain.systemConfig).map((_) => systemConfig).distinct();
|
||||||
|
|
||||||
|
Stream<void> _watchDomain(MetadataDomain domain) {
|
||||||
|
final query = _db.select(_db.metadataEntity)..where((t) => t.key.like('${domain.prefix}.%'));
|
||||||
|
return query.watch().map((rows) => rows.forEach(_updateCacheForRow));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateCacheForRow(MetadataEntityData row) {
|
||||||
|
final key = MetadataKey.fromKey(row.key);
|
||||||
|
if (key == null) return;
|
||||||
|
_cache[key] = _decode(key, row.value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,20 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/domain/models/config/app_config.dart';
|
import 'package:immich_mobile/domain/models/config/app_config.dart';
|
||||||
import 'package:immich_mobile/domain/models/config/system_config.dart';
|
import 'package:immich_mobile/domain/models/config/system_config.dart';
|
||||||
import 'package:immich_mobile/domain/models/metadata/system_metadata.dart';
|
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart';
|
||||||
import 'package:immich_mobile/domain/models/metadata_kind.dart';
|
|
||||||
import 'package:immich_mobile/infrastructure/repositories/cached_metadata.repository.dart';
|
|
||||||
|
|
||||||
final metadataProvider = Provider.autoDispose<CachedMetadataRepository>((_) => CachedMetadataRepository.instance);
|
final metadataProvider = Provider<MetadataRepository>((_) => MetadataRepository.instance);
|
||||||
|
|
||||||
final appConfigProvider = Provider.autoDispose<AppConfig>((ref) {
|
final appConfigProvider = Provider.autoDispose<AppConfig>((ref) {
|
||||||
final subscription = ref.watch(metadataProvider).watch(MetadataKind.appConfig).listen((event) {
|
final repo = ref.watch(metadataProvider);
|
||||||
ref.state = event;
|
final subscription = repo.watchAppConfig().listen((event) => ref.state = event);
|
||||||
});
|
|
||||||
ref.onDispose(subscription.cancel);
|
ref.onDispose(subscription.cancel);
|
||||||
return ref.watch(metadataProvider).read(MetadataKind.appConfig);
|
return repo.appConfig;
|
||||||
});
|
});
|
||||||
|
|
||||||
final systemConfigProvider = Provider.autoDispose<SystemConfig>((ref) {
|
final systemConfigProvider = Provider.autoDispose<SystemConfig>((ref) {
|
||||||
final subscription = ref.watch(metadataProvider).watch(MetadataKind.systemConfig).listen((event) {
|
final repo = ref.watch(metadataProvider);
|
||||||
ref.state = event;
|
final subscription = repo.watchSystemConfig().listen((event) => ref.state = event);
|
||||||
});
|
|
||||||
ref.onDispose(subscription.cancel);
|
ref.onDispose(subscription.cancel);
|
||||||
return ref.watch(metadataProvider).read(MetadataKind.systemConfig);
|
return repo.systemConfig;
|
||||||
});
|
|
||||||
|
|
||||||
final systemMetadataProvider = Provider.autoDispose<SystemMetadata>((ref) {
|
|
||||||
final subscription = ref.watch(metadataProvider).watch(MetadataKind.systemMetadata).listen((event) {
|
|
||||||
ref.state = event;
|
|
||||||
});
|
|
||||||
ref.onDispose(subscription.cancel);
|
|
||||||
return ref.watch(metadataProvider).read(MetadataKind.systemMetadata);
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import 'package:immich_mobile/theme/dynamic_theme.dart';
|
|||||||
import 'package:immich_mobile/theme/theme_data.dart';
|
import 'package:immich_mobile/theme/theme_data.dart';
|
||||||
import 'package:immich_mobile/utils/debug_print.dart';
|
import 'package:immich_mobile/utils/debug_print.dart';
|
||||||
|
|
||||||
final immichThemeModeProvider = StateProvider<ThemeMode>((ref) => ref.read(appConfigProvider).theme.mode);
|
final immichThemeModeProvider = StateProvider<ThemeMode>((ref) => ref.watch(appConfigProvider).theme.mode);
|
||||||
|
|
||||||
final immichThemePresetProvider = StateProvider<ImmichColorPreset>((ref) {
|
final immichThemePresetProvider = StateProvider<ImmichColorPreset>((ref) {
|
||||||
final appSettingsProvider = ref.watch(appSettingsServiceProvider);
|
final appSettingsProvider = ref.watch(appSettingsServiceProvider);
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/domain/models/metadata_kind.dart';
|
import 'package:immich_mobile/domain/models/metadata_key.dart';
|
||||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||||
import 'package:immich_mobile/domain/utils/background_sync.dart';
|
import 'package:immich_mobile/domain/utils/background_sync.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/cached_metadata.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
|
||||||
import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart';
|
import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart';
|
||||||
import 'package:immich_mobile/models/auth/login_response.model.dart';
|
import 'package:immich_mobile/models/auth/login_response.model.dart';
|
||||||
@@ -40,7 +40,7 @@ class AuthService {
|
|||||||
final NetworkService _networkService;
|
final NetworkService _networkService;
|
||||||
final BackgroundSyncManager _backgroundSyncManager;
|
final BackgroundSyncManager _backgroundSyncManager;
|
||||||
final AppSettingsService _appSettingsService;
|
final AppSettingsService _appSettingsService;
|
||||||
final CachedMetadataRepository _metadataRepository;
|
final MetadataRepository _metadataRepository;
|
||||||
final _log = Logger("AuthService");
|
final _log = Logger("AuthService");
|
||||||
|
|
||||||
AuthService(
|
AuthService(
|
||||||
@@ -134,7 +134,7 @@ class AuthService {
|
|||||||
Store.delete(StoreKey.preferredWifiName),
|
Store.delete(StoreKey.preferredWifiName),
|
||||||
Store.delete(StoreKey.localEndpoint),
|
Store.delete(StoreKey.localEndpoint),
|
||||||
Store.delete(StoreKey.externalEndpointList),
|
Store.delete(StoreKey.externalEndpointList),
|
||||||
_metadataRepository.clear(MetadataKind.appConfig),
|
_metadataRepository.clearDomain(MetadataDomain.appConfig),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import 'package:immich_mobile/constants/constants.dart';
|
|||||||
import 'package:immich_mobile/domain/services/log.service.dart';
|
import 'package:immich_mobile/domain/services/log.service.dart';
|
||||||
import 'package:immich_mobile/domain/services/store.service.dart';
|
import 'package:immich_mobile/domain/services/store.service.dart';
|
||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/cached_metadata.repository.dart';
|
|
||||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart';
|
||||||
@@ -50,13 +49,11 @@ abstract final class Bootstrap {
|
|||||||
|
|
||||||
await StoreService.init(storeRepository: storeRepo, listenUpdates: listenStoreUpdates);
|
await StoreService.init(storeRepository: storeRepo, listenUpdates: listenStoreUpdates);
|
||||||
|
|
||||||
final cachedMetadataRepository = await CachedMetadataRepository.ensureInitialized(
|
final metadataRepo = await MetadataRepository.ensureInitialized(drift);
|
||||||
repository: MetadataRepository(drift),
|
|
||||||
);
|
|
||||||
|
|
||||||
await LogService.init(
|
await LogService.init(
|
||||||
logRepository: LogRepository(logDb),
|
logRepository: LogRepository(logDb),
|
||||||
metadataRepository: cachedMetadataRepository,
|
metadataRepository: metadataRepo,
|
||||||
shouldBuffer: shouldBufferLogs,
|
shouldBuffer: shouldBufferLogs,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ import 'dart:async';
|
|||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:immich_mobile/domain/models/log.model.dart';
|
import 'package:immich_mobile/domain/models/log.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/metadata_kind.dart';
|
import 'package:immich_mobile/domain/models/metadata_key.dart';
|
||||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||||
import 'package:immich_mobile/domain/services/log.service.dart';
|
import 'package:immich_mobile/domain/services/log.service.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/cached_metadata.repository.dart';
|
|
||||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/network.repository.dart';
|
||||||
import 'package:immich_mobile/services/api.service.dart';
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
|
|
||||||
@@ -43,20 +43,19 @@ Future<void> _migrateTo26(Drift drift) async {
|
|||||||
const int themeModeKey = 102;
|
const int themeModeKey = 102;
|
||||||
const int logLevelKey = 115;
|
const int logLevelKey = 115;
|
||||||
|
|
||||||
final cache = CachedMetadataRepository.instance;
|
final repo = MetadataRepository.instance;
|
||||||
final migrated = <int>[];
|
final migrated = <int>[];
|
||||||
|
|
||||||
final themeMode = await _readLegacyStoreString(drift, themeModeKey);
|
final themeMode = await _readLegacyStoreString(drift, themeModeKey);
|
||||||
if (themeMode != null) {
|
if (themeMode != null) {
|
||||||
final mode = ThemeMode.values.firstWhere((m) => m.name == themeMode, orElse: () => ThemeMode.system);
|
final mode = ThemeMode.values.firstWhere((m) => m.name == themeMode, orElse: () => ThemeMode.system);
|
||||||
await cache.update(MetadataKind.appConfig, (current) => current.copyWith(themeMode: mode));
|
await repo.write(MetadataKey.themeMode, mode);
|
||||||
migrated.add(themeModeKey);
|
migrated.add(themeModeKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
final logLevelIndex = await _readLegacyStoreInt(drift, logLevelKey);
|
final logLevelIndex = await _readLegacyStoreInt(drift, logLevelKey);
|
||||||
if (logLevelIndex != null) {
|
if (logLevelIndex != null) {
|
||||||
final logLevel = LogLevel.values.elementAtOrNull(logLevelIndex) ?? LogLevel.info;
|
final logLevel = LogLevel.values.elementAtOrNull(logLevelIndex) ?? LogLevel.info;
|
||||||
await cache.update(MetadataKind.systemConfig, (current) => current.copyWith(logLevel: logLevel));
|
|
||||||
await LogService.I.setLogLevel(logLevel);
|
await LogService.I.setLogLevel(logLevel);
|
||||||
migrated.add(logLevelKey);
|
migrated.add(logLevelKey);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/domain/models/metadata_kind.dart';
|
import 'package:immich_mobile/domain/models/metadata_key.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/cached_metadata.repository.dart';
|
|
||||||
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
|
||||||
import 'package:immich_mobile/providers/theme.provider.dart';
|
import 'package:immich_mobile/providers/theme.provider.dart';
|
||||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||||
@@ -38,7 +37,7 @@ class ThemeSetting extends HookConsumerWidget {
|
|||||||
ref.watch(immichThemeModeProvider.notifier).state = ThemeMode.light;
|
ref.watch(immichThemeModeProvider.notifier).state = ThemeMode.light;
|
||||||
currentTheme.value = ThemeMode.light;
|
currentTheme.value = ThemeMode.light;
|
||||||
}
|
}
|
||||||
ref.read(metadataProvider).setAppConfig((config) => config.copyWith(themeMode: currentTheme.value));
|
ref.read(metadataProvider).write(MetadataKey.themeMode, currentTheme.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
void onSystemThemeChange(bool isSystem) {
|
void onSystemThemeChange(bool isSystem) {
|
||||||
@@ -58,9 +57,7 @@ class ThemeSetting extends HookConsumerWidget {
|
|||||||
ref.watch(immichThemeModeProvider.notifier).state = ThemeMode.dark;
|
ref.watch(immichThemeModeProvider.notifier).state = ThemeMode.dark;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ref
|
ref.read(metadataProvider).write(MetadataKey.themeMode, currentTheme.value);
|
||||||
.read(metadataProvider)
|
|
||||||
.update(MetadataKind.appConfig, (appConfig) => appConfig.copyWith(themeMode: currentTheme.value));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void onSurfaceColorSettingChange(bool useColorfulInterface) {
|
void onSurfaceColorSettingChange(bool useColorfulInterface) {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import 'package:immich_mobile/constants/constants.dart';
|
|||||||
import 'package:immich_mobile/domain/models/config/log_config.dart';
|
import 'package:immich_mobile/domain/models/config/log_config.dart';
|
||||||
import 'package:immich_mobile/domain/models/config/system_config.dart';
|
import 'package:immich_mobile/domain/models/config/system_config.dart';
|
||||||
import 'package:immich_mobile/domain/models/log.model.dart';
|
import 'package:immich_mobile/domain/models/log.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/metadata_kind.dart';
|
import 'package:immich_mobile/domain/models/metadata_key.dart';
|
||||||
import 'package:immich_mobile/domain/services/log.service.dart';
|
import 'package:immich_mobile/domain/services/log.service.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
@@ -30,21 +30,20 @@ final _kWarnLog = LogMessage(
|
|||||||
void main() {
|
void main() {
|
||||||
late LogService sut;
|
late LogService sut;
|
||||||
late LogRepository mockLogRepo;
|
late LogRepository mockLogRepo;
|
||||||
late MockCachedMetadataRepository mockMetadataRepository;
|
late MockMetadataRepository mockMetadataRepository;
|
||||||
|
|
||||||
setUp(() async {
|
setUp(() async {
|
||||||
mockLogRepo = MockLogRepository();
|
mockLogRepo = MockLogRepository();
|
||||||
mockMetadataRepository = MockCachedMetadataRepository();
|
mockMetadataRepository = MockMetadataRepository();
|
||||||
|
|
||||||
registerFallbackValue(_kInfoLog);
|
registerFallbackValue(_kInfoLog);
|
||||||
SystemConfig identityMutator(SystemConfig c) => c;
|
registerFallbackValue(LogLevel.info);
|
||||||
registerFallbackValue(identityMutator);
|
|
||||||
|
|
||||||
when(() => mockLogRepo.truncate(limit: any(named: 'limit'))).thenAnswer((_) async => {});
|
when(() => mockLogRepo.truncate(limit: any(named: 'limit'))).thenAnswer((_) async => {});
|
||||||
when(
|
when(
|
||||||
() => mockMetadataRepository.read(MetadataKind.systemConfig),
|
() => mockMetadataRepository.systemConfig,
|
||||||
).thenReturn(const SystemConfig(log: LogConfig(level: LogLevel.fine)));
|
).thenReturn(const SystemConfig(log: LogConfig(level: LogLevel.fine)));
|
||||||
when(() => mockMetadataRepository.update<SystemConfig>(MetadataKind.systemConfig, any())).thenAnswer((_) async {});
|
when(() => mockMetadataRepository.write<LogLevel>(MetadataKey.logLevel, any())).thenAnswer((_) async {});
|
||||||
when(() => mockLogRepo.getAll()).thenAnswer((_) async => []);
|
when(() => mockLogRepo.getAll()).thenAnswer((_) async => []);
|
||||||
when(() => mockLogRepo.insert(any())).thenAnswer((_) async => true);
|
when(() => mockLogRepo.insert(any())).thenAnswer((_) async => true);
|
||||||
when(() => mockLogRepo.insertAll(any())).thenAnswer((_) async => true);
|
when(() => mockLogRepo.insertAll(any())).thenAnswer((_) async => true);
|
||||||
@@ -63,7 +62,7 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('Sets log level based on the metadata repository', () {
|
test('Sets log level based on the metadata repository', () {
|
||||||
verify(() => mockMetadataRepository.read(MetadataKind.systemConfig)).called(1);
|
verify(() => mockMetadataRepository.systemConfig).called(1);
|
||||||
expect(Logger.root.level, Level.FINE);
|
expect(Logger.root.level, Level.FINE);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -74,13 +73,10 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('Updates the log level via metadata repository', () {
|
test('Updates the log level via metadata repository', () {
|
||||||
final mutator =
|
final captured = verify(
|
||||||
verify(
|
() => mockMetadataRepository.write<LogLevel>(MetadataKey.logLevel, captureAny()),
|
||||||
() => mockMetadataRepository.update<SystemConfig>(MetadataKind.systemConfig, captureAny()),
|
).captured.firstOrNull;
|
||||||
).captured.firstOrNull
|
expect(captured, LogLevel.shout);
|
||||||
as SystemConfig Function(SystemConfig)?;
|
|
||||||
final result = mutator?.call(const SystemConfig());
|
|
||||||
expect(result?.log.level, LogLevel.shout);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Sets log level on logger', () {
|
test('Sets log level on logger', () {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import 'package:immich_mobile/infrastructure/repositories/backup.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/backup.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/cached_metadata.repository.dart';
|
|
||||||
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
|
||||||
@@ -18,7 +18,7 @@ import 'package:mocktail/mocktail.dart';
|
|||||||
|
|
||||||
class MockDriftStoreRepository extends Mock implements DriftStoreRepository {}
|
class MockDriftStoreRepository extends Mock implements DriftStoreRepository {}
|
||||||
|
|
||||||
class MockCachedMetadataRepository extends Mock implements CachedMetadataRepository {}
|
class MockMetadataRepository extends Mock implements MetadataRepository {}
|
||||||
|
|
||||||
class MockLogRepository extends Mock implements LogRepository {}
|
class MockLogRepository extends Mock implements LogRepository {}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/log.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/metadata_key.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/metadata.entity.drift.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart';
|
||||||
|
|
||||||
|
import '../repository_context.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
late MediumRepositoryContext ctx;
|
||||||
|
late MetadataRepository sut;
|
||||||
|
|
||||||
|
setUpAll(() async {
|
||||||
|
ctx = MediumRepositoryContext();
|
||||||
|
sut = await MetadataRepository.ensureInitialized(ctx.db);
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDownAll(() async {
|
||||||
|
await ctx.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
await ctx.db.delete(ctx.db.metadataEntity).go();
|
||||||
|
await MetadataRepository.refresh();
|
||||||
|
});
|
||||||
|
|
||||||
|
group('defaults', () {
|
||||||
|
test('appConfig returns key defaults when DB is empty', () {
|
||||||
|
expect(sut.appConfig.theme.mode, ThemeMode.system);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('systemConfig returns key defaults when DB is empty', () {
|
||||||
|
expect(sut.systemConfig.log.level, LogLevel.info);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('write', () {
|
||||||
|
test('persists a value and reflects it in the composed view', () async {
|
||||||
|
await sut.write(.themeMode, ThemeMode.dark);
|
||||||
|
expect(sut.appConfig.theme.mode, ThemeMode.dark);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('persists across domains independently', () async {
|
||||||
|
await sut.write(.themeMode, ThemeMode.light);
|
||||||
|
await sut.write(.logLevel, LogLevel.severe);
|
||||||
|
expect(sut.appConfig.theme.mode, ThemeMode.light);
|
||||||
|
expect(sut.systemConfig.log.level, LogLevel.severe);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('delete', () {
|
||||||
|
test('removes the row and reverts to default', () async {
|
||||||
|
await sut.write(.themeMode, ThemeMode.dark);
|
||||||
|
expect(sut.appConfig.theme.mode, ThemeMode.dark);
|
||||||
|
|
||||||
|
await sut.delete(.themeMode);
|
||||||
|
expect(sut.appConfig.theme.mode, ThemeMode.system);
|
||||||
|
|
||||||
|
final rows = await ctx.db.select(ctx.db.metadataEntity).get();
|
||||||
|
expect(rows, isEmpty);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('clearDomain', () {
|
||||||
|
test('clears every key in the domain and leaves other domains alone', () async {
|
||||||
|
await sut.write(.themeMode, ThemeMode.dark);
|
||||||
|
await sut.write(.logLevel, LogLevel.severe);
|
||||||
|
|
||||||
|
await sut.clearDomain(.appConfig);
|
||||||
|
|
||||||
|
expect(sut.appConfig.theme.mode, ThemeMode.system);
|
||||||
|
expect(sut.systemConfig.log.level, LogLevel.severe);
|
||||||
|
|
||||||
|
final remainingKeys = (await ctx.db.select(ctx.db.metadataEntity).get()).map((r) => r.key);
|
||||||
|
expect(remainingKeys, [MetadataKey.logLevel.key]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('refresh', () {
|
||||||
|
test('picks up rows that were inserted directly into the DB', () async {
|
||||||
|
await ctx.db
|
||||||
|
.into(ctx.db.metadataEntity)
|
||||||
|
.insert(
|
||||||
|
MetadataEntityCompanion.insert(
|
||||||
|
key: MetadataKey.themeMode.key,
|
||||||
|
value: ThemeMode.dark.name,
|
||||||
|
updatedAt: Value(DateTime.now()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Cache hasn't seen this row yet — view still returns the default.
|
||||||
|
expect(sut.appConfig.theme.mode, ThemeMode.system);
|
||||||
|
|
||||||
|
await MetadataRepository.refresh();
|
||||||
|
|
||||||
|
expect(sut.appConfig.theme.mode, ThemeMode.dark);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('drops cached values for rows that were deleted out from under the repo', () async {
|
||||||
|
await sut.write(.themeMode, ThemeMode.dark);
|
||||||
|
// Wipe the row directly. Cache still holds the old value.
|
||||||
|
await ctx.db.delete(ctx.db.metadataEntity).go();
|
||||||
|
expect(sut.appConfig.theme.mode, ThemeMode.dark);
|
||||||
|
|
||||||
|
await MetadataRepository.refresh();
|
||||||
|
|
||||||
|
expect(sut.appConfig.theme.mode, ThemeMode.system);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('skips rows whose key is unknown to MetadataKey', () async {
|
||||||
|
await ctx.db
|
||||||
|
.into(ctx.db.metadataEntity)
|
||||||
|
.insert(
|
||||||
|
MetadataEntityCompanion.insert(
|
||||||
|
key: 'app-config.unknown.future-key',
|
||||||
|
value: 'whatever',
|
||||||
|
updatedAt: Value(DateTime.now()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await MetadataRepository.refresh();
|
||||||
|
expect(sut.appConfig.theme.mode, ThemeMode.system);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('watch', () {
|
||||||
|
test('watchAppConfig emits the new value after a write', () async {
|
||||||
|
final expectation = expectLater(sut.watchAppConfig().map((c) => c.theme.mode), emitsThrough(ThemeMode.dark));
|
||||||
|
await sut.write(MetadataKey.themeMode, ThemeMode.dark);
|
||||||
|
await expectation;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('watchAppConfig does not emit when only system-config rows change', () async {
|
||||||
|
final emissions = <ThemeMode>[];
|
||||||
|
// skip(1) drops the on-subscribe replay so we only capture emissions caused by the write below.
|
||||||
|
final sub = sut.watchAppConfig().skip(1).listen((c) => emissions.add(c.theme.mode));
|
||||||
|
|
||||||
|
await sut.write(MetadataKey.logLevel, LogLevel.severe);
|
||||||
|
await pumpEventQueue();
|
||||||
|
await sub.cancel();
|
||||||
|
|
||||||
|
expect(emissions, isEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('watchSystemConfig emits the new value after a write', () async {
|
||||||
|
final expectation = expectLater(sut.watchSystemConfig().map((c) => c.log.level), emitsThrough(LogLevel.warning));
|
||||||
|
await sut.write(MetadataKey.logLevel, LogLevel.warning);
|
||||||
|
await expectation;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import 'package:drift/drift.dart' hide isNull;
|
|||||||
import 'package:drift/native.dart';
|
import 'package:drift/native.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/metadata_key.dart';
|
||||||
import 'package:immich_mobile/domain/services/store.service.dart';
|
import 'package:immich_mobile/domain/services/store.service.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
|
||||||
@@ -24,7 +25,7 @@ void main() {
|
|||||||
late MockNetworkService networkService;
|
late MockNetworkService networkService;
|
||||||
late MockBackgroundSyncManager backgroundSyncManager;
|
late MockBackgroundSyncManager backgroundSyncManager;
|
||||||
late MockAppSettingService appSettingsService;
|
late MockAppSettingService appSettingsService;
|
||||||
late MockCachedMetadataRepository metadataRepository;
|
late MockMetadataRepository metadataRepository;
|
||||||
late Drift db;
|
late Drift db;
|
||||||
|
|
||||||
setUp(() async {
|
setUp(() async {
|
||||||
@@ -34,7 +35,7 @@ void main() {
|
|||||||
networkService = MockNetworkService();
|
networkService = MockNetworkService();
|
||||||
backgroundSyncManager = MockBackgroundSyncManager();
|
backgroundSyncManager = MockBackgroundSyncManager();
|
||||||
appSettingsService = MockAppSettingService();
|
appSettingsService = MockAppSettingService();
|
||||||
metadataRepository = MockCachedMetadataRepository();
|
metadataRepository = MockMetadataRepository();
|
||||||
|
|
||||||
sut = AuthService(
|
sut = AuthService(
|
||||||
authApiRepository,
|
authApiRepository,
|
||||||
@@ -114,6 +115,10 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
group('logout', () {
|
group('logout', () {
|
||||||
|
setUp(() {
|
||||||
|
when(() => metadataRepository.clearDomain(MetadataDomain.appConfig)).thenAnswer((_) async {});
|
||||||
|
});
|
||||||
|
|
||||||
test('Should logout user', () async {
|
test('Should logout user', () async {
|
||||||
when(() => authApiRepository.logout()).thenAnswer((_) async => {});
|
when(() => authApiRepository.logout()).thenAnswer((_) async => {});
|
||||||
when(() => backgroundSyncManager.cancel()).thenAnswer((_) async => {});
|
when(() => backgroundSyncManager.cancel()).thenAnswer((_) async => {});
|
||||||
|
|||||||
Reference in New Issue
Block a user