From 3485dbffdae5221fe255f60e461280944c48c024 Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Wed, 22 Jul 2026 01:53:50 +0530 Subject: [PATCH] refactor(mobile): extract CachedKeyValueRepository (#28789) Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- mobile/lib/domain/models/settings_key.dart | 187 ++---------------- mobile/lib/domain/models/value_codec.dart | 153 ++++++++++++++ .../cached_key_value_repository.dart | 41 ++++ .../repositories/settings.repository.dart | 56 +++--- 4 files changed, 236 insertions(+), 201 deletions(-) create mode 100644 mobile/lib/domain/models/value_codec.dart create mode 100644 mobile/lib/infrastructure/repositories/cached_key_value_repository.dart diff --git a/mobile/lib/domain/models/settings_key.dart b/mobile/lib/domain/models/settings_key.dart index 00c5286e07..4308d69555 100644 --- a/mobile/lib/domain/models/settings_key.dart +++ b/mobile/lib/domain/models/settings_key.dart @@ -1,17 +1,16 @@ -import 'dart:convert'; - import 'package:flutter/material.dart'; import 'package:immich_mobile/constants/colors.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/log.model.dart'; import 'package:immich_mobile/domain/models/timeline.model.dart'; +import 'package:immich_mobile/domain/models/value_codec.dart'; import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; import 'package:immich_mobile/utils/semver.dart'; enum SettingsKey { // Theme - themePrimaryColor(codec: _EnumCodec(ImmichColorPreset.values)), - themeMode(codec: _EnumCodec(ThemeMode.values)), + themePrimaryColor(codec: EnumCodec(ImmichColorPreset.values)), + themeMode(codec: EnumCodec(ThemeMode.values)), themeDynamic(), themeColorfulInterface(), @@ -27,13 +26,13 @@ enum SettingsKey { // Network networkAutoEndpointSwitching(), + networkExternalEndpointList>(codec: ListCodec(PrimitiveCodec.string)), + networkCustomHeaders>(codec: MapCodec(PrimitiveCodec.string, PrimitiveCodec.string)), networkPreferredWifiName(), networkLocalEndpoint(), - networkExternalEndpointList>(codec: _ListCodec(_PrimitiveCodec.string)), - networkCustomHeaders>(codec: _MapCodec(_PrimitiveCodec.string, _PrimitiveCodec.string)), // Album - albumSortMode(codec: _EnumCodec(AlbumSortMode.values)), + albumSortMode(codec: EnumCodec(AlbumSortMode.values)), albumIsReverse(), albumIsGrid(), @@ -47,195 +46,45 @@ enum SettingsKey { // Timeline timelineTilesPerRow(), - timelineGroupAssetsBy(codec: _EnumCodec(GroupAssetsBy.values)), + timelineGroupAssetsBy(codec: EnumCodec(GroupAssetsBy.values)), timelineStorageIndicator(), // Log - logLevel(codec: _EnumCodec(LogLevel.values)), + logLevel(codec: EnumCodec(LogLevel.values)), // Map mapShowFavoriteOnly(), mapRelativeDate(), mapIncludeArchived(), - mapThemeMode(codec: _EnumCodec(ThemeMode.values)), + mapThemeMode(codec: EnumCodec(ThemeMode.values)), mapWithPartners(), // Cleanup cleanupKeepFavorites(), - cleanupKeepMediaType(codec: _EnumCodec(AssetKeepType.values)), - cleanupKeepAlbumIds>(codec: _ListCodec(_PrimitiveCodec.string)), + cleanupKeepMediaType(codec: EnumCodec(AssetKeepType.values)), + cleanupKeepAlbumIds>(codec: ListCodec(PrimitiveCodec.string)), cleanupCutoffDaysAgo(), cleanupDefaultsInitialized(), // Share - shareFileType(codec: _EnumCodec(ShareAssetType.values)), + shareFileType(codec: EnumCodec(ShareAssetType.values)), // Slideshow slideshowRepeat(), slideshowDuration(), - slideshowLook(codec: _EnumCodec(SlideshowLook.values)), - slideshowDirection(codec: _EnumCodec(SlideshowDirection.values)), + slideshowLook(codec: EnumCodec(SlideshowLook.values)), + slideshowDirection(codec: EnumCodec(SlideshowDirection.values)), // Feature message - featureMessageSeenRelease(codec: _SemVerCodec()); + featureMessageSeenRelease(codec: SemVerCodec()); - final _SettingsCodec? _codecOverride; + final ValueCodec? _codecOverride; - const SettingsKey({_SettingsCodec? codec}) : _codecOverride = codec; + const SettingsKey({ValueCodec? codec}) : _codecOverride = codec; - _SettingsCodec get _codec => _codecOverride ?? _SettingsCodec.forType(T); + ValueCodec get _codec => _codecOverride ?? ValueCodec.forType(T); String encode(T value) => _codec.encode(value); T decode(String raw) => _codec.decode(raw); } - -sealed class _SettingsCodec { - const _SettingsCodec(); - - String encode(T value); - T decode(String raw); - - static final Map> _primitives = { - ..._register(_PrimitiveCodec.integer), - ..._register(_PrimitiveCodec.real), - ..._register(_PrimitiveCodec.boolean), - ..._register(_PrimitiveCodec.string), - ..._register(const _DateTimeCodec()), - }; - - static Map> _register(_SettingsCodec codec) => { - T: codec, - // Reifies the nullable type T so it can be used as a key in the _primitives map - _typeOf(): codec, - }; - - static Type _typeOf() => T; - - static _SettingsCodec forType(Type runtimeType) { - final codec = _primitives[runtimeType]; - if (codec == null) { - throw StateError('No primitive codec for $runtimeType. Provide an explicit codec when defining the SettingsKey.'); - } - return codec as _SettingsCodec; - } -} - -final class _EnumCodec extends _SettingsCodec { - final List values; - - const _EnumCodec(this.values); - - @override - String encode(T value) => value.name; - - @override - T decode(String raw) => values.firstWhere((v) => v.name == raw); -} - -final class _DateTimeCodec extends _SettingsCodec { - const _DateTimeCodec(); - - @override - String encode(DateTime value) => value.toIso8601String(); - - @override - DateTime decode(String raw) => DateTime.parse(raw); -} - -final class _SemVerCodec extends _SettingsCodec { - const _SemVerCodec(); - - @override - String encode(SemVer value) => value.toString(); - - @override - SemVer decode(String raw) => SemVer.fromString(raw); -} - -final class _MapCodec extends _SettingsCodec> { - final _SettingsCodec _keyCodec; - final _SettingsCodec _valueCodec; - - const _MapCodec(this._keyCodec, this._valueCodec); - - @override - String encode(Map value) { - final entries = {}; - value.forEach((k, v) => entries[_keyCodec.encode(k)] = _valueCodec.encode(v)); - return jsonEncode(entries); - } - - @override - Map decode(String raw) { - try { - final decoded = jsonDecode(raw); - if (decoded is! Map) { - return {}; - } - final result = {}; - for (final entry in decoded.entries) { - final rawKey = entry.key; - final rawValue = entry.value; - if (rawKey is! String || rawValue is! String) { - return {}; - } - final k = _keyCodec.decode(rawKey); - final v = _valueCodec.decode(rawValue); - result[k] = v; - } - return result; - } on FormatException { - return {}; - } - } -} - -final class _ListCodec extends _SettingsCodec> { - final _SettingsCodec _elementCodec; - - const _ListCodec(this._elementCodec); - - @override - String encode(List value) => jsonEncode(value.map(_elementCodec.encode).toList()); - - @override - List decode(String raw) { - try { - final decoded = jsonDecode(raw); - if (decoded is! List) { - return []; - } - final result = []; - for (final item in decoded) { - if (item is! String) { - return []; - } - final element = _elementCodec.decode(item); - result.add(element); - } - return result; - } on FormatException { - return []; - } - } -} - -final class _PrimitiveCodec extends _SettingsCodec { - 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.parse); - static const real = _PrimitiveCodec._(double.parse); - static const boolean = _PrimitiveCodec._(bool.parse); - static const string = _PrimitiveCodec._(_identity); - - static String _identity(String s) => s; -} diff --git a/mobile/lib/domain/models/value_codec.dart b/mobile/lib/domain/models/value_codec.dart new file mode 100644 index 0000000000..814ef1b926 --- /dev/null +++ b/mobile/lib/domain/models/value_codec.dart @@ -0,0 +1,153 @@ +import 'dart:convert'; + +import 'package:immich_mobile/utils/semver.dart'; + +sealed class ValueCodec { + const ValueCodec(); + + String encode(T value); + T decode(String raw); + + static final Map> _primitives = { + ..._register(PrimitiveCodec.integer), + ..._register(PrimitiveCodec.real), + ..._register(PrimitiveCodec.boolean), + ..._register(PrimitiveCodec.string), + ..._register(const DateTimeCodec()), + }; + + static Map> _register(ValueCodec codec) => { + T: codec, + // Reifies the nullable type T so it can be used as a key in the _primitives map + _typeOf(): codec, + }; + + static Type _typeOf() => T; + + static ValueCodec forType(Type runtimeType) { + final codec = _primitives[runtimeType]; + if (codec == null) { + throw StateError('No primitive codec for $runtimeType. Provide an explicit codec when defining the key.'); + } + return codec as ValueCodec; + } +} + +final class EnumCodec extends ValueCodec { + final List values; + + const EnumCodec(this.values); + + @override + String encode(T value) => value.name; + + @override + T decode(String raw) => values.firstWhere((v) => v.name == raw); +} + +final class DateTimeCodec extends ValueCodec { + const DateTimeCodec(); + + @override + String encode(DateTime value) => value.toIso8601String(); + + @override + DateTime decode(String raw) => DateTime.parse(raw); +} + +final class SemVerCodec extends ValueCodec { + const SemVerCodec(); + + @override + String encode(SemVer value) => value.toString(); + + @override + SemVer decode(String raw) => SemVer.fromString(raw); +} + +final class MapCodec extends ValueCodec> { + final ValueCodec _keyCodec; + final ValueCodec _valueCodec; + + const MapCodec(this._keyCodec, this._valueCodec); + + @override + String encode(Map value) { + final entries = {}; + value.forEach((k, v) => entries[_keyCodec.encode(k)] = _valueCodec.encode(v)); + return jsonEncode(entries); + } + + @override + Map decode(String raw) { + try { + final decoded = jsonDecode(raw); + if (decoded is! Map) { + return {}; + } + final result = {}; + for (final entry in decoded.entries) { + final rawKey = entry.key; + final rawValue = entry.value; + if (rawKey is! String || rawValue is! String) { + continue; + } + final k = _keyCodec.decode(rawKey); + final v = _valueCodec.decode(rawValue); + result[k] = v; + } + return result; + } on FormatException { + return {}; + } + } +} + +final class ListCodec extends ValueCodec> { + final ValueCodec _elementCodec; + + const ListCodec(this._elementCodec); + + @override + String encode(List value) => jsonEncode(value.map(_elementCodec.encode).toList()); + + @override + List decode(String raw) { + try { + final decoded = jsonDecode(raw); + if (decoded is! List) { + return []; + } + final result = []; + for (final item in decoded) { + if (item is! String) { + return []; + } + final element = _elementCodec.decode(item); + result.add(element); + } + return result; + } on FormatException { + return []; + } + } +} + +final class PrimitiveCodec extends ValueCodec { + 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.parse); + static const real = PrimitiveCodec._(double.parse); + static const boolean = PrimitiveCodec._(bool.parse); + static const string = PrimitiveCodec._(_identity); + + static String _identity(String s) => s; +} diff --git a/mobile/lib/infrastructure/repositories/cached_key_value_repository.dart b/mobile/lib/infrastructure/repositories/cached_key_value_repository.dart new file mode 100644 index 0000000000..afeb31fa27 --- /dev/null +++ b/mobile/lib/infrastructure/repositories/cached_key_value_repository.dart @@ -0,0 +1,41 @@ +import 'package:collection/collection.dart'; +import 'package:drift/drift.dart'; +// ignore: depend_on_referenced_packages +import 'package:meta/meta.dart'; + +abstract class CachedKeyValueRepository { + CachedKeyValueRepository(this._snapshot); + + S _snapshot; + S get snapshot => _snapshot; + @protected + set snapshot(S value) => _snapshot = value; + + List get keys; + + Object decodeValue(K key, String raw); + + S buildSnapshot(Map overrides); + + Selectable<({String key, String? value})> selectable(); + + Future refresh() async => _snapshot = _build(await selectable().get()); + + Stream watchSnapshot() => selectable().watch().map((rows) => _snapshot = _build(rows)); + + S _build(List<({String key, String? value})> rows) => buildSnapshot( + rows.fold({}, (overrides, row) { + final key = keys.firstWhereOrNull((key) => key.name == row.key); + if (key == null) { + return overrides; + } + + Object? decodedValue; + if (row.value != null) { + decodedValue = decodeValue(key, row.value!); + } + + return {...overrides, key: decodedValue}; + }), + ); +} diff --git a/mobile/lib/infrastructure/repositories/settings.repository.dart b/mobile/lib/infrastructure/repositories/settings.repository.dart index c974963f6a..7063779336 100644 --- a/mobile/lib/infrastructure/repositories/settings.repository.dart +++ b/mobile/lib/infrastructure/repositories/settings.repository.dart @@ -1,13 +1,14 @@ -import 'package:collection/collection.dart'; +import 'package:drift/drift.dart'; import 'package:immich_mobile/domain/models/config/app_config.dart'; import 'package:immich_mobile/domain/models/settings_key.dart'; import 'package:immich_mobile/infrastructure/entities/settings.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/repositories/cached_key_value_repository.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; -class SettingsRepository extends DriftDatabaseRepository { +class SettingsRepository extends CachedKeyValueRepository { final Drift _db; - SettingsRepository._(this._db) : super(_db); + SettingsRepository._(this._db) : super(const .new()); static SettingsRepository? _instance; @@ -19,9 +20,6 @@ class SettingsRepository extends DriftDatabaseRepository { return instance; } - AppConfig _appConfig = const .new(); - AppConfig get appConfig => _appConfig; - static Future ensureInitialized(Drift db) async { if (_instance == null) { final instance = SettingsRepository._(db); @@ -31,7 +29,20 @@ class SettingsRepository extends DriftDatabaseRepository { return _instance!; } - Future refresh() async => _applyOverrides(await _db.select(_db.settingsEntity).get()); + @override + List get keys => SettingsKey.values; + + @override + Object decodeValue(SettingsKey key, String raw) => key.decode(raw); + + @override + AppConfig buildSnapshot(Map overrides) => AppConfig.fromEntries(overrides); + + @override + Selectable<({String key, String? value})> selectable() => + _db.select(_db.settingsEntity).map((row) => (key: row.key, value: row.value)); + + AppConfig get appConfig => snapshot; Future clear(Iterable keys) async { if (keys.isEmpty) { @@ -41,13 +52,15 @@ class SettingsRepository extends DriftDatabaseRepository { final names = keys.map((key) => key.name).toList(); await (_db.delete(_db.settingsEntity)..where((row) => row.key.isIn(names))).go(); + var config = snapshot; for (final key in keys) { - _appConfig = _appConfig.write(key, defaultConfig.read(key)); + config = config.write(key, defaultConfig.read(key)); } + snapshot = config; } Future write(SettingsKey key, U value) async { - if (value == _appConfig.read(key)) { + if (value == snapshot.read(key)) { return; } @@ -65,29 +78,8 @@ class SettingsRepository extends DriftDatabaseRepository { .insertOnConflictUpdate( SettingsEntityCompanion.insert(key: key.name, value: .new(resolvedValue), updatedAt: .new(DateTime.now())), ); - _appConfig = _appConfig.write(key, value); + snapshot = snapshot.write(key, value); } - Stream watchConfig() => _db.select(_db.settingsEntity).watch().map((rows) { - _applyOverrides(rows); - return _appConfig; - }); - - void _applyOverrides(List rows) { - _appConfig = AppConfig.fromEntries( - rows.fold({}, (overrides, row) { - final metadataKey = SettingsKey.values.firstWhereOrNull((key) => key.name == row.key); - if (metadataKey == null) { - return overrides; - } - - Object? decodedValue; - if (row.value != null) { - decodedValue = metadataKey.decode(row.value!); - } - - return {...overrides, metadataKey: decodedValue}; - }), - ); - } + Stream watchConfig() => watchSnapshot(); }