From 296b696be966fbadb1dfad58998c3f39aef654c5 Mon Sep 17 00:00:00 2001 From: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Date: Sun, 3 May 2026 22:07:12 +0700 Subject: [PATCH] migrate colorfulInterface --- .../domain/models/config/theme_config.dart | 24 +++- mobile/lib/domain/models/metadata_key.dart | 3 +- mobile/lib/domain/models/store.model.dart | 4 +- .../repositories/metadata.repository.dart | 1 + mobile/lib/providers/theme.provider.dart | 8 +- mobile/lib/services/app_settings.service.dart | 1 - mobile/lib/utils/migration.dart | 116 +++++++++++------- .../preference_settings/theme_setting.dart | 17 +-- .../repositories/store_repository_test.dart | 30 ++--- 9 files changed, 117 insertions(+), 87 deletions(-) diff --git a/mobile/lib/domain/models/config/theme_config.dart b/mobile/lib/domain/models/config/theme_config.dart index fd83defdd6..fa955c5d46 100644 --- a/mobile/lib/domain/models/config/theme_config.dart +++ b/mobile/lib/domain/models/config/theme_config.dart @@ -5,13 +5,25 @@ class ThemeConfig { final ThemeMode mode; final ImmichColorPreset primaryColor; final bool dynamicTheme; + final bool colorfulInterface; - const ThemeConfig({this.mode = .system, this.primaryColor = .indigo, this.dynamicTheme = false}); + const ThemeConfig({ + this.mode = .system, + this.primaryColor = .indigo, + this.dynamicTheme = false, + this.colorfulInterface = true, + }); - ThemeConfig copyWith({ThemeMode? mode, ImmichColorPreset? primaryColor, bool? dynamicTheme}) => .new( + ThemeConfig copyWith({ + ThemeMode? mode, + ImmichColorPreset? primaryColor, + bool? dynamicTheme, + bool? colorfulInterface, + }) => .new( mode: mode ?? this.mode, primaryColor: primaryColor ?? this.primaryColor, dynamicTheme: dynamicTheme ?? this.dynamicTheme, + colorfulInterface: colorfulInterface ?? this.colorfulInterface, ); @override @@ -20,11 +32,13 @@ class ThemeConfig { (other is ThemeConfig && other.mode == mode && other.primaryColor == primaryColor && - other.dynamicTheme == dynamicTheme); + other.dynamicTheme == dynamicTheme && + other.colorfulInterface == colorfulInterface); @override - int get hashCode => Object.hash(mode, primaryColor, dynamicTheme); + int get hashCode => Object.hash(mode, primaryColor, dynamicTheme, colorfulInterface); @override - String toString() => 'ThemeConfig(mode: $mode, primaryColor: $primaryColor, dynamicTheme: $dynamicTheme)'; + String toString() => + 'ThemeConfig(mode: $mode, primaryColor: $primaryColor, dynamicTheme: $dynamicTheme, colorfulInterface: $colorfulInterface)'; } diff --git a/mobile/lib/domain/models/metadata_key.dart b/mobile/lib/domain/models/metadata_key.dart index 6d510413da..a5d158f9c7 100644 --- a/mobile/lib/domain/models/metadata_key.dart +++ b/mobile/lib/domain/models/metadata_key.dart @@ -17,7 +17,8 @@ enum MetadataKey { // Theme primaryColor(.appConfig, 'theme.primaryColor', .indigo, _EnumCodec(ImmichColorPreset.values)), themeMode(.appConfig, 'theme.mode', .system, _EnumCodec(ThemeMode.values)), - dynamicTheme(.appConfig, 'dynamicTheme', false), + dynamicTheme(.appConfig, 'theme.dynamicTheme', false), + colorfulInterface(.appConfig, 'theme.colorfulInterface', true), // Log logLevel(.systemConfig, 'log.level', .info, _EnumCodec(LogLevel.values)); diff --git a/mobile/lib/domain/models/store.model.dart b/mobile/lib/domain/models/store.model.dart index 413549d021..fb497f1e9e 100644 --- a/mobile/lib/domain/models/store.model.dart +++ b/mobile/lib/domain/models/store.model.dart @@ -48,9 +48,6 @@ enum StoreKey { enableHapticFeedback._(126), customHeaders._(127), - // theme settings - colorfulInterface._(130), - syncAlbums._(131), // Auto endpoint switching @@ -95,6 +92,7 @@ enum StoreKey { // Legacy keys that have been migrated to the new metadata store legacyPrimaryColor._(128), legacyDynamicTheme._(129), + legacyColorfulInterface._(130), legacyThemeMode._(102), legacyLogLevel._(115); diff --git a/mobile/lib/infrastructure/repositories/metadata.repository.dart b/mobile/lib/infrastructure/repositories/metadata.repository.dart index 825e8e664e..15a2eb5230 100644 --- a/mobile/lib/infrastructure/repositories/metadata.repository.dart +++ b/mobile/lib/infrastructure/repositories/metadata.repository.dart @@ -105,6 +105,7 @@ extension on MetadataDomain { mode: repo._read(.themeMode), primaryColor: repo._read(.primaryColor), dynamicTheme: repo._read(.dynamicTheme), + colorfulInterface: repo._read(.colorfulInterface), ), ); case .systemConfig: diff --git a/mobile/lib/providers/theme.provider.dart b/mobile/lib/providers/theme.provider.dart index f6497bfa0a..a56a8e7bd8 100644 --- a/mobile/lib/providers/theme.provider.dart +++ b/mobile/lib/providers/theme.provider.dart @@ -1,9 +1,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/colors.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/theme/color_scheme.dart'; import 'package:immich_mobile/theme/dynamic_theme.dart'; import 'package:immich_mobile/theme/theme_data.dart'; @@ -18,9 +16,9 @@ final dynamicThemeSettingProvider = StateProvider( (ref) => ref.watch(appConfigProvider.select((config) => config.theme.dynamicTheme)), ); -final colorfulInterfaceSettingProvider = StateProvider((ref) { - return ref.watch(appSettingsServiceProvider).getSetting(AppSettingsEnum.colorfulInterface); -}); +final colorfulInterfaceSettingProvider = StateProvider( + (ref) => ref.watch(appConfigProvider.select((config) => config.theme.colorfulInterface)), +); // Provider for current selected theme final immichThemeProvider = StateProvider((ref) { diff --git a/mobile/lib/services/app_settings.service.dart b/mobile/lib/services/app_settings.service.dart index d2363a72eb..5c565aff03 100644 --- a/mobile/lib/services/app_settings.service.dart +++ b/mobile/lib/services/app_settings.service.dart @@ -4,7 +4,6 @@ import 'package:immich_mobile/entities/store.entity.dart'; enum AppSettingsEnum { loadPreview(StoreKey.loadPreview, "loadPreview", true), loadOriginal(StoreKey.loadOriginal, "loadOriginal", false), - colorfulInterface(StoreKey.colorfulInterface, "colorfulInterface", true), tilesPerRow(StoreKey.tilesPerRow, "tilesPerRow", 4), dynamicLayout(StoreKey.dynamicLayout, "dynamicLayout", false), groupAssetsBy(StoreKey.groupAssetsBy, "groupBy", 0), diff --git a/mobile/lib/utils/migration.dart b/mobile/lib/utils/migration.dart index 4a5429e7e5..7b3cfa00dc 100644 --- a/mobile/lib/utils/migration.dart +++ b/mobile/lib/utils/migration.dart @@ -6,10 +6,9 @@ import 'package:immich_mobile/constants/colors.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'; -import 'package:immich_mobile/domain/services/log.service.dart'; import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/metadata.entity.drift.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/network.repository.dart'; import 'package:immich_mobile/services/api.service.dart'; @@ -41,51 +40,78 @@ Future _migrateTo25() async { } Future _migrateTo26(Drift drift) async { - final repo = MetadataRepository.instance; - final migrated = []; - - final themeMode = await _readLegacyStoreString(drift, StoreKey.legacyThemeMode.id); - if (themeMode != null) { - final mode = ThemeMode.values.firstWhere((m) => m.name == themeMode, orElse: () => ThemeMode.system); - await repo.write(MetadataKey.themeMode, mode); - migrated.add(StoreKey.legacyThemeMode.id); - } - - final logLevelIndex = await _readLegacyStoreInt(drift, StoreKey.legacyLogLevel.id); - if (logLevelIndex != null) { - final logLevel = LogLevel.values.elementAtOrNull(logLevelIndex) ?? LogLevel.info; - await LogService.I.setLogLevel(logLevel); - migrated.add(StoreKey.legacyLogLevel.id); - } - - final primaryColorIndex = await _readLegacyStoreInt(drift, StoreKey.legacyPrimaryColor.id); - if (primaryColorIndex != null) { - final primaryColor = ImmichColorPreset.values.elementAtOrNull(primaryColorIndex) ?? ImmichColorPreset.indigo; - await repo.write(MetadataKey.primaryColor, primaryColor); - migrated.add(StoreKey.legacyPrimaryColor.id); - } - - final dynamicTheme = await _readLegacyStoreInt(drift, StoreKey.legacyDynamicTheme.id); - if (dynamicTheme != null) { - final dynamicThemeValue = dynamicTheme != 0; - await repo.write(MetadataKey.dynamicTheme, dynamicThemeValue); - migrated.add(StoreKey.legacyDynamicTheme.id); - } - - await _deleteLegacyStoreRows(drift, migrated); + final migrator = _StoreMigrator(drift); + await migrator.migrateEnumName(StoreKey.legacyThemeMode, MetadataKey.themeMode, ThemeMode.values); + await migrator.migrateEnumIndex(StoreKey.legacyLogLevel, MetadataKey.logLevel, LogLevel.values); + await migrator.migrateEnumName(StoreKey.legacyPrimaryColor, MetadataKey.primaryColor, ImmichColorPreset.values); + await migrator.migrateBool(StoreKey.legacyDynamicTheme, MetadataKey.dynamicTheme); + await migrator.migrateBool(StoreKey.legacyColorfulInterface, MetadataKey.colorfulInterface); + await migrator.complete(); } -Future _readLegacyStoreString(Drift drift, int id) async { - final row = await (drift.storeEntity.select()..where((t) => t.id.equals(id))).getSingleOrNull(); - return row?.stringValue; -} +class _StoreMigrator { + final Drift _db; + final Map, Object> _cache = {}; + final List _migratedStoreIds = []; -Future _readLegacyStoreInt(Drift drift, int id) async { - final row = await (drift.storeEntity.select()..where((t) => t.id.equals(id))).getSingleOrNull(); - return row?.intValue; -} + _StoreMigrator(this._db); -Future _deleteLegacyStoreRows(Drift drift, List ids) async { - if (ids.isEmpty) return; - await (drift.storeEntity.delete()..where((t) => t.id.isIn(ids))).go(); + Future migrateEnumIndex(StoreKey legacyKey, MetadataKey newKey, List values) async { + final index = await _readLegacyStoreInt(legacyKey.id); + if (index == null) return; + + final enumValue = values.elementAtOrNull(index) ?? newKey.defaultValue; + _cache[newKey] = enumValue; + _migratedStoreIds.add(legacyKey.id); + } + + Future migrateEnumName( + StoreKey legacyKey, + MetadataKey newKey, + List values, + ) async { + final name = await _readLegacyStoreString(legacyKey.id); + if (name == null) return; + + final enumValue = values.firstWhere((e) => e.name == name, orElse: () => newKey.defaultValue); + _cache[newKey] = enumValue; + _migratedStoreIds.add(legacyKey.id); + } + + Future migrateBool(StoreKey legacyKey, MetadataKey newKey) async { + final intValue = await _readLegacyStoreInt(legacyKey.id); + if (intValue == null) return; + + final boolValue = intValue != 0; + _cache[newKey] = boolValue; + _migratedStoreIds.add(legacyKey.id); + } + + Future complete() async { + await _db.batch((batch) { + for (final entry in _cache.entries) { + batch.insert( + _db.metadataEntity, + MetadataEntityCompanion(key: Value(entry.key.key), value: Value(entry.key.encode(entry.value))), + mode: InsertMode.insertOrReplace, + ); + } + }); + await _deleteLegacyStoreRows(_migratedStoreIds); + } + + Future _readLegacyStoreString(int id) async { + final row = await (_db.storeEntity.select()..where((t) => t.id.equals(id))).getSingleOrNull(); + return row?.stringValue; + } + + Future _readLegacyStoreInt(int id) async { + final row = await (_db.storeEntity.select()..where((t) => t.id.equals(id))).getSingleOrNull(); + return row?.intValue; + } + + Future _deleteLegacyStoreRows(List ids) async { + if (ids.isEmpty) return; + await (_db.storeEntity.delete()..where((t) => t.id.isIn(ids))).go(); + } } diff --git a/mobile/lib/widgets/settings/preference_settings/theme_setting.dart b/mobile/lib/widgets/settings/preference_settings/theme_setting.dart index 05eb65f16e..a12b14cb3c 100644 --- a/mobile/lib/widgets/settings/preference_settings/theme_setting.dart +++ b/mobile/lib/widgets/settings/preference_settings/theme_setting.dart @@ -6,8 +6,6 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; import 'package:immich_mobile/providers/theme.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; import 'package:immich_mobile/widgets/settings/preference_settings/primary_color_setting.dart'; import 'package:immich_mobile/widgets/settings/setting_group_title.dart'; import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; @@ -20,13 +18,8 @@ class ThemeSetting extends HookConsumerWidget { final currentTheme = useState(ref.read(immichThemeModeProvider)); final isDarkTheme = useValueNotifier(currentTheme.value == ThemeMode.dark); final isSystemTheme = useValueNotifier(currentTheme.value == ThemeMode.system); - - final applyThemeToBackgroundSetting = useAppSettingsState(AppSettingsEnum.colorfulInterface); - final applyThemeToBackgroundProvider = useValueNotifier(ref.read(colorfulInterfaceSettingProvider)); - - useValueChanged( - applyThemeToBackgroundSetting.value, - (_, __) => applyThemeToBackgroundProvider.value = applyThemeToBackgroundSetting.value, + final colorfulInterface = useValueNotifier( + ref.watch(appConfigProvider.select((config) => config.theme.colorfulInterface)), ); void onThemeChange(bool isDark) { @@ -61,8 +54,8 @@ class ThemeSetting extends HookConsumerWidget { } void onSurfaceColorSettingChange(bool useColorfulInterface) { - applyThemeToBackgroundSetting.value = useColorfulInterface; - ref.watch(colorfulInterfaceSettingProvider.notifier).state = useColorfulInterface; + ref.read(metadataProvider).write(MetadataKey.colorfulInterface, useColorfulInterface); + colorfulInterface.value = useColorfulInterface; } return Column( @@ -85,7 +78,7 @@ class ThemeSetting extends HookConsumerWidget { ), const PrimaryColorSetting(), SettingsSwitchListTile( - valueNotifier: applyThemeToBackgroundProvider, + valueNotifier: colorfulInterface, title: "theme_setting_colorful_interface_title".t(context: context), subtitle: 'theme_setting_colorful_interface_subtitle'.t(context: context), onChanged: onSurfaceColorSettingChange, diff --git a/mobile/test/infrastructure/repositories/store_repository_test.dart b/mobile/test/infrastructure/repositories/store_repository_test.dart index 4cf1adc6b1..fc789589ca 100644 --- a/mobile/test/infrastructure/repositories/store_repository_test.dart +++ b/mobile/test/infrastructure/repositories/store_repository_test.dart @@ -14,7 +14,7 @@ import '../../fixtures/user.stub.dart'; const _kTestAccessToken = "#TestToken"; final _kTestBackupFailed = DateTime(2025, 2, 20, 11, 45); const _kTestVersion = 10; -const _kTestColorfulInterface = false; +const _kTestBackupRequireWifi = false; final _kTestUser = UserStub.admin; Future _populateStore(Drift db) async { @@ -22,8 +22,8 @@ Future _populateStore(Drift db) async { batch.insert( db.storeEntity, StoreEntityCompanion( - id: Value(StoreKey.colorfulInterface.id), - intValue: const Value(_kTestColorfulInterface ? 1 : 0), + id: Value(StoreKey.backupRequireWifi.id), + intValue: const Value(_kTestBackupRequireWifi ? 1 : 0), stringValue: const Value(null), ), ); @@ -93,11 +93,11 @@ void main() { }); test('converts bool', () async { - bool? colorfulInterface = await sut.tryGet(StoreKey.colorfulInterface); - expect(colorfulInterface, isNull); - await sut.upsert(StoreKey.colorfulInterface, _kTestColorfulInterface); - colorfulInterface = await sut.tryGet(StoreKey.colorfulInterface); - expect(colorfulInterface, _kTestColorfulInterface); + bool? backupRequireWifi = await sut.tryGet(StoreKey.backupRequireWifi); + expect(backupRequireWifi, isNull); + await sut.upsert(StoreKey.backupRequireWifi, _kTestBackupRequireWifi); + backupRequireWifi = await sut.tryGet(StoreKey.backupRequireWifi); + expect(backupRequireWifi, _kTestBackupRequireWifi); }); test('converts user', () async { @@ -115,11 +115,11 @@ void main() { }); test('delete()', () async { - bool? isColorful = await sut.tryGet(StoreKey.colorfulInterface); - expect(isColorful, isFalse); - await sut.delete(StoreKey.colorfulInterface); - isColorful = await sut.tryGet(StoreKey.colorfulInterface); - expect(isColorful, isNull); + bool? backupRequireWifi = await sut.tryGet(StoreKey.backupRequireWifi); + expect(backupRequireWifi, isFalse); + await sut.delete(StoreKey.backupRequireWifi); + backupRequireWifi = await sut.tryGet(StoreKey.backupRequireWifi); + expect(backupRequireWifi, isNull); }); test('deleteAll()', () async { @@ -166,13 +166,13 @@ void main() { const StoreDto(StoreKey.version, _kTestVersion), StoreDto(StoreKey.backupFailedSince, _kTestBackupFailed), const StoreDto(StoreKey.accessToken, _kTestAccessToken), - const StoreDto(StoreKey.colorfulInterface, _kTestColorfulInterface), + const StoreDto(StoreKey.backupRequireWifi, _kTestBackupRequireWifi), ], [ const StoreDto(StoreKey.version, _kTestVersion + 10), StoreDto(StoreKey.backupFailedSince, _kTestBackupFailed), const StoreDto(StoreKey.accessToken, _kTestAccessToken), - const StoreDto(StoreKey.colorfulInterface, _kTestColorfulInterface), + const StoreDto(StoreKey.backupRequireWifi, _kTestBackupRequireWifi), ], ]), ),