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'; enum MetadataDomain { appConfig('config.app'), systemConfig('config.system'); final String prefix; const MetadataDomain(this.prefix); } enum MetadataKey { // Theme themePrimaryColor(.appConfig, 'theme.primaryColor', .indigo, _EnumCodec(ImmichColorPreset.values)), themeMode(.appConfig, 'theme.mode', .system, _EnumCodec(ThemeMode.values)), themeDynamic(.appConfig, 'theme.dynamic', false), themeColorfulInterface(.appConfig, 'theme.colorfulInterface', true), // Image imagePreferRemote(.appConfig, 'image.preferRemote', false), imageLoadOriginal(.appConfig, 'image.loadOriginal', false), // Viewer viewerLoopVideo(.appConfig, 'viewer.loopVideo', true), viewerLoadOriginalVideo(.appConfig, 'viewer.loadOriginalVideo', false), viewerAutoPlayVideo(.appConfig, 'viewer.autoPlayVideo', true), viewerTapToNavigate(.appConfig, 'viewer.tapToNavigate', false), // Timeline timelineTilesPerRow(.appConfig, 'timeline.tilesPerRow', 4), timelineGroupAssetsBy( .appConfig, 'timeline.groupAssetsBy', GroupAssetsBy.day, _EnumCodec(GroupAssetsBy.values), ), timelineStorageIndicator(.appConfig, 'timeline.storageIndicator', true), // Log logLevel(.systemConfig, 'log.level', .info, _EnumCodec(LogLevel.values)), // Map mapShowFavoriteOnly(.appConfig, 'map.showFavoriteOnly', false), mapRelativeDate(.appConfig, 'map.relativeDate', 0), mapIncludeArchived(.appConfig, 'map.includeArchived', false), mapThemeMode(.appConfig, 'map.themeMode', .system, _EnumCodec(ThemeMode.values)), mapWithPartners(.appConfig, 'map.withPartners', false), // Cleanup cleanupKeepFavorites(.appConfig, 'cleanup.keepFavorites', true), cleanupKeepMediaType( .appConfig, 'cleanup.keepMediaType', AssetKeepType.none, _EnumCodec(AssetKeepType.values), ), cleanupKeepAlbumIds>(.appConfig, 'cleanup.keepAlbumIds', [], _ListCodec(_PrimitiveCodec.string)), cleanupCutoffDaysAgo(.appConfig, 'cleanup.cutoffDaysAgo', -1), cleanupDefaultsInitialized(.appConfig, 'cleanup.defaultsInitialized', false); final MetadataDomain domain; final String name; final T defaultValue; final _MetadataCodec? _codecOverride; 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 _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; }