import 'dart:convert'; import 'package:collection/collection.dart'; 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/config/app_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/timeline.model.dart'; import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; enum MetadataDomain { appConfig('config.app'), systemConfig('config.system'); final String prefix; const MetadataDomain(this.prefix); } enum MetadataKey { // Theme themePrimaryColor(.appConfig, .indigo, _EnumCodec(ImmichColorPreset.values)), themeMode(.appConfig, .system, _EnumCodec(ThemeMode.values)), themeDynamic(.appConfig, false), themeColorfulInterface(.appConfig, true), // Image imagePreferRemote(.appConfig, false), imageLoadOriginal(.appConfig, false), // Viewer viewerLoopVideo(.appConfig, true), viewerLoadOriginalVideo(.appConfig, false), viewerAutoPlayVideo(.appConfig, true), viewerTapToNavigate(.appConfig, false), // Network networkAutoEndpointSwitching(.systemConfig, false), networkPreferredWifiName(.systemConfig, ''), networkLocalEndpoint(.systemConfig, ''), networkExternalEndpointList>(.systemConfig, [], _ListCodec(_PrimitiveCodec.string)), networkCustomHeaders>( .systemConfig, {}, _MapCodec(_PrimitiveCodec.string, _PrimitiveCodec.string), ), // Album albumSortMode(.appConfig, AlbumSortMode.mostRecent, _EnumCodec(AlbumSortMode.values)), albumIsReverse(.appConfig, true), albumIsGrid(.appConfig, false), // Backup backupEnabled(.appConfig, false), backupUseCellularForVideos(.appConfig, false), backupUseCellularForPhotos(.appConfig, false), backupRequireCharging(.appConfig, false), backupTriggerDelay(.appConfig, 30), backupSyncAlbums(.appConfig, false), // Timeline timelineTilesPerRow(.appConfig, 4), timelineGroupAssetsBy(.appConfig, GroupAssetsBy.day, _EnumCodec(GroupAssetsBy.values)), timelineStorageIndicator(.appConfig, true), // Log logLevel(.systemConfig, .info, _EnumCodec(LogLevel.values)), // Map mapShowFavoriteOnly(.appConfig, false), mapRelativeDate(.appConfig, 0), mapIncludeArchived(.appConfig, false), mapThemeMode(.appConfig, .system, _EnumCodec(ThemeMode.values)), mapWithPartners(.appConfig, false), // Cleanup cleanupKeepFavorites(.appConfig, true), cleanupKeepMediaType(.appConfig, AssetKeepType.none, _EnumCodec(AssetKeepType.values)), cleanupKeepAlbumIds>(.appConfig, [], _ListCodec(_PrimitiveCodec.string)), cleanupCutoffDaysAgo(.appConfig, -1), cleanupDefaultsInitialized(.appConfig, false); final MetadataDomain domain; final T defaultValue; final _MetadataCodec? _codecOverride; const MetadataKey(this.domain, 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 _MapCodec extends _MetadataCodec> { final _MetadataCodec _keyCodec; final _MetadataCodec _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 null; final result = {}; for (final entry in decoded.entries) { final rawKey = entry.key; final rawValue = entry.value; if (rawKey is! String || rawValue is! String) return null; final k = _keyCodec.decode(rawKey); final v = _valueCodec.decode(rawValue); if (k == null || v == null) return null; result[k] = v; } return result; } on FormatException { return null; } } } final class _ListCodec extends _MetadataCodec> { final _MetadataCodec _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 null; final result = []; for (final item in decoded) { if (item is! String) return null; final element = _elementCodec.decode(item); if (element == null) return null; result.add(element); } return result; } on FormatException { return null; } } } 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; }