diff --git a/mobile/lib/domain/models/settings_key.dart b/mobile/lib/domain/models/settings_key.dart index 00c5286e07..e979f07f75 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,23 +46,23 @@ 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(), @@ -79,163 +78,13 @@ enum SettingsKey { // Feature message 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..22132162f1 --- /dev/null +++ b/mobile/lib/domain/models/value_codec.dart @@ -0,0 +1,133 @@ +import 'dart:convert'; + +sealed class ValueCodec { + const ValueCodec(); + + 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 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 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) { + return {}; + } + 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; +}