mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
refactor
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.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';
|
||||||
@@ -12,17 +13,89 @@ enum MetadataDomain<T extends Object> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum MetadataKey<T extends Object> {
|
enum MetadataKey<T extends Object> {
|
||||||
themeMode<ThemeMode>(.appConfig, 'theme.mode', .system, ThemeMode.values),
|
themeMode<ThemeMode>(.appConfig, 'theme.mode', .system, _EnumCodec(ThemeMode.values)),
|
||||||
logLevel<LogLevel>(.systemConfig, 'log.level', .info, LogLevel.values);
|
logLevel<LogLevel>(.systemConfig, 'log.level', .info, _EnumCodec(LogLevel.values));
|
||||||
|
|
||||||
final MetadataDomain domain;
|
final MetadataDomain domain;
|
||||||
final String name;
|
final String name;
|
||||||
final T defaultValue;
|
final T defaultValue;
|
||||||
final List<T>? enumValues;
|
final _MetadataCodec<T>? _codecOverride;
|
||||||
|
|
||||||
const MetadataKey(this.domain, this.name, this.defaultValue, [this.enumValues]);
|
const MetadataKey(this.domain, this.name, this.defaultValue, [this._codecOverride]);
|
||||||
|
|
||||||
String get key => '${domain.prefix}.$name';
|
String get key => '${domain.prefix}.$name';
|
||||||
|
|
||||||
|
_MetadataCodec<T> get _codec => _codecOverride ?? _MetadataCodec.forPrimitive(defaultValue);
|
||||||
|
|
||||||
|
String encode(T value) => _codec.encode(value);
|
||||||
|
|
||||||
|
T decode(String raw) => _codec.decode(raw) ?? defaultValue;
|
||||||
|
|
||||||
static Map<String, MetadataKey<Object>> asKeyMap() => {for (var value in MetadataKey.values) value.key: value};
|
static Map<String, MetadataKey<Object>> asKeyMap() => {for (var value in MetadataKey.values) value.key: value};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sealed class _MetadataCodec<T extends Object> {
|
||||||
|
const _MetadataCodec();
|
||||||
|
|
||||||
|
String encode(T value);
|
||||||
|
T? decode(String raw);
|
||||||
|
|
||||||
|
static const Map<Type, _MetadataCodec<Object>> _primitives = {
|
||||||
|
int: _PrimitiveCodec.integer,
|
||||||
|
double: _PrimitiveCodec.real,
|
||||||
|
bool: _PrimitiveCodec.boolean,
|
||||||
|
String: _PrimitiveCodec.string,
|
||||||
|
DateTime: _DateTimeCodec(),
|
||||||
|
};
|
||||||
|
|
||||||
|
static _MetadataCodec<T> forPrimitive<T extends Object>(T sample) {
|
||||||
|
final codec = _primitives[sample.runtimeType];
|
||||||
|
if (codec == null) {
|
||||||
|
throw StateError(
|
||||||
|
'No primitive codec for ${sample.runtimeType}. Provide an explicit codec when defining the MetadataKey.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return codec as _MetadataCodec<T>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class _EnumCodec<T extends Enum> extends _MetadataCodec<T> {
|
||||||
|
final List<T> values;
|
||||||
|
|
||||||
|
const _EnumCodec(this.values);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String encode(T value) => value.name;
|
||||||
|
|
||||||
|
@override
|
||||||
|
T? decode(String raw) => values.firstWhereOrNull((v) => v.name == raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
final class _DateTimeCodec extends _MetadataCodec<DateTime> {
|
||||||
|
const _DateTimeCodec();
|
||||||
|
|
||||||
|
@override
|
||||||
|
String encode(DateTime value) => value.toIso8601String();
|
||||||
|
|
||||||
|
@override
|
||||||
|
DateTime? decode(String raw) => DateTime.tryParse(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
final class _PrimitiveCodec<T extends Object> extends _MetadataCodec<T> {
|
||||||
|
final T? Function(String) _parse;
|
||||||
|
|
||||||
|
const _PrimitiveCodec._(this._parse);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String encode(T value) => value.toString();
|
||||||
|
|
||||||
|
@override
|
||||||
|
T? decode(String raw) => _parse(raw);
|
||||||
|
|
||||||
|
static const integer = _PrimitiveCodec<int>._(int.tryParse);
|
||||||
|
static const real = _PrimitiveCodec<double>._(double.tryParse);
|
||||||
|
static const boolean = _PrimitiveCodec<bool>._(bool.tryParse);
|
||||||
|
static const string = _PrimitiveCodec<String>._(_identity);
|
||||||
|
|
||||||
|
static String? _identity(String s) => s;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:flutter/foundation.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_key.dart';
|
import 'package:immich_mobile/domain/models/metadata_key.dart';
|
||||||
@@ -55,30 +53,11 @@ class MetadataRepository extends DriftDatabaseRepository {
|
|||||||
await _db
|
await _db
|
||||||
.into(_db.metadataEntity)
|
.into(_db.metadataEntity)
|
||||||
.insertOnConflictUpdate(
|
.insertOnConflictUpdate(
|
||||||
MetadataEntityCompanion.insert(key: key.key, value: encode(value), updatedAt: Value(DateTime.now())),
|
MetadataEntityCompanion.insert(key: key.key, value: key.encode(value), updatedAt: Value(DateTime.now())),
|
||||||
);
|
);
|
||||||
_updateCache(key, value);
|
_updateCache(key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@visibleForTesting
|
|
||||||
static String encode<T extends Object>(T value) => switch (value) {
|
|
||||||
Enum() => value.name,
|
|
||||||
DateTime() => value.toIso8601String(),
|
|
||||||
_ => throw ArgumentError('Unsupported metadata value type: ${value.runtimeType}'),
|
|
||||||
};
|
|
||||||
|
|
||||||
@visibleForTesting
|
|
||||||
static T decode<T extends Object>(MetadataKey<T> key, String raw) {
|
|
||||||
final enumValues = key.enumValues;
|
|
||||||
if (enumValues != null) {
|
|
||||||
return enumValues.firstWhereOrNull((v) => (v as Enum).name == raw) ?? key.defaultValue;
|
|
||||||
}
|
|
||||||
return switch (key.defaultValue) {
|
|
||||||
DateTime() => (DateTime.tryParse(raw) ?? key.defaultValue) as T,
|
|
||||||
_ => throw ArgumentError('Unsupported metadata value type: ${key.defaultValue.runtimeType}'),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> delete<T extends Object>(MetadataKey<T> key) async {
|
Future<void> delete<T extends Object>(MetadataKey<T> key) async {
|
||||||
await (_db.delete(_db.metadataEntity)..where((t) => t.key.equals(key.key))).go();
|
await (_db.delete(_db.metadataEntity)..where((t) => t.key.equals(key.key))).go();
|
||||||
_updateCache(key, key.defaultValue);
|
_updateCache(key, key.defaultValue);
|
||||||
@@ -101,7 +80,7 @@ class MetadataRepository extends DriftDatabaseRepository {
|
|||||||
for (final row in rows) {
|
for (final row in rows) {
|
||||||
final key = keyMap[row.key];
|
final key = keyMap[row.key];
|
||||||
if (key == null) continue;
|
if (key == null) continue;
|
||||||
_updateCache(key, decode(key, row.value));
|
_updateCache(key, key.decode(row.value));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,23 @@
|
|||||||
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/models/metadata_key.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart';
|
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
group('codec', () {
|
group('MetadataKey', () {
|
||||||
test('every MetadataKey encodes and decodes values losslessly', () {
|
test('every key round-trips its default value losslessly', () {
|
||||||
for (final key in MetadataKey.values) {
|
for (final key in MetadataKey.values) {
|
||||||
final encoded = MetadataRepository.encode(key.defaultValue);
|
final encoded = key.encode(key.defaultValue);
|
||||||
final decoded = MetadataRepository.decode(key, encoded);
|
final decoded = key.decode(encoded);
|
||||||
expect(decoded, key.defaultValue, reason: 'codec round-trip failed for ${key.name}');
|
expect(decoded, key.defaultValue, reason: 'round-trip failed for ${key.name}');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('decode falls back to the default value when the raw input is unparseable', () {
|
||||||
|
for (final key in MetadataKey.values) {
|
||||||
|
expect(
|
||||||
|
key.decode('not a valid encoding for any key'),
|
||||||
|
key.defaultValue,
|
||||||
|
reason: 'fallback failed for ${key.name}',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user