From 3b23f71a3fd507e38baca532497b240b168e473c Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Thu, 28 May 2026 01:19:25 +0530 Subject: [PATCH] refactor: cleanup metadata (#28485) jason-ify metadata Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- .../lib/domain/models/config/app_config.dart | 131 +++++++++++- .../domain/models/config/network_config.dart | 8 +- .../domain/models/config/system_config.dart | 22 -- mobile/lib/domain/models/metadata_key.dart | 189 +++++++----------- mobile/lib/domain/services/log.service.dart | 2 +- .../repositories/metadata.repository.dart | 165 +++------------ .../drift_backup_album_selection.page.dart | 4 +- .../backup/drift_backup_options.page.dart | 2 +- .../pages/common/headers_settings.page.dart | 2 +- .../lib/pages/common/splash_screen.page.dart | 4 +- mobile/lib/providers/auth.provider.dart | 6 +- .../infrastructure/metadata.provider.dart | 10 +- mobile/lib/repositories/auth.repository.dart | 18 +- mobile/lib/services/api.service.dart | 6 +- mobile/lib/utils/migration.dart | 27 ++- .../widgets/settings/advanced_settings.dart | 2 +- .../external_network_preference.dart | 2 +- .../networking_settings.dart | 2 +- .../domain/services/log_service_test.dart | 6 +- .../metadata_repository_test.dart | 42 ++-- .../metadata_repository_test.dart | 28 +-- 21 files changed, 303 insertions(+), 375 deletions(-) delete mode 100644 mobile/lib/domain/models/config/system_config.dart diff --git a/mobile/lib/domain/models/config/app_config.dart b/mobile/lib/domain/models/config/app_config.dart index 1baa368df4..dc1bf96679 100644 --- a/mobile/lib/domain/models/config/app_config.dart +++ b/mobile/lib/domain/models/config/app_config.dart @@ -1,14 +1,25 @@ +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/album_config.dart'; import 'package:immich_mobile/domain/models/config/backup_config.dart'; import 'package:immich_mobile/domain/models/config/cleanup_config.dart'; import 'package:immich_mobile/domain/models/config/image_config.dart'; import 'package:immich_mobile/domain/models/config/map_config.dart'; +import 'package:immich_mobile/domain/models/config/network_config.dart'; import 'package:immich_mobile/domain/models/config/slideshow_config.dart'; import 'package:immich_mobile/domain/models/config/theme_config.dart'; import 'package:immich_mobile/domain/models/config/timeline_config.dart'; import 'package:immich_mobile/domain/models/config/viewer_config.dart'; +import 'package:immich_mobile/domain/models/log.model.dart'; +import 'package:immich_mobile/domain/models/metadata_key.dart'; +import 'package:immich_mobile/domain/models/timeline.model.dart'; +import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; + +const defaultConfig = AppConfig(); class AppConfig { + final LogLevel logLevel; final ThemeConfig theme; final CleanupConfig cleanup; final MapConfig map; @@ -18,8 +29,10 @@ class AppConfig { final SlideshowConfig slideshow; final AlbumConfig album; final BackupConfig backup; + final NetworkConfig network; const AppConfig({ + this.logLevel = .info, this.theme = const .new(), this.cleanup = const .new(), this.map = const .new(), @@ -29,9 +42,11 @@ class AppConfig { this.slideshow = const .new(), this.album = const .new(), this.backup = const .new(), + this.network = const .new(), }); AppConfig copyWith({ + LogLevel? logLevel, ThemeConfig? theme, CleanupConfig? cleanup, MapConfig? map, @@ -41,7 +56,9 @@ class AppConfig { SlideshowConfig? slideshow, AlbumConfig? album, BackupConfig? backup, + NetworkConfig? network, }) => .new( + logLevel: logLevel ?? this.logLevel, theme: theme ?? this.theme, cleanup: cleanup ?? this.cleanup, map: map ?? this.map, @@ -51,12 +68,14 @@ class AppConfig { slideshow: slideshow ?? this.slideshow, album: album ?? this.album, backup: backup ?? this.backup, + network: network ?? this.network, ); @override bool operator ==(Object other) => identical(this, other) || (other is AppConfig && + other.logLevel == logLevel && other.theme == theme && other.cleanup == cleanup && other.map == map && @@ -65,12 +84,118 @@ class AppConfig { other.viewer == viewer && other.slideshow == slideshow && other.album == album && - other.backup == backup); + other.backup == backup && + other.network == network); @override - int get hashCode => Object.hash(theme, cleanup, map, timeline, image, viewer, slideshow, album, backup); + int get hashCode => + Object.hash(logLevel, theme, cleanup, map, timeline, image, viewer, slideshow, album, backup, network); @override String toString() => - 'AppConfig(theme: $theme, cleanup: $cleanup, map: $map, timeline: $timeline, image: $image, viewer: $viewer, slideshow: $slideshow, album: $album, backup: $backup)'; + 'AppConfig(logLevel: $logLevel, theme: $theme, cleanup: $cleanup, map: $map, timeline: $timeline, image: $image, viewer: $viewer, slideshow: $slideshow, album: $album, backup: $backup, network: $network)'; + + T read(MetadataKey key) => + (switch (key) { + .logLevel => logLevel, + .themePrimaryColor => theme.primaryColor, + .themeMode => theme.mode, + .themeDynamic => theme.dynamicTheme, + .themeColorfulInterface => theme.colorfulInterface, + .imagePreferRemote => image.preferRemote, + .imageLoadOriginal => image.loadOriginal, + .viewerLoopVideo => viewer.loopVideo, + .viewerLoadOriginalVideo => viewer.loadOriginalVideo, + .viewerAutoPlayVideo => viewer.autoPlayVideo, + .viewerTapToNavigate => viewer.tapToNavigate, + .networkAutoEndpointSwitching => network.autoEndpointSwitching, + .networkPreferredWifiName => network.preferredWifiName, + .networkLocalEndpoint => network.localEndpoint, + .networkExternalEndpointList => network.externalEndpointList, + .networkCustomHeaders => network.customHeaders, + .albumSortMode => album.sortMode, + .albumIsReverse => album.isReverse, + .albumIsGrid => album.isGrid, + .backupEnabled => backup.enabled, + .backupUseCellularForVideos => backup.useCellularForVideos, + .backupUseCellularForPhotos => backup.useCellularForPhotos, + .backupRequireCharging => backup.requireCharging, + .backupTriggerDelay => backup.triggerDelay, + .backupSyncAlbums => backup.syncAlbums, + .timelineTilesPerRow => timeline.tilesPerRow, + .timelineGroupAssetsBy => timeline.groupAssetsBy, + .timelineStorageIndicator => timeline.storageIndicator, + .mapShowFavoriteOnly => map.favoritesOnly, + .mapRelativeDate => map.relativeDays, + .mapIncludeArchived => map.includeArchived, + .mapThemeMode => map.themeMode, + .mapWithPartners => map.withPartners, + .cleanupKeepFavorites => cleanup.keepFavorites, + .cleanupKeepMediaType => cleanup.keepMediaType, + .cleanupKeepAlbumIds => cleanup.keepAlbumIds, + .cleanupCutoffDaysAgo => cleanup.cutoffDaysAgo, + .cleanupDefaultsInitialized => cleanup.defaultsInitialized, + .slideshowTransition => slideshow.transition, + .slideshowRepeat => slideshow.repeat, + .slideshowDuration => slideshow.duration, + .slideshowLook => slideshow.look, + .slideshowDirection => slideshow.direction, + }) + as T; + + factory AppConfig.fromEntries(Map, Object> entries) { + var config = const AppConfig(); + for (final MapEntry(key: key, value: value) in entries.entries) { + config = config.write(key, value); + } + return config; + } + + AppConfig write(MetadataKey key, T value) { + return switch (key) { + .logLevel => copyWith(logLevel: value as LogLevel), + .themePrimaryColor => copyWith(theme: theme.copyWith(primaryColor: value as ImmichColorPreset)), + .themeMode => copyWith(theme: theme.copyWith(mode: value as ThemeMode)), + .themeDynamic => copyWith(theme: theme.copyWith(dynamicTheme: value as bool)), + .themeColorfulInterface => copyWith(theme: theme.copyWith(colorfulInterface: value as bool)), + .imagePreferRemote => copyWith(image: image.copyWith(preferRemote: value as bool)), + .imageLoadOriginal => copyWith(image: image.copyWith(loadOriginal: value as bool)), + .viewerLoopVideo => copyWith(viewer: viewer.copyWith(loopVideo: value as bool)), + .viewerLoadOriginalVideo => copyWith(viewer: viewer.copyWith(loadOriginalVideo: value as bool)), + .viewerAutoPlayVideo => copyWith(viewer: viewer.copyWith(autoPlayVideo: value as bool)), + .viewerTapToNavigate => copyWith(viewer: viewer.copyWith(tapToNavigate: value as bool)), + .networkAutoEndpointSwitching => copyWith(network: network.copyWith(autoEndpointSwitching: value as bool)), + .networkPreferredWifiName => copyWith(network: network.copyWith(preferredWifiName: (value as String))), + .networkLocalEndpoint => copyWith(network: network.copyWith(localEndpoint: (value as String))), + .networkExternalEndpointList => copyWith(network: network.copyWith(externalEndpointList: value as List)), + .networkCustomHeaders => copyWith(network: network.copyWith(customHeaders: value as Map)), + .albumSortMode => copyWith(album: album.copyWith(sortMode: value as AlbumSortMode)), + .albumIsReverse => copyWith(album: album.copyWith(isReverse: value as bool)), + .albumIsGrid => copyWith(album: album.copyWith(isGrid: value as bool)), + .backupEnabled => copyWith(backup: backup.copyWith(enabled: value as bool)), + .backupUseCellularForVideos => copyWith(backup: backup.copyWith(useCellularForVideos: value as bool)), + .backupUseCellularForPhotos => copyWith(backup: backup.copyWith(useCellularForPhotos: value as bool)), + .backupRequireCharging => copyWith(backup: backup.copyWith(requireCharging: value as bool)), + .backupTriggerDelay => copyWith(backup: backup.copyWith(triggerDelay: value as int)), + .backupSyncAlbums => copyWith(backup: backup.copyWith(syncAlbums: value as bool)), + .timelineTilesPerRow => copyWith(timeline: timeline.copyWith(tilesPerRow: value as int)), + .timelineGroupAssetsBy => copyWith(timeline: timeline.copyWith(groupAssetsBy: value as GroupAssetsBy)), + .timelineStorageIndicator => copyWith(timeline: timeline.copyWith(storageIndicator: value as bool)), + .mapShowFavoriteOnly => copyWith(map: map.copyWith(favoritesOnly: value as bool)), + .mapRelativeDate => copyWith(map: map.copyWith(relativeDays: value as int)), + .mapIncludeArchived => copyWith(map: map.copyWith(includeArchived: value as bool)), + .mapThemeMode => copyWith(map: map.copyWith(themeMode: value as ThemeMode)), + .mapWithPartners => copyWith(map: map.copyWith(withPartners: value as bool)), + .cleanupKeepFavorites => copyWith(cleanup: cleanup.copyWith(keepFavorites: value as bool)), + .cleanupKeepMediaType => copyWith(cleanup: cleanup.copyWith(keepMediaType: value as AssetKeepType)), + .cleanupKeepAlbumIds => copyWith(cleanup: cleanup.copyWith(keepAlbumIds: value as List)), + .cleanupCutoffDaysAgo => copyWith(cleanup: cleanup.copyWith(cutoffDaysAgo: value as int)), + .cleanupDefaultsInitialized => copyWith(cleanup: cleanup.copyWith(defaultsInitialized: value as bool)), + .slideshowTransition => copyWith(slideshow: slideshow.copyWith(transition: value as bool)), + .slideshowRepeat => copyWith(slideshow: slideshow.copyWith(repeat: value as bool)), + .slideshowDuration => copyWith(slideshow: slideshow.copyWith(duration: value as int)), + .slideshowLook => copyWith(slideshow: slideshow.copyWith(look: value as SlideshowLook)), + .slideshowDirection => copyWith(slideshow: slideshow.copyWith(direction: value as SlideshowDirection)), + }; + } } diff --git a/mobile/lib/domain/models/config/network_config.dart b/mobile/lib/domain/models/config/network_config.dart index 78f0482a1a..34666d60e1 100644 --- a/mobile/lib/domain/models/config/network_config.dart +++ b/mobile/lib/domain/models/config/network_config.dart @@ -2,15 +2,15 @@ import 'package:flutter/foundation.dart'; class NetworkConfig { final bool autoEndpointSwitching; - final String? preferredWifiName; - final String? localEndpoint; + final String preferredWifiName; + final String localEndpoint; final List externalEndpointList; final Map customHeaders; const NetworkConfig({ this.autoEndpointSwitching = false, - this.preferredWifiName, - this.localEndpoint, + this.preferredWifiName = '', + this.localEndpoint = '', this.externalEndpointList = const [], this.customHeaders = const {}, }); diff --git a/mobile/lib/domain/models/config/system_config.dart b/mobile/lib/domain/models/config/system_config.dart deleted file mode 100644 index 7d8fef6dd8..0000000000 --- a/mobile/lib/domain/models/config/system_config.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:immich_mobile/domain/models/config/network_config.dart'; -import 'package:immich_mobile/domain/models/log.model.dart'; - -class SystemConfig { - final LogLevel logLevel; - final NetworkConfig network; - - const SystemConfig({this.logLevel = .info, this.network = const .new()}); - - SystemConfig copyWith({LogLevel? logLevel, NetworkConfig? network}) => - SystemConfig(logLevel: logLevel ?? this.logLevel, network: network ?? this.network); - - @override - bool operator ==(Object other) => - identical(this, other) || (other is SystemConfig && other.logLevel == logLevel && other.network == network); - - @override - int get hashCode => Object.hash(logLevel, network); - - @override - String toString() => 'SystemConfig(logLevel: $logLevel, network: $network)'; -} diff --git a/mobile/lib/domain/models/metadata_key.dart b/mobile/lib/domain/models/metadata_key.dart index 541c538169..f68af0be84 100644 --- a/mobile/lib/domain/models/metadata_key.dart +++ b/mobile/lib/domain/models/metadata_key.dart @@ -1,142 +1,105 @@ 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'); +enum MetadataScope { + user, // keys with this scope are deleted on logout + system; - final String prefix; - const MetadataDomain(this.prefix); + const MetadataScope(); } 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), + themePrimaryColor(codec: _EnumCodec(ImmichColorPreset.values)), + themeMode(codec: _EnumCodec(ThemeMode.values)), + themeDynamic(), + themeColorfulInterface(), // Image - imagePreferRemote(.appConfig, 'image.preferRemote', false), - imageLoadOriginal(.appConfig, 'image.loadOriginal', false), + imagePreferRemote(), + imageLoadOriginal(), // Viewer - viewerLoopVideo(.appConfig, 'viewer.loopVideo', true), - viewerLoadOriginalVideo(.appConfig, 'viewer.loadOriginalVideo', false), - viewerAutoPlayVideo(.appConfig, 'viewer.autoPlayVideo', true), - viewerTapToNavigate(.appConfig, 'viewer.tapToNavigate', false), + viewerLoopVideo(), + viewerLoadOriginalVideo(), + viewerAutoPlayVideo(), + viewerTapToNavigate(), // Network - networkAutoEndpointSwitching(.systemConfig, 'network.autoEndpointSwitching', false), - networkPreferredWifiName(.systemConfig, 'network.preferredWifiName', ''), - networkLocalEndpoint(.systemConfig, 'network.localEndpoint', ''), - networkExternalEndpointList>( - .systemConfig, - 'network.externalEndpointList', - [], - _ListCodec(_PrimitiveCodec.string), - ), + networkAutoEndpointSwitching(scope: .system), + networkPreferredWifiName(scope: .system), + networkLocalEndpoint(scope: .system), + networkExternalEndpointList>(scope: .system, codec: _ListCodec(_PrimitiveCodec.string)), networkCustomHeaders>( - .systemConfig, - 'network.customHeaders', - {}, - _MapCodec(_PrimitiveCodec.string, _PrimitiveCodec.string), + scope: .system, + codec: _MapCodec(_PrimitiveCodec.string, _PrimitiveCodec.string), ), // Album - albumSortMode( - .appConfig, - 'album.sortMode', - AlbumSortMode.mostRecent, - _EnumCodec(AlbumSortMode.values), - ), - albumIsReverse(.appConfig, 'album.isReverse', true), - albumIsGrid(.appConfig, 'album.isGrid', false), + albumSortMode(codec: _EnumCodec(AlbumSortMode.values)), + albumIsReverse(), + albumIsGrid(), // Backup - backupEnabled(.appConfig, 'backup.enabled', false), - backupUseCellularForVideos(.appConfig, 'backup.useCellularForVideos', false), - backupUseCellularForPhotos(.appConfig, 'backup.useCellularForPhotos', false), - backupRequireCharging(.appConfig, 'backup.requireCharging', false), - backupTriggerDelay(.appConfig, 'backup.triggerDelay', 30), - backupSyncAlbums(.appConfig, 'backup.syncAlbums', false), + backupEnabled(), + backupUseCellularForVideos(), + backupUseCellularForPhotos(), + backupRequireCharging(), + backupTriggerDelay(), + backupSyncAlbums(), // Timeline - timelineTilesPerRow(.appConfig, 'timeline.tilesPerRow', 4), - timelineGroupAssetsBy( - .appConfig, - 'timeline.groupAssetsBy', - GroupAssetsBy.day, - _EnumCodec(GroupAssetsBy.values), - ), - timelineStorageIndicator(.appConfig, 'timeline.storageIndicator', true), + timelineTilesPerRow(), + timelineGroupAssetsBy(codec: _EnumCodec(GroupAssetsBy.values)), + timelineStorageIndicator(), // Log - logLevel(.systemConfig, 'log.level', .info, _EnumCodec(LogLevel.values)), + logLevel(scope: .system, codec: _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), + mapShowFavoriteOnly(), + mapRelativeDate(), + mapIncludeArchived(), + mapThemeMode(codec: _EnumCodec(ThemeMode.values)), + mapWithPartners(), // 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), + cleanupKeepFavorites(), + cleanupKeepMediaType(codec: _EnumCodec(AssetKeepType.values)), + cleanupKeepAlbumIds>(codec: _ListCodec(_PrimitiveCodec.string)), + cleanupCutoffDaysAgo(), + cleanupDefaultsInitialized(), // Slideshow - slideshowTransition(.appConfig, 'slideshow.transition', true), - slideshowRepeat(.appConfig, 'slideshow.repeat', true), - slideshowDuration(.appConfig, 'slideshow.duration', 5), - slideshowLook(.appConfig, 'slideshow.look', SlideshowLook.contain, _EnumCodec(SlideshowLook.values)), - slideshowDirection( - .appConfig, - 'slideshow.direction', - SlideshowDirection.forward, - _EnumCodec(SlideshowDirection.values), - ); + slideshowTransition(), + slideshowRepeat(), + slideshowDuration(), + slideshowLook(codec: _EnumCodec(SlideshowLook.values)), + slideshowDirection(codec: _EnumCodec(SlideshowDirection.values)); - final MetadataDomain domain; - final String name; - final T defaultValue; + final MetadataScope scope; final _MetadataCodec? _codecOverride; - const MetadataKey(this.domain, this.name, this.defaultValue, [this._codecOverride]); + const MetadataKey({this.scope = .user, _MetadataCodec? codec}) : _codecOverride = codec; - String get key => '${domain.prefix}.$name'; - - _MetadataCodec get _codec => _codecOverride ?? _MetadataCodec.forPrimitive(defaultValue); + _MetadataCodec get _codec => _codecOverride ?? _MetadataCodec.forType(T); 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}; + T decode(String raw) => _codec.decode(raw); } sealed class _MetadataCodec { const _MetadataCodec(); String encode(T value); - T? decode(String raw); + T decode(String raw); static const Map> _primitives = { int: _PrimitiveCodec.integer, @@ -146,12 +109,10 @@ sealed class _MetadataCodec { DateTime: _DateTimeCodec(), }; - static _MetadataCodec forPrimitive(T sample) { - final codec = _primitives[sample.runtimeType]; + static _MetadataCodec forType(Type runtimeType) { + final codec = _primitives[runtimeType]; if (codec == null) { - throw StateError( - 'No primitive codec for ${sample.runtimeType}. Provide an explicit codec when defining the MetadataKey.', - ); + throw StateError('No primitive codec for $runtimeType. Provide an explicit codec when defining the MetadataKey.'); } return codec as _MetadataCodec; } @@ -166,7 +127,7 @@ final class _EnumCodec extends _MetadataCodec { String encode(T value) => value.name; @override - T? decode(String raw) => values.firstWhereOrNull((v) => v.name == raw); + T decode(String raw) => values.firstWhere((v) => v.name == raw); } final class _DateTimeCodec extends _MetadataCodec { @@ -176,7 +137,7 @@ final class _DateTimeCodec extends _MetadataCodec { String encode(DateTime value) => value.toIso8601String(); @override - DateTime? decode(String raw) => DateTime.tryParse(raw); + DateTime decode(String raw) => DateTime.parse(raw); } final class _MapCodec extends _MetadataCodec> { @@ -193,29 +154,26 @@ final class _MapCodec extends _MetadataCodec } @override - Map? decode(String raw) { + Map decode(String raw) { try { final decoded = jsonDecode(raw); if (decoded is! Map) { - return null; + 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 null; + return {}; } 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; + return {}; } } } @@ -229,32 +187,29 @@ final class _ListCodec extends _MetadataCodec> { String encode(List value) => jsonEncode(value.map(_elementCodec.encode).toList()); @override - List? decode(String raw) { + List decode(String raw) { try { final decoded = jsonDecode(raw); if (decoded is! List) { - return null; + return []; } final result = []; for (final item in decoded) { if (item is! String) { - return null; + return []; } final element = _elementCodec.decode(item); - if (element == null) { - return null; - } result.add(element); } return result; } on FormatException { - return null; + return []; } } } final class _PrimitiveCodec extends _MetadataCodec { - final T? Function(String) _parse; + final T Function(String) _parse; const _PrimitiveCodec._(this._parse); @@ -262,12 +217,12 @@ final class _PrimitiveCodec extends _MetadataCodec { String encode(T value) => value.toString(); @override - T? decode(String raw) => _parse(raw); + 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 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; + static String _identity(String s) => s; } diff --git a/mobile/lib/domain/services/log.service.dart b/mobile/lib/domain/services/log.service.dart index 1235d7ac76..ac65c9bedf 100644 --- a/mobile/lib/domain/services/log.service.dart +++ b/mobile/lib/domain/services/log.service.dart @@ -56,7 +56,7 @@ class LogService { }) async { final instance = LogService._(logRepository, metadataRepository, shouldBuffer); await logRepository.truncate(limit: kLogTruncateLimit); - final level = instance._metadataRepository.systemConfig.logLevel; + final level = instance._metadataRepository.appConfig.logLevel; Logger.root.level = Level.LEVELS.elementAtOrNull(level.index) ?? Level.INFO; return instance; } diff --git a/mobile/lib/infrastructure/repositories/metadata.repository.dart b/mobile/lib/infrastructure/repositories/metadata.repository.dart index fa1d275026..d43f67fe87 100644 --- a/mobile/lib/infrastructure/repositories/metadata.repository.dart +++ b/mobile/lib/infrastructure/repositories/metadata.repository.dart @@ -1,14 +1,12 @@ +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/config/system_config.dart'; import 'package:immich_mobile/domain/models/metadata_key.dart'; -import 'package:immich_mobile/extensions/string_extensions.dart'; import 'package:immich_mobile/infrastructure/entities/metadata.entity.drift.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; class MetadataRepository extends DriftDatabaseRepository { final Drift _db; - final Map _cache = {}; MetadataRepository._(this._db) : super(_db); @@ -25,153 +23,50 @@ class MetadataRepository extends DriftDatabaseRepository { AppConfig _appConfig = const .new(); AppConfig get appConfig => _appConfig; - SystemConfig _systemConfig = const .new(); - SystemConfig get systemConfig => _systemConfig; - static Future ensureInitialized(Drift db) async { if (_instance == null) { final instance = MetadataRepository._(db); - await instance._hydrate(); + await instance.refresh(); _instance = instance; } return _instance!; } - static Future refresh() async { - instance._cache.clear(); - instance._appConfig = const .new(); - instance._systemConfig = const .new(); - await instance._hydrate(); - } - - Future _hydrate() async => _hydrateCache(await _db.select(_db.metadataEntity).get()); - - T _read(MetadataKey key) => (_cache[key] as T?) ?? key.defaultValue; + Future refresh() async => _applyOverrides(await _db.select(_db.metadataEntity).get()); Future write(MetadataKey key, U value) async { - if (_read(key) == value) { + if (value == _appConfig.read(key)) { return; } - await _db - .into(_db.metadataEntity) - .insertOnConflictUpdate( - MetadataEntityCompanion.insert(key: key.key, value: key.encode(value), updatedAt: Value(DateTime.now())), - ); - _updateCache(key, value); - } - - Future delete(MetadataKey key) async { - await (_db.delete(_db.metadataEntity)..where((t) => t.key.equals(key.key))).go(); - _updateCache(key, key.defaultValue); - } - - Stream watchAppConfig() => _watchDomain(.appConfig).distinct(); - - Stream watchSystemConfig() => _watchDomain(.systemConfig).distinct(); - - Stream _watchDomain(MetadataDomain domain) { - final query = _db.select(_db.metadataEntity)..where((t) => t.key.like('${domain.prefix}.%')); - return query.watch().map((rows) { - _hydrateCache(rows); - return domain.config(this); - }); - } - - void _hydrateCache(List rows) { - final keyMap = MetadataKey.asKeyMap(); - for (final row in rows) { - final key = keyMap[row.key]; - if (key == null) { - continue; - } - _updateCache(key, key.decode(row.value)); + if (value == defaultConfig.read(key)) { + await (_db.delete(_db.metadataEntity)..where((t) => t.key.equals(key.name))).go(); + } else { + await _db + .into(_db.metadataEntity) + .insertOnConflictUpdate( + MetadataEntityCompanion.insert(key: key.name, value: key.encode(value), updatedAt: Value(DateTime.now())), + ); } + + _appConfig = _appConfig.write(key, value); } - void _updateCache(MetadataKey key, T value) { - if (_cache[key] == value) { - return; - } - _cache[key] = value; - key.domain.rebuild(this); - } -} - -extension on MetadataDomain { - T config(MetadataRepository repo) => switch (this) { - .appConfig => repo._appConfig as T, - .systemConfig => repo._systemConfig as T, - }; - - void rebuild(MetadataRepository repo) { - switch (this) { - case .appConfig: - repo._appConfig = .new( - theme: .new( - mode: repo._read(.themeMode), - primaryColor: repo._read(.themePrimaryColor), - dynamicTheme: repo._read(.themeDynamic), - colorfulInterface: repo._read(.themeColorfulInterface), - ), - cleanup: .new( - keepFavorites: repo._read(.cleanupKeepFavorites), - keepMediaType: repo._read(.cleanupKeepMediaType), - keepAlbumIds: repo._read(.cleanupKeepAlbumIds), - cutoffDaysAgo: repo._read(.cleanupCutoffDaysAgo), - defaultsInitialized: repo._read(.cleanupDefaultsInitialized), - ), - map: .new( - relativeDays: repo._read(.mapRelativeDate), - favoritesOnly: repo._read(.mapShowFavoriteOnly), - includeArchived: repo._read(.mapIncludeArchived), - themeMode: repo._read(.mapThemeMode), - withPartners: repo._read(.mapWithPartners), - ), - timeline: .new( - tilesPerRow: repo._read(.timelineTilesPerRow), - groupAssetsBy: repo._read(.timelineGroupAssetsBy), - storageIndicator: repo._read(.timelineStorageIndicator), - ), - image: .new(preferRemote: repo._read(.imagePreferRemote), loadOriginal: repo._read(.imageLoadOriginal)), - viewer: .new( - loopVideo: repo._read(.viewerLoopVideo), - loadOriginalVideo: repo._read(.viewerLoadOriginalVideo), - autoPlayVideo: repo._read(.viewerAutoPlayVideo), - tapToNavigate: repo._read(.viewerTapToNavigate), - ), - slideshow: .new( - transition: repo._read(.slideshowTransition), - repeat: repo._read(.slideshowRepeat), - duration: repo._read(.slideshowDuration), - look: repo._read(.slideshowLook), - direction: repo._read(.slideshowDirection), - ), - album: .new( - sortMode: repo._read(.albumSortMode), - isReverse: repo._read(.albumIsReverse), - isGrid: repo._read(.albumIsGrid), - ), - backup: .new( - enabled: repo._read(.backupEnabled), - useCellularForVideos: repo._read(.backupUseCellularForVideos), - useCellularForPhotos: repo._read(.backupUseCellularForPhotos), - requireCharging: repo._read(.backupRequireCharging), - triggerDelay: repo._read(.backupTriggerDelay), - syncAlbums: repo._read(.backupSyncAlbums), - ), - ); - case .systemConfig: - repo._systemConfig = .new( - logLevel: repo._read(.logLevel), - network: .new( - autoEndpointSwitching: repo._read(.networkAutoEndpointSwitching), - preferredWifiName: repo._read(.networkPreferredWifiName).nullIfEmpty, - localEndpoint: repo._read(.networkLocalEndpoint).nullIfEmpty, - externalEndpointList: repo._read(.networkExternalEndpointList), - customHeaders: repo._read(.networkCustomHeaders), - ), - ); - } + Stream watchConfig() => _db.select(_db.metadataEntity).watch().map((rows) { + _applyOverrides(rows); + return _appConfig; + }); + + void _applyOverrides(List rows) { + _appConfig = AppConfig.fromEntries( + rows.fold({}, (overrides, row) { + final metadataKey = MetadataKey.values.firstWhereOrNull((key) => key.name == row.key); + if (metadataKey == null) { + return overrides; + } + + return {...overrides, metadataKey: metadataKey.decode(row.value)}; + }), + ); } } diff --git a/mobile/lib/pages/backup/drift_backup_album_selection.page.dart b/mobile/lib/pages/backup/drift_backup_album_selection.page.dart index de37437326..c9398febc6 100644 --- a/mobile/lib/pages/backup/drift_backup_album_selection.page.dart +++ b/mobile/lib/pages/backup/drift_backup_album_selection.page.dart @@ -43,7 +43,7 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState p.totalCount)); @@ -55,7 +55,7 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState a.backupSelection == BackupSelection.selected) diff --git a/mobile/lib/pages/backup/drift_backup_options.page.dart b/mobile/lib/pages/backup/drift_backup_options.page.dart index 4e8a185955..ee26d0bf87 100644 --- a/mobile/lib/pages/backup/drift_backup_options.page.dart +++ b/mobile/lib/pages/backup/drift_backup_options.page.dart @@ -19,7 +19,7 @@ class DriftBackupOptionsPage extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { bool hasPopped = false; - final previousBackup = ref.read(metadataProvider).appConfig.backup; + final previousBackup = ref.read(appConfigProvider).backup; final previousCellularForVideos = previousBackup.useCellularForVideos; final previousCellularForPhotos = previousBackup.useCellularForPhotos; return PopScope( diff --git a/mobile/lib/pages/common/headers_settings.page.dart b/mobile/lib/pages/common/headers_settings.page.dart index e599286dcf..d342add5af 100644 --- a/mobile/lib/pages/common/headers_settings.page.dart +++ b/mobile/lib/pages/common/headers_settings.page.dart @@ -22,7 +22,7 @@ class HeaderSettingsPage extends HookConsumerWidget { final headers = useState>([]); final setInitialHeaders = useState(false); - final storedHeaders = ref.read(metadataProvider).systemConfig.network.customHeaders; + final storedHeaders = ref.read(metadataProvider).appConfig.network.customHeaders; if (!setInitialHeaders.value) { storedHeaders.forEach((k, v) { final header = SettingsHeader(); diff --git a/mobile/lib/pages/common/splash_screen.page.dart b/mobile/lib/pages/common/splash_screen.page.dart index 7b49d98307..36fd9f5bf0 100644 --- a/mobile/lib/pages/common/splash_screen.page.dart +++ b/mobile/lib/pages/common/splash_screen.page.dart @@ -7,7 +7,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/locales.dart'; -import 'package:immich_mobile/domain/models/metadata_key.dart'; +import 'package:immich_mobile/domain/models/config/app_config.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/generated/codegen_loader.g.dart'; @@ -36,7 +36,7 @@ class BootstrapErrorWidget extends StatelessWidget { @override Widget build(BuildContext _) { - final immichTheme = MetadataKey.themePrimaryColor.defaultValue.themeOfPreset; + final immichTheme = defaultConfig.theme.primaryColor.themeOfPreset; return EasyLocalization( supportedLocales: locales.values.toList(), diff --git a/mobile/lib/providers/auth.provider.dart b/mobile/lib/providers/auth.provider.dart index ae97909349..0bd5783fee 100644 --- a/mobile/lib/providers/auth.provider.dart +++ b/mobile/lib/providers/auth.provider.dart @@ -130,7 +130,7 @@ class AuthNotifier extends StateNotifier { await _apiService.updateHeaders(); final serverEndpoint = Store.get(StoreKey.serverEndpoint); - final headerMap = _ref.read(metadataProvider).systemConfig.network.customHeaders; + final headerMap = _ref.read(metadataProvider).appConfig.network.customHeaders; final customHeaders = headerMap.isEmpty ? null : jsonEncode(headerMap); await _widgetService.writeCredentials(serverEndpoint, accessToken, customHeaders); @@ -187,11 +187,11 @@ class AuthNotifier extends StateNotifier { } String? getSavedWifiName() { - return _ref.read(metadataProvider).systemConfig.network.preferredWifiName; + return _ref.read(metadataProvider).appConfig.network.preferredWifiName; } String? getSavedLocalEndpoint() { - return _ref.read(metadataProvider).systemConfig.network.localEndpoint; + return _ref.read(metadataProvider).appConfig.network.localEndpoint; } /// Returns the current server endpoint (with /api) URL from the store diff --git a/mobile/lib/providers/infrastructure/metadata.provider.dart b/mobile/lib/providers/infrastructure/metadata.provider.dart index 46ff1069f9..d9e6920d62 100644 --- a/mobile/lib/providers/infrastructure/metadata.provider.dart +++ b/mobile/lib/providers/infrastructure/metadata.provider.dart @@ -1,20 +1,12 @@ import 'package:hooks_riverpod/hooks_riverpod.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/infrastructure/repositories/metadata.repository.dart'; final metadataProvider = Provider.autoDispose((_) => MetadataRepository.instance); final appConfigProvider = Provider.autoDispose((ref) { final repo = ref.watch(metadataProvider); - final subscription = repo.watchAppConfig().listen((event) => ref.state = event); + final subscription = repo.watchConfig().listen((event) => ref.state = event); ref.onDispose(subscription.cancel); return repo.appConfig; }); - -final systemConfigProvider = Provider.autoDispose((ref) { - final repo = ref.watch(metadataProvider); - final subscription = repo.watchSystemConfig().listen((event) => ref.state = event); - ref.onDispose(subscription.cancel); - return repo.systemConfig; -}); diff --git a/mobile/lib/repositories/auth.repository.dart b/mobile/lib/repositories/auth.repository.dart index e71c752f44..5aca8e76ac 100644 --- a/mobile/lib/repositories/auth.repository.dart +++ b/mobile/lib/repositories/auth.repository.dart @@ -1,40 +1,38 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/config/app_config.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart'; import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; final authRepositoryProvider = Provider( - (ref) => AuthRepository(ref.watch(driftProvider), ref.watch(metadataProvider)), + (ref) => AuthRepository(ref.watch(driftProvider), ref.watch(appConfigProvider)), ); class AuthRepository { final Drift _drift; - final MetadataRepository _metadata; + final AppConfig _config; - const AuthRepository(this._drift, this._metadata); + const AuthRepository(this._drift, this._config); Future clearLocalData() async { await SyncStreamRepository(_drift).reset(); } bool getEndpointSwitchingFeature() { - return _metadata.systemConfig.network.autoEndpointSwitching; + return _config.network.autoEndpointSwitching; } String? getPreferredWifiName() { - return _metadata.systemConfig.network.preferredWifiName; + return _config.network.preferredWifiName; } String? getLocalEndpoint() { - return _metadata.systemConfig.network.localEndpoint; + return _config.network.localEndpoint; } List getExternalEndpointList() { - return _metadata.systemConfig.network.externalEndpointList - .map((url) => AuxilaryEndpoint(url: url, status: .valid)) - .toList(); + return _config.network.externalEndpointList.map((url) => AuxilaryEndpoint(url: url, status: .valid)).toList(); } } diff --git a/mobile/lib/services/api.service.dart b/mobile/lib/services/api.service.dart index bc36c98768..99f618b832 100644 --- a/mobile/lib/services/api.service.dart +++ b/mobile/lib/services/api.service.dart @@ -177,9 +177,9 @@ class ApiService { if (serverEndpoint != null && serverEndpoint.isNotEmpty) { urls.add(serverEndpoint); } - final network = MetadataRepository.instance.systemConfig.network; + final network = MetadataRepository.instance.appConfig.network; final localEndpoint = network.localEndpoint; - if (localEndpoint != null) { + if (localEndpoint.isNotEmpty) { urls.add(localEndpoint); } for (final url in network.externalEndpointList) { @@ -191,7 +191,7 @@ class ApiService { } static Map getRequestHeaders() { - return MetadataRepository.instance.systemConfig.network.customHeaders; + return MetadataRepository.instance.appConfig.network.customHeaders; } ApiClient get apiClient => _apiClient; diff --git a/mobile/lib/utils/migration.dart b/mobile/lib/utils/migration.dart index 41301fb227..e1f40c8751 100644 --- a/mobile/lib/utils/migration.dart +++ b/mobile/lib/utils/migration.dart @@ -1,10 +1,12 @@ import 'dart:async'; import 'dart:convert'; +import 'package:collection/collection.dart'; import 'package:drift/drift.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/log.model.dart'; import 'package:immich_mobile/domain/models/metadata_key.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; @@ -136,15 +138,11 @@ Future _migrateTo26(Drift drift) async { Future _migrateAlbumSortMode(_StoreMigrator migrator) async { final raw = await migrator.readLegacyStoreInt(StoreKey.legacySelectedAlbumSortOrder.id); - if (raw == null) { + final mode = AlbumSortMode.values.firstWhereOrNull((e) => raw != null && e.storeIndex == raw); + if (mode == null) { return; } - final mode = AlbumSortMode.values.firstWhere( - (e) => e.storeIndex == raw, - orElse: () => MetadataKey.albumSortMode.defaultValue, - ); - migrator.stage(StoreKey.legacySelectedAlbumSortOrder, MetadataKey.albumSortMode, mode); } @@ -208,7 +206,11 @@ class _StoreMigrator { return; } - final enumValue = values.elementAtOrNull(index) ?? newKey.defaultValue; + final enumValue = values.elementAtOrNull(index); + if (enumValue == null) { + return; + } + _cache[newKey] = enumValue; _migratedStoreIds.add(legacyKey.id); } @@ -223,7 +225,11 @@ class _StoreMigrator { return; } - final enumValue = values.firstWhere((e) => e.name == name, orElse: () => newKey.defaultValue); + final enumValue = values.firstWhereOrNull((e) => e.name == name); + if (enumValue == null) { + return; + } + _cache[newKey] = enumValue; _migratedStoreIds.add(legacyKey.id); } @@ -267,9 +273,12 @@ class _StoreMigrator { Future complete() async { await _db.batch((batch) { for (final entry in _cache.entries) { + if (entry.value == defaultConfig.read(entry.key)) { + continue; + } batch.insert( _db.metadataEntity, - MetadataEntityCompanion(key: Value(entry.key.key), value: Value(entry.key.encode(entry.value))), + MetadataEntityCompanion(key: Value(entry.key.name), value: Value(entry.key.encode(entry.value))), mode: InsertMode.insertOrReplace, ); } diff --git a/mobile/lib/widgets/settings/advanced_settings.dart b/mobile/lib/widgets/settings/advanced_settings.dart index 5de2570737..b4b24a43e6 100644 --- a/mobile/lib/widgets/settings/advanced_settings.dart +++ b/mobile/lib/widgets/settings/advanced_settings.dart @@ -31,7 +31,7 @@ class AdvancedSettings extends HookConsumerWidget { final manageLocalMediaAndroid = useAppSettingsState(AppSettingsEnum.manageLocalMediaAndroid); final isManageMediaSupported = useState(false); final manageMediaAndroidPermission = useState(false); - final levelId = useState(ref.read(systemConfigProvider).logLevel.index); + final levelId = useState(ref.read(appConfigProvider).logLevel.index); final preferRemote = useState(ref.read(appConfigProvider).image.preferRemote); useValueChanged( preferRemote.value, diff --git a/mobile/lib/widgets/settings/networking_settings/external_network_preference.dart b/mobile/lib/widgets/settings/networking_settings/external_network_preference.dart index bd5d05d02e..02be04ac31 100644 --- a/mobile/lib/widgets/settings/networking_settings/external_network_preference.dart +++ b/mobile/lib/widgets/settings/networking_settings/external_network_preference.dart @@ -64,7 +64,7 @@ class ExternalNetworkPreference extends HookConsumerWidget { } useEffect(() { - final urls = ref.read(metadataProvider).systemConfig.network.externalEndpointList; + final urls = ref.read(appConfigProvider).network.externalEndpointList; if (urls.isEmpty) { return null; diff --git a/mobile/lib/widgets/settings/networking_settings/networking_settings.dart b/mobile/lib/widgets/settings/networking_settings/networking_settings.dart index f232f41a5d..037d66b076 100644 --- a/mobile/lib/widgets/settings/networking_settings/networking_settings.dart +++ b/mobile/lib/widgets/settings/networking_settings/networking_settings.dart @@ -19,7 +19,7 @@ class NetworkingSettings extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final currentEndpoint = getServerUrl(); - final featureEnabled = useState(ref.read(systemConfigProvider).network.autoEndpointSwitching); + final featureEnabled = useState(ref.read(appConfigProvider).network.autoEndpointSwitching); useValueChanged(featureEnabled.value, (_, __) { ref.read(metadataProvider).write(.networkAutoEndpointSwitching, featureEnabled.value); }); diff --git a/mobile/test/domain/services/log_service_test.dart b/mobile/test/domain/services/log_service_test.dart index ee596f449e..f442b9514c 100644 --- a/mobile/test/domain/services/log_service_test.dart +++ b/mobile/test/domain/services/log_service_test.dart @@ -1,7 +1,7 @@ import 'package:collection/collection.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:immich_mobile/constants/constants.dart'; -import 'package:immich_mobile/domain/models/config/system_config.dart'; +import 'package:immich_mobile/domain/models/config/app_config.dart'; import 'package:immich_mobile/domain/models/log.model.dart'; import 'package:immich_mobile/domain/models/metadata_key.dart'; import 'package:immich_mobile/domain/services/log.service.dart'; @@ -39,7 +39,7 @@ void main() { registerFallbackValue(LogLevel.info); when(() => mockLogRepo.truncate(limit: any(named: 'limit'))).thenAnswer((_) async => {}); - when(() => mockMetadataRepository.systemConfig).thenReturn(const SystemConfig(logLevel: LogLevel.fine)); + when(() => mockMetadataRepository.appConfig).thenReturn(const AppConfig(logLevel: LogLevel.fine)); when(() => mockMetadataRepository.write(MetadataKey.logLevel, any())).thenAnswer((_) async {}); when(() => mockLogRepo.getAll()).thenAnswer((_) async => []); when(() => mockLogRepo.insert(any())).thenAnswer((_) async => true); @@ -59,7 +59,7 @@ void main() { }); test('Sets log level based on the metadata repository', () { - verify(() => mockMetadataRepository.systemConfig).called(1); + verify(() => mockMetadataRepository.appConfig).called(1); expect(Logger.root.level, Level.FINE); }); }); diff --git a/mobile/test/medium/repositories/metadata_repository_test.dart b/mobile/test/medium/repositories/metadata_repository_test.dart index 7b185f3bec..8662e8bdd0 100644 --- a/mobile/test/medium/repositories/metadata_repository_test.dart +++ b/mobile/test/medium/repositories/metadata_repository_test.dart @@ -23,7 +23,7 @@ void main() { setUp(() async { await ctx.db.delete(ctx.db.metadataEntity).go(); - await MetadataRepository.refresh(); + await MetadataRepository.instance.refresh(); }); group('defaults', () { @@ -31,8 +31,8 @@ void main() { expect(sut.appConfig.theme.mode, ThemeMode.system); }); - test('systemConfig returns key defaults when DB is empty', () { - expect(sut.systemConfig.logLevel, LogLevel.info); + test('appConfig returns key defaults when DB is empty', () { + expect(sut.appConfig.logLevel, LogLevel.info); }); }); @@ -46,16 +46,14 @@ void main() { await sut.write(.themeMode, ThemeMode.light); await sut.write(.logLevel, LogLevel.severe); expect(sut.appConfig.theme.mode, ThemeMode.light); - expect(sut.systemConfig.logLevel, LogLevel.severe); + expect(sut.appConfig.logLevel, LogLevel.severe); }); - }); - group('delete', () { test('removes the row and reverts to default', () async { await sut.write(.themeMode, ThemeMode.dark); expect(sut.appConfig.theme.mode, ThemeMode.dark); - await sut.delete(.themeMode); + await sut.write(.themeMode, ThemeMode.system); expect(sut.appConfig.theme.mode, ThemeMode.system); final rows = await ctx.db.select(ctx.db.metadataEntity).get(); @@ -63,13 +61,15 @@ void main() { }); }); - group('refresh', () { + group('delete', () {}); + + group('sync', () { test('picks up rows that were inserted directly into the DB', () async { await ctx.db .into(ctx.db.metadataEntity) .insert( MetadataEntityCompanion.insert( - key: MetadataKey.themeMode.key, + key: MetadataKey.themeMode.name, value: ThemeMode.dark.name, updatedAt: Value(DateTime.now()), ), @@ -78,7 +78,7 @@ void main() { // Cache hasn't seen this row yet — view still returns the default. expect(sut.appConfig.theme.mode, ThemeMode.system); - await MetadataRepository.refresh(); + await MetadataRepository.instance.refresh(); expect(sut.appConfig.theme.mode, ThemeMode.dark); }); @@ -88,7 +88,7 @@ void main() { await ctx.db.delete(ctx.db.metadataEntity).go(); expect(sut.appConfig.theme.mode, ThemeMode.dark); - await MetadataRepository.refresh(); + await MetadataRepository.instance.refresh(); expect(sut.appConfig.theme.mode, ThemeMode.system); }); @@ -103,32 +103,20 @@ void main() { ), ); - await MetadataRepository.refresh(); + await MetadataRepository.instance.refresh(); expect(sut.appConfig.theme.mode, ThemeMode.system); }); }); group('watch', () { test('watchAppConfig emits the new value after a write', () async { - final expectation = expectLater(sut.watchAppConfig().map((c) => c.theme.mode), emitsThrough(ThemeMode.dark)); + final expectation = expectLater(sut.watchConfig().map((c) => c.theme.mode), emitsThrough(ThemeMode.dark)); await sut.write(MetadataKey.themeMode, ThemeMode.dark); await expectation; }); - test('watchAppConfig does not emit when only system-config rows change', () async { - final emissions = []; - // skip(1) drops the on-subscribe replay so we only capture emissions caused by the write below. - final sub = sut.watchAppConfig().skip(1).listen((c) => emissions.add(c.theme.mode)); - - await sut.write(MetadataKey.logLevel, LogLevel.severe); - await pumpEventQueue(); - await sub.cancel(); - - expect(emissions, isEmpty); - }); - - test('watchSystemConfig emits the new value after a write', () async { - final expectation = expectLater(sut.watchSystemConfig().map((c) => c.logLevel), emitsThrough(LogLevel.warning)); + test('watchConfig emits the new value after a write', () async { + final expectation = expectLater(sut.watchConfig().map((c) => c.logLevel), emitsThrough(LogLevel.warning)); await sut.write(MetadataKey.logLevel, LogLevel.warning); await expectation; }); diff --git a/mobile/test/unit/repositories/metadata_repository_test.dart b/mobile/test/unit/repositories/metadata_repository_test.dart index 75b34da7cb..e51b21f238 100644 --- a/mobile/test/unit/repositories/metadata_repository_test.dart +++ b/mobile/test/unit/repositories/metadata_repository_test.dart @@ -1,28 +1,16 @@ import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/domain/models/config/app_config.dart'; import 'package:immich_mobile/domain/models/metadata_key.dart'; void main() { group('MetadataKey', () { - test('every key round-trips its default value losslessly', () { - for (final key in MetadataKey.values) { - final encoded = key.encode(key.defaultValue); + for (final key in MetadataKey.values) { + test('verify codec for $key', () { + final defaultValue = defaultConfig.read(key); + final encoded = key.encode(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) { - // String keys can decode any string. So skip them - if (key.defaultValue is String) { - continue; - } - expect( - key.decode('not a valid encoding for any key'), - key.defaultValue, - reason: 'fallback failed for ${key.name}', - ); - } - }); + expect(decoded, defaultValue, reason: 'round-trip failed for ${key.name}'); + }); + } }); }