From 0a4ed6fd71df412b06c980d7ef1f899b0c9dfe49 Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Wed, 13 May 2026 21:36:19 +0700 Subject: [PATCH] refactor: migrate viewer config to metadata table (#28396) * refactor: app metadata * refactor to per row store * cleanup * more test * review changes * more refactor * refactor * migrate primary color * migrate dynamic theme * migrate colorfulInterface * cleanup providers * migrate cleanup * migrate mapconfig * remove unused keys * migrate timeline config * migrate image config * migrate viewer config --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- .../lib/domain/models/config/app_config.dart | 13 +++++-- .../domain/models/config/viewer_config.dart | 37 +++++++++++++++++++ mobile/lib/domain/models/metadata_key.dart | 6 +++ mobile/lib/domain/models/setting.model.dart | 2 - mobile/lib/domain/models/store.model.dart | 10 ++--- .../repositories/metadata.repository.dart | 6 +++ .../asset_viewer/asset_page.widget.dart | 5 +-- .../asset_viewer/video_viewer.widget.dart | 12 ++---- mobile/lib/services/app_settings.service.dart | 4 -- mobile/lib/utils/migration.dart | 5 +++ .../image_viewer_tap_to_navigate_setting.dart | 11 +++--- .../video_viewer_settings.dart | 25 ++++++++----- .../metadata_repository_test.dart | 3 -- 13 files changed, 96 insertions(+), 43 deletions(-) create mode 100644 mobile/lib/domain/models/config/viewer_config.dart diff --git a/mobile/lib/domain/models/config/app_config.dart b/mobile/lib/domain/models/config/app_config.dart index 942260158b..beca1c21e7 100644 --- a/mobile/lib/domain/models/config/app_config.dart +++ b/mobile/lib/domain/models/config/app_config.dart @@ -3,6 +3,7 @@ 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/theme_config.dart'; import 'package:immich_mobile/domain/models/config/timeline_config.dart'; +import 'package:immich_mobile/domain/models/config/viewer_config.dart'; class AppConfig { final ThemeConfig theme; @@ -10,6 +11,7 @@ class AppConfig { final MapConfig map; final TimelineConfig timeline; final ImageConfig image; + final ViewerConfig viewer; const AppConfig({ this.theme = const .new(), @@ -17,6 +19,7 @@ class AppConfig { this.map = const .new(), this.timeline = const .new(), this.image = const .new(), + this.viewer = const .new(), }); AppConfig copyWith({ @@ -25,12 +28,14 @@ class AppConfig { MapConfig? map, TimelineConfig? timeline, ImageConfig? image, + ViewerConfig? viewer, }) => .new( theme: theme ?? this.theme, cleanup: cleanup ?? this.cleanup, map: map ?? this.map, timeline: timeline ?? this.timeline, image: image ?? this.image, + viewer: viewer ?? this.viewer, ); @override @@ -41,11 +46,13 @@ class AppConfig { other.cleanup == cleanup && other.map == map && other.timeline == timeline && - other.image == image); + other.image == image && + other.viewer == viewer); @override - int get hashCode => Object.hash(theme, cleanup, map, timeline, image); + int get hashCode => Object.hash(theme, cleanup, map, timeline, image, viewer); @override - String toString() => 'AppConfig(theme: $theme, cleanup: $cleanup, map: $map, timeline: $timeline, image: $image)'; + String toString() => + 'AppConfig(theme: $theme, cleanup: $cleanup, map: $map, timeline: $timeline, image: $image, viewer: $viewer)'; } diff --git a/mobile/lib/domain/models/config/viewer_config.dart b/mobile/lib/domain/models/config/viewer_config.dart new file mode 100644 index 0000000000..595f2bee5d --- /dev/null +++ b/mobile/lib/domain/models/config/viewer_config.dart @@ -0,0 +1,37 @@ +class ViewerConfig { + final bool loopVideo; + final bool loadOriginalVideo; + final bool autoPlayVideo; + final bool tapToNavigate; + + const ViewerConfig({ + this.loopVideo = true, + this.loadOriginalVideo = false, + this.autoPlayVideo = true, + this.tapToNavigate = false, + }); + + ViewerConfig copyWith({bool? loopVideo, bool? loadOriginalVideo, bool? autoPlayVideo, bool? tapToNavigate}) => + ViewerConfig( + loopVideo: loopVideo ?? this.loopVideo, + loadOriginalVideo: loadOriginalVideo ?? this.loadOriginalVideo, + autoPlayVideo: autoPlayVideo ?? this.autoPlayVideo, + tapToNavigate: tapToNavigate ?? this.tapToNavigate, + ); + + @override + bool operator ==(Object other) => + identical(this, other) || + (other is ViewerConfig && + other.loopVideo == loopVideo && + other.loadOriginalVideo == loadOriginalVideo && + other.autoPlayVideo == autoPlayVideo && + other.tapToNavigate == tapToNavigate); + + @override + int get hashCode => Object.hash(loopVideo, loadOriginalVideo, autoPlayVideo, tapToNavigate); + + @override + String toString() => + 'ViewerConfig(loopVideo: $loopVideo, loadOriginalVideo: $loadOriginalVideo, autoPlayVideo: $autoPlayVideo, tapToNavigate: $tapToNavigate)'; +} diff --git a/mobile/lib/domain/models/metadata_key.dart b/mobile/lib/domain/models/metadata_key.dart index c692d77f6b..61a3cebc8a 100644 --- a/mobile/lib/domain/models/metadata_key.dart +++ b/mobile/lib/domain/models/metadata_key.dart @@ -28,6 +28,12 @@ enum MetadataKey { 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( diff --git a/mobile/lib/domain/models/setting.model.dart b/mobile/lib/domain/models/setting.model.dart index f7cb340ee3..0dc48de3b1 100644 --- a/mobile/lib/domain/models/setting.model.dart +++ b/mobile/lib/domain/models/setting.model.dart @@ -1,8 +1,6 @@ import 'package:immich_mobile/domain/models/store.model.dart'; enum Setting { - loadOriginalVideo(StoreKey.loadOriginalVideo, false), - autoPlayVideo(StoreKey.autoPlayVideo, true), advancedTroubleshooting(StoreKey.advancedTroubleshooting, false), enableBackup(StoreKey.enableBackup, false); diff --git a/mobile/lib/domain/models/store.model.dart b/mobile/lib/domain/models/store.model.dart index 9244eb3c52..e52e8a0a92 100644 --- a/mobile/lib/domain/models/store.model.dart +++ b/mobile/lib/domain/models/store.model.dart @@ -40,12 +40,6 @@ enum StoreKey { albumGridView._(140), loadOriginal._(101), - // Image viewer navigation settings - loopVideo._(117), - loadOriginalVideo._(136), - autoPlayVideo._(139), - tapToNavigate._(141), - // Experimental stuff enableBackup._(1003), useWifiForUploadVideos._(1004), @@ -53,6 +47,10 @@ enum StoreKey { syncMigrationStatus._(1013), // Legacy keys that have been migrated to the new metadata store + legacyLoopVideo._(117), + legacyLoadOriginalVideo._(136), + legacyAutoPlayVideo._(139), + legacyTapToNavigate._(141), legacyPreferRemoteImage._(116), legacyLoadOriginal._(101), legacyPrimaryColor._(128), diff --git a/mobile/lib/infrastructure/repositories/metadata.repository.dart b/mobile/lib/infrastructure/repositories/metadata.repository.dart index ef9ad6b8ab..d8c8f55898 100644 --- a/mobile/lib/infrastructure/repositories/metadata.repository.dart +++ b/mobile/lib/infrastructure/repositories/metadata.repository.dart @@ -133,6 +133,12 @@ extension on MetadataDomain { 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), + ), ); case .systemConfig: repo._systemConfig = .new(logLevel: repo._read(.logLevel)); diff --git a/mobile/lib/presentation/widgets/asset_viewer/asset_page.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/asset_page.widget.dart index a531917e5b..77f693f5c4 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/asset_page.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/asset_page.widget.dart @@ -17,11 +17,10 @@ import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_stack.widg import 'package:immich_mobile/presentation/widgets/asset_viewer/video_viewer.widget.dart'; import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart'; import 'package:immich_mobile/widgets/photo_view/photo_view.dart'; @@ -231,7 +230,7 @@ class _AssetPageState extends ConsumerState { return; } - final tapToNavigate = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.tapToNavigate); + final tapToNavigate = ref.read(metadataProvider).appConfig.viewer.tapToNavigate; if (!tapToNavigate) { _viewer.toggleControls(); return; diff --git a/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart index bf2ab17425..97ca8ace10 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart @@ -3,21 +3,17 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/domain/models/setting.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/domain/services/setting.service.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/video_player_provider.dart'; import 'package:immich_mobile/providers/cast.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; -import 'package:immich_mobile/providers/infrastructure/setting.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; import 'package:immich_mobile/services/api.service.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:logging/logging.dart'; import 'package:native_video_player/native_video_player.dart'; @@ -132,7 +128,7 @@ class _NativeVideoViewerState extends ConsumerState with Widg final remoteId = (videoAsset as RemoteAsset).id; final serverEndpoint = Store.get(StoreKey.serverEndpoint); - final isOriginalVideo = ref.read(settingsProvider).get(Setting.loadOriginalVideo); + final isOriginalVideo = ref.read(metadataProvider).appConfig.viewer.loadOriginalVideo; final String postfixUrl = isOriginalVideo ? 'original' : 'video/playback'; final String videoUrl = videoAsset.livePhotoVideoId != null ? '$serverEndpoint/assets/${videoAsset.livePhotoVideoId}/$postfixUrl' @@ -165,7 +161,7 @@ class _NativeVideoViewerState extends ConsumerState with Widg return; } - final autoPlayVideo = AppSetting.get(Setting.autoPlayVideo); + final autoPlayVideo = ref.read(metadataProvider).appConfig.viewer.autoPlayVideo; if (autoPlayVideo || widget.asset.isMotionPhoto) { await _notifier.play(); } @@ -216,7 +212,7 @@ class _NativeVideoViewerState extends ConsumerState with Widg } await _notifier.load(source); - final loopVideo = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.loopVideo); + final loopVideo = ref.read(metadataProvider).appConfig.viewer.loopVideo; await _notifier.setLoop(!widget.asset.isMotionPhoto && loopVideo); await _notifier.setVolume(1); } diff --git a/mobile/lib/services/app_settings.service.dart b/mobile/lib/services/app_settings.service.dart index e04c200989..38d3e028cb 100644 --- a/mobile/lib/services/app_settings.service.dart +++ b/mobile/lib/services/app_settings.service.dart @@ -10,10 +10,6 @@ enum AppSettingsEnum { selectedAlbumSortOrder(StoreKey.selectedAlbumSortOrder, "selectedAlbumSortOrder", 2), advancedTroubleshooting(StoreKey.advancedTroubleshooting, null, false), manageLocalMediaAndroid(StoreKey.manageLocalMediaAndroid, null, false), - loopVideo(StoreKey.loopVideo, "loopVideo", true), - loadOriginalVideo(StoreKey.loadOriginalVideo, "loadOriginalVideo", false), - autoPlayVideo(StoreKey.autoPlayVideo, "autoPlayVideo", true), - tapToNavigate(StoreKey.tapToNavigate, "tapToNavigate", false), allowSelfSignedSSLCert(StoreKey.selfSignedCert, null, false), selectedAlbumSortReverse(StoreKey.selectedAlbumSortReverse, null, true), enableHapticFeedback(StoreKey.enableHapticFeedback, null, true), diff --git a/mobile/lib/utils/migration.dart b/mobile/lib/utils/migration.dart index e6d2143468..8f0eb00b16 100644 --- a/mobile/lib/utils/migration.dart +++ b/mobile/lib/utils/migration.dart @@ -91,6 +91,11 @@ Future _migrateTo26(Drift drift) async { // Image await migrator.migrateBool(StoreKey.legacyPreferRemoteImage, MetadataKey.imagePreferRemote); await migrator.migrateBool(StoreKey.legacyLoadOriginal, MetadataKey.imageLoadOriginal); + // Viewer + await migrator.migrateBool(StoreKey.legacyLoopVideo, MetadataKey.viewerLoopVideo); + await migrator.migrateBool(StoreKey.legacyLoadOriginalVideo, MetadataKey.viewerLoadOriginalVideo); + await migrator.migrateBool(StoreKey.legacyAutoPlayVideo, MetadataKey.viewerAutoPlayVideo); + await migrator.migrateBool(StoreKey.legacyTapToNavigate, MetadataKey.viewerTapToNavigate); await migrator.complete(); } diff --git a/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_tap_to_navigate_setting.dart b/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_tap_to_navigate_setting.dart index 759162cab8..5af64b0be9 100644 --- a/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_tap_to_navigate_setting.dart +++ b/mobile/lib/widgets/settings/asset_viewer_settings/image_viewer_tap_to_navigate_setting.dart @@ -1,18 +1,20 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; +import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; import 'package:immich_mobile/widgets/settings/settings_sub_title.dart'; import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; -import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; class ImageViewerTapToNavigateSetting extends HookConsumerWidget { const ImageViewerTapToNavigateSetting({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - final tapToNavigate = useAppSettingsState(AppSettingsEnum.tapToNavigate); + final tapToNavigate = useState(ref.read(appConfigProvider).viewer.tapToNavigate); + useValueChanged(tapToNavigate.value, (_, __) { + ref.read(metadataProvider).write(.viewerTapToNavigate, tapToNavigate.value); + }); return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -22,7 +24,6 @@ class ImageViewerTapToNavigateSetting extends HookConsumerWidget { valueNotifier: tapToNavigate, title: "setting_image_navigation_enable_title".tr(), subtitle: "setting_image_navigation_enable_subtitle".tr(), - onChanged: (_) => ref.invalidate(appSettingsServiceProvider), ), ], ); diff --git a/mobile/lib/widgets/settings/asset_viewer_settings/video_viewer_settings.dart b/mobile/lib/widgets/settings/asset_viewer_settings/video_viewer_settings.dart index c03dcc51b4..8d62544dd4 100644 --- a/mobile/lib/widgets/settings/asset_viewer_settings/video_viewer_settings.dart +++ b/mobile/lib/widgets/settings/asset_viewer_settings/video_viewer_settings.dart @@ -1,20 +1,30 @@ import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; +import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; import 'package:immich_mobile/widgets/settings/setting_group_title.dart'; import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; -import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; class VideoViewerSettings extends HookConsumerWidget { const VideoViewerSettings({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - final useLoopVideo = useAppSettingsState(AppSettingsEnum.loopVideo); - final useOriginalVideo = useAppSettingsState(AppSettingsEnum.loadOriginalVideo); - final useAutoPlayVideo = useAppSettingsState(AppSettingsEnum.autoPlayVideo); + final viewer = ref.read(appConfigProvider).viewer; + final useAutoPlayVideo = useState(viewer.autoPlayVideo); + final useLoopVideo = useState(viewer.loopVideo); + final useOriginalVideo = useState(viewer.loadOriginalVideo); + + useValueChanged(useAutoPlayVideo.value, (_, __) { + ref.read(metadataProvider).write(.viewerAutoPlayVideo, useAutoPlayVideo.value); + }); + useValueChanged(useLoopVideo.value, (_, __) { + ref.read(metadataProvider).write(.viewerLoopVideo, useLoopVideo.value); + }); + useValueChanged(useOriginalVideo.value, (_, __) { + ref.read(metadataProvider).write(.viewerLoadOriginalVideo, useOriginalVideo.value); + }); return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -27,19 +37,16 @@ class VideoViewerSettings extends HookConsumerWidget { valueNotifier: useAutoPlayVideo, title: "setting_video_viewer_auto_play_title".t(context: context), subtitle: "setting_video_viewer_auto_play_subtitle".t(context: context), - onChanged: (_) => ref.invalidate(appSettingsServiceProvider), ), SettingsSwitchListTile( valueNotifier: useLoopVideo, title: "setting_video_viewer_looping_title".t(context: context), subtitle: "loop_videos_description".t(context: context), - onChanged: (_) => ref.invalidate(appSettingsServiceProvider), ), SettingsSwitchListTile( valueNotifier: useOriginalVideo, title: "setting_video_viewer_original_video_title".t(context: context), subtitle: "setting_video_viewer_original_video_subtitle".t(context: context), - onChanged: (_) => ref.invalidate(appSettingsServiceProvider), ), ], ); diff --git a/mobile/test/medium/repositories/metadata_repository_test.dart b/mobile/test/medium/repositories/metadata_repository_test.dart index 32f613483d..7b185f3bec 100644 --- a/mobile/test/medium/repositories/metadata_repository_test.dart +++ b/mobile/test/medium/repositories/metadata_repository_test.dart @@ -79,7 +79,6 @@ void main() { expect(sut.appConfig.theme.mode, ThemeMode.system); await MetadataRepository.refresh(); - expect(sut.appConfig.theme.mode, ThemeMode.dark); }); @@ -90,7 +89,6 @@ void main() { expect(sut.appConfig.theme.mode, ThemeMode.dark); await MetadataRepository.refresh(); - expect(sut.appConfig.theme.mode, ThemeMode.system); }); @@ -135,5 +133,4 @@ void main() { await expectation; }); }); - }