From 6fdb841732cee49ba57f0a5d7ba8311ae5772872 Mon Sep 17 00:00:00 2001 From: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Date: Fri, 1 May 2026 23:03:23 +0700 Subject: [PATCH] refactor --- mobile/lib/domain/models/metadata_key.dart | 81 ++++++++++++++++++- .../repositories/metadata.repository.dart | 25 +----- .../metadata_repository_test.dart | 21 +++-- 3 files changed, 94 insertions(+), 33 deletions(-) diff --git a/mobile/lib/domain/models/metadata_key.dart b/mobile/lib/domain/models/metadata_key.dart index 24e3c0da25..2e122e6c5d 100644 --- a/mobile/lib/domain/models/metadata_key.dart +++ b/mobile/lib/domain/models/metadata_key.dart @@ -1,3 +1,4 @@ +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/domain/models/config/app_config.dart'; import 'package:immich_mobile/domain/models/config/system_config.dart'; @@ -12,17 +13,89 @@ enum MetadataDomain { } enum MetadataKey { - themeMode(.appConfig, 'theme.mode', .system, ThemeMode.values), - logLevel(.systemConfig, 'log.level', .info, LogLevel.values); + themeMode(.appConfig, 'theme.mode', .system, _EnumCodec(ThemeMode.values)), + logLevel(.systemConfig, 'log.level', .info, _EnumCodec(LogLevel.values)); final MetadataDomain domain; final String name; final T defaultValue; - final List? enumValues; + final _MetadataCodec? _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'; + _MetadataCodec get _codec => _codecOverride ?? _MetadataCodec.forPrimitive(defaultValue); + + String encode(T value) => _codec.encode(value); + + T decode(String raw) => _codec.decode(raw) ?? defaultValue; + static Map> asKeyMap() => {for (var value in MetadataKey.values) value.key: value}; } + +sealed class _MetadataCodec { + const _MetadataCodec(); + + String encode(T value); + T? decode(String raw); + + static const Map> _primitives = { + int: _PrimitiveCodec.integer, + double: _PrimitiveCodec.real, + bool: _PrimitiveCodec.boolean, + String: _PrimitiveCodec.string, + DateTime: _DateTimeCodec(), + }; + + static _MetadataCodec forPrimitive(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; + } +} + +final class _EnumCodec extends _MetadataCodec { + final List 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 { + const _DateTimeCodec(); + + @override + String encode(DateTime value) => value.toIso8601String(); + + @override + DateTime? decode(String raw) => DateTime.tryParse(raw); +} + +final class _PrimitiveCodec extends _MetadataCodec { + 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.tryParse); + static const real = _PrimitiveCodec._(double.tryParse); + static const boolean = _PrimitiveCodec._(bool.tryParse); + static const string = _PrimitiveCodec._(_identity); + + static String? _identity(String s) => s; +} diff --git a/mobile/lib/infrastructure/repositories/metadata.repository.dart b/mobile/lib/infrastructure/repositories/metadata.repository.dart index 1209961c07..d0e0ab2501 100644 --- a/mobile/lib/infrastructure/repositories/metadata.repository.dart +++ b/mobile/lib/infrastructure/repositories/metadata.repository.dart @@ -1,6 +1,4 @@ -import 'package:collection/collection.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/system_config.dart'; import 'package:immich_mobile/domain/models/metadata_key.dart'; @@ -55,30 +53,11 @@ class MetadataRepository extends DriftDatabaseRepository { await _db .into(_db.metadataEntity) .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); } - @visibleForTesting - static String encode(T value) => switch (value) { - Enum() => value.name, - DateTime() => value.toIso8601String(), - _ => throw ArgumentError('Unsupported metadata value type: ${value.runtimeType}'), - }; - - @visibleForTesting - static T decode(MetadataKey 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 delete(MetadataKey key) async { await (_db.delete(_db.metadataEntity)..where((t) => t.key.equals(key.key))).go(); _updateCache(key, key.defaultValue); @@ -101,7 +80,7 @@ class MetadataRepository extends DriftDatabaseRepository { for (final row in rows) { final key = keyMap[row.key]; if (key == null) continue; - _updateCache(key, decode(key, row.value)); + _updateCache(key, key.decode(row.value)); } } diff --git a/mobile/test/unit/repositories/metadata_repository_test.dart b/mobile/test/unit/repositories/metadata_repository_test.dart index 6b2cb2652e..4c29ce3a01 100644 --- a/mobile/test/unit/repositories/metadata_repository_test.dart +++ b/mobile/test/unit/repositories/metadata_repository_test.dart @@ -1,14 +1,23 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:immich_mobile/domain/models/metadata_key.dart'; -import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart'; void main() { - group('codec', () { - test('every MetadataKey encodes and decodes values losslessly', () { + group('MetadataKey', () { + test('every key round-trips its default value losslessly', () { for (final key in MetadataKey.values) { - final encoded = MetadataRepository.encode(key.defaultValue); - final decoded = MetadataRepository.decode(key, encoded); - expect(decoded, key.defaultValue, reason: 'codec round-trip failed for ${key.name}'); + final encoded = key.encode(key.defaultValue); + final decoded = key.decode(encoded); + 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}', + ); } }); });