From d4a97f2d25113b47ebbaa3ad493f90fc8dc132e1 Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Thu, 7 May 2026 22:12:14 +0700 Subject: [PATCH] refactor: move theme config to metadata table (#28224) * 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 --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> --- mobile/lib/constants/colors.dart | 3 - .../domain/models/config/theme_config.dart | 36 ++++++- mobile/lib/domain/models/metadata_key.dart | 7 ++ mobile/lib/domain/models/store.model.dart | 8 +- .../repositories/metadata.repository.dart | 15 ++- mobile/lib/main.dart | 3 +- .../lib/pages/common/splash_screen.page.dart | 4 +- .../infrastructure/metadata.provider.dart | 2 +- mobile/lib/providers/theme.provider.dart | 43 ++------- mobile/lib/services/app_settings.service.dart | 4 - mobile/lib/utils/migration.dart | 95 +++++++++++++------ .../primary_color_setting.dart | 34 ++----- .../preference_settings/theme_setting.dart | 31 ++---- .../domain/services/log_service_test.dart | 4 +- .../repositories/store_repository_test.dart | 30 +++--- 15 files changed, 166 insertions(+), 153 deletions(-) diff --git a/mobile/lib/constants/colors.dart b/mobile/lib/constants/colors.dart index e39480de32..655d2d9c09 100644 --- a/mobile/lib/constants/colors.dart +++ b/mobile/lib/constants/colors.dart @@ -2,9 +2,6 @@ import 'package:flutter/material.dart'; enum ImmichColorPreset { indigo, deepPurple, pink, red, orange, yellow, lime, green, cyan, slateGray } -const ImmichColorPreset defaultColorPreset = ImmichColorPreset.indigo; -const String defaultColorPresetName = "indigo"; - const Color immichBrandColorLight = Color(0xFF4150AF); const Color immichBrandColorDark = Color(0xFFACCBFA); const Color whiteOpacity75 = Color.fromRGBO(255, 255, 255, 0.75); diff --git a/mobile/lib/domain/models/config/theme_config.dart b/mobile/lib/domain/models/config/theme_config.dart index 6e0c007151..fa955c5d46 100644 --- a/mobile/lib/domain/models/config/theme_config.dart +++ b/mobile/lib/domain/models/config/theme_config.dart @@ -1,18 +1,44 @@ import 'package:flutter/material.dart'; +import 'package:immich_mobile/constants/colors.dart'; class ThemeConfig { final ThemeMode mode; + final ImmichColorPreset primaryColor; + final bool dynamicTheme; + final bool colorfulInterface; - const ThemeConfig({this.mode = .system}); + const ThemeConfig({ + this.mode = .system, + this.primaryColor = .indigo, + this.dynamicTheme = false, + this.colorfulInterface = true, + }); - ThemeConfig copyWith({ThemeMode? mode}) => .new(mode: mode ?? this.mode); + 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 - bool operator ==(Object other) => identical(this, other) || (other is ThemeConfig && other.mode == mode); + bool operator ==(Object other) => + identical(this, other) || + (other is ThemeConfig && + other.mode == mode && + other.primaryColor == primaryColor && + other.dynamicTheme == dynamicTheme && + other.colorfulInterface == colorfulInterface); @override - int get hashCode => mode.hashCode; + int get hashCode => Object.hash(mode, primaryColor, dynamicTheme, colorfulInterface); @override - String toString() => 'ThemeConfig(mode: $mode)'; + 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 2e122e6c5d..a5d158f9c7 100644 --- a/mobile/lib/domain/models/metadata_key.dart +++ b/mobile/lib/domain/models/metadata_key.dart @@ -1,5 +1,6 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; +import 'package:immich_mobile/constants/colors.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'; @@ -13,7 +14,13 @@ enum MetadataDomain { } enum MetadataKey { + // Theme + primaryColor(.appConfig, 'theme.primaryColor', .indigo, _EnumCodec(ImmichColorPreset.values)), themeMode(.appConfig, 'theme.mode', .system, _EnumCodec(ThemeMode.values)), + dynamicTheme(.appConfig, 'theme.dynamicTheme', false), + colorfulInterface(.appConfig, 'theme.colorfulInterface', true), + + // Log logLevel(.systemConfig, 'log.level', .info, _EnumCodec(LogLevel.values)); final MetadataDomain domain; diff --git a/mobile/lib/domain/models/store.model.dart b/mobile/lib/domain/models/store.model.dart index 481085c4c1..fb497f1e9e 100644 --- a/mobile/lib/domain/models/store.model.dart +++ b/mobile/lib/domain/models/store.model.dart @@ -48,11 +48,6 @@ enum StoreKey { enableHapticFeedback._(126), customHeaders._(127), - // theme settings - primaryColor._(128), - dynamicTheme._(129), - colorfulInterface._(130), - syncAlbums._(131), // Auto endpoint switching @@ -95,6 +90,9 @@ enum StoreKey { syncMigrationStatus._(1013), // 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 d0e0ab2501..96b2f2b3df 100644 --- a/mobile/lib/infrastructure/repositories/metadata.repository.dart +++ b/mobile/lib/infrastructure/repositories/metadata.repository.dart @@ -47,7 +47,7 @@ class MetadataRepository extends DriftDatabaseRepository { T _read(MetadataKey key) => (_cache[key] as T?) ?? key.defaultValue; - Future write(MetadataKey key, T value) async { + Future write(MetadataKey key, U value) async { if (_read(key) == value) return; await _db @@ -63,9 +63,9 @@ class MetadataRepository extends DriftDatabaseRepository { _updateCache(key, key.defaultValue); } - Stream watchAppConfig() => _watchDomain(MetadataDomain.appConfig).distinct(); + Stream watchAppConfig() => _watchDomain(.appConfig).distinct(); - Stream watchSystemConfig() => _watchDomain(MetadataDomain.systemConfig).distinct(); + Stream watchSystemConfig() => _watchDomain(.systemConfig).distinct(); Stream _watchDomain(MetadataDomain domain) { final query = _db.select(_db.metadataEntity)..where((t) => t.key.like('${domain.prefix}.%')); @@ -100,7 +100,14 @@ extension on MetadataDomain { void rebuild(MetadataRepository repo) { switch (this) { case .appConfig: - repo._appConfig = .new(theme: .new(mode: repo._read(.themeMode))); + repo._appConfig = .new( + theme: .new( + mode: repo._read(.themeMode), + primaryColor: repo._read(.primaryColor), + dynamicTheme: repo._read(.dynamicTheme), + colorfulInterface: repo._read(.colorfulInterface), + ), + ); case .systemConfig: repo._systemConfig = .new(logLevel: repo._read(.logLevel)); } diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index c74b03c809..f1e3b61e52 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -24,6 +24,7 @@ import 'package:immich_mobile/platform/background_worker_lock_api.g.dart'; import 'package:immich_mobile/providers/app_life_cycle.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:immich_mobile/providers/locale_provider.dart'; import 'package:immich_mobile/providers/routes.provider.dart'; @@ -241,7 +242,7 @@ class ImmichAppState extends ConsumerState with WidgetsBindingObserve localizationsDelegates: context.localizationDelegates, supportedLocales: context.supportedLocales, locale: context.locale, - themeMode: ref.watch(immichThemeModeProvider), + themeMode: ref.watch(appConfigProvider.select((config) => config.theme.mode)), darkTheme: getThemeData(colorScheme: immichTheme.dark, locale: context.locale), theme: getThemeData(colorScheme: immichTheme.light, locale: context.locale), builder: (context, child) => ImmichTranslationProvider( diff --git a/mobile/lib/pages/common/splash_screen.page.dart b/mobile/lib/pages/common/splash_screen.page.dart index 725f7f9e85..512940aeb3 100644 --- a/mobile/lib/pages/common/splash_screen.page.dart +++ b/mobile/lib/pages/common/splash_screen.page.dart @@ -6,8 +6,8 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/colors.dart'; import 'package:immich_mobile/constants/locales.dart'; +import 'package:immich_mobile/domain/models/metadata_key.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'; @@ -35,7 +35,7 @@ class BootstrapErrorWidget extends StatelessWidget { @override Widget build(BuildContext _) { - final immichTheme = defaultColorPreset.themeOfPreset; + final immichTheme = MetadataKey.primaryColor.defaultValue.themeOfPreset; return EasyLocalization( supportedLocales: locales.values.toList(), diff --git a/mobile/lib/providers/infrastructure/metadata.provider.dart b/mobile/lib/providers/infrastructure/metadata.provider.dart index e6b2232b0e..46ff1069f9 100644 --- a/mobile/lib/providers/infrastructure/metadata.provider.dart +++ b/mobile/lib/providers/infrastructure/metadata.provider.dart @@ -3,7 +3,7 @@ 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((_) => MetadataRepository.instance); +final metadataProvider = Provider.autoDispose((_) => MetadataRepository.instance); final appConfigProvider = Provider.autoDispose((ref) { final repo = ref.watch(metadataProvider); diff --git a/mobile/lib/providers/theme.provider.dart b/mobile/lib/providers/theme.provider.dart index 2ea3e15c5d..909b8137c1 100644 --- a/mobile/lib/providers/theme.provider.dart +++ b/mobile/lib/providers/theme.provider.dart @@ -1,46 +1,17 @@ -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'; -import 'package:immich_mobile/utils/debug_print.dart'; - -final immichThemeModeProvider = StateProvider((ref) => ref.watch(appConfigProvider).theme.mode); - -final immichThemePresetProvider = StateProvider((ref) { - final appSettingsProvider = ref.watch(appSettingsServiceProvider); - final primaryColorPreset = appSettingsProvider.getSetting(AppSettingsEnum.primaryColor); - - dPrint(() => "Current theme preset $primaryColorPreset"); - - try { - return ImmichColorPreset.values.firstWhere((e) => e.name == primaryColorPreset); - } catch (e) { - dPrint(() => "Theme preset $primaryColorPreset not found. Applying default preset."); - appSettingsProvider.setSetting(AppSettingsEnum.primaryColor, defaultColorPresetName); - return defaultColorPreset; - } -}); - -final dynamicThemeSettingProvider = StateProvider((ref) { - return ref.watch(appSettingsServiceProvider).getSetting(AppSettingsEnum.dynamicTheme); -}); - -final colorfulInterfaceSettingProvider = StateProvider((ref) { - return ref.watch(appSettingsServiceProvider).getSetting(AppSettingsEnum.colorfulInterface); -}); // Provider for current selected theme final immichThemeProvider = StateProvider((ref) { - final primaryColorPreset = ref.read(immichThemePresetProvider); - final useSystemColor = ref.watch(dynamicThemeSettingProvider); - final useColorfulInterface = ref.watch(colorfulInterfaceSettingProvider); - final ImmichTheme? dynamicTheme = DynamicTheme.theme; - final currentTheme = (useSystemColor && dynamicTheme != null) ? dynamicTheme : primaryColorPreset.themeOfPreset; + final themeConfig = ref.watch(appConfigProvider.select((config) => config.theme)); - return useColorfulInterface ? currentTheme : decolorizeSurfaces(theme: currentTheme); + final ImmichTheme? dynamicTheme = DynamicTheme.theme; + final currentTheme = (themeConfig.dynamicTheme && dynamicTheme != null) + ? dynamicTheme + : themeConfig.primaryColor.themeOfPreset; + + return themeConfig.colorfulInterface ? currentTheme : decolorizeSurfaces(theme: currentTheme); }); diff --git a/mobile/lib/services/app_settings.service.dart b/mobile/lib/services/app_settings.service.dart index bd3d512edf..5c565aff03 100644 --- a/mobile/lib/services/app_settings.service.dart +++ b/mobile/lib/services/app_settings.service.dart @@ -1,13 +1,9 @@ -import 'package:immich_mobile/constants/colors.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; enum AppSettingsEnum { loadPreview(StoreKey.loadPreview, "loadPreview", true), loadOriginal(StoreKey.loadOriginal, "loadOriginal", false), - primaryColor(StoreKey.primaryColor, "primaryColor", defaultColorPresetName), - dynamicTheme(StoreKey.dynamicTheme, "dynamicTheme", 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 dcb6f5a44c..7b3cfa00dc 100644 --- a/mobile/lib/utils/migration.dart +++ b/mobile/lib/utils/migration.dart @@ -2,13 +2,13 @@ import 'dart:async'; import 'package:drift/drift.dart'; import 'package:flutter/material.dart'; +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'; @@ -40,37 +40,78 @@ Future _migrateTo25() async { } Future _migrateTo26(Drift drift) async { - final repo = MetadataRepository.instance; - final 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(); +} - 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); +class _StoreMigrator { + final Drift _db; + final Map, Object> _cache = {}; + final List _migratedStoreIds = []; + + _StoreMigrator(this._db); + + 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); } - 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); + 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); } - await _deleteLegacyStoreRows(drift, migrated); -} + Future migrateBool(StoreKey legacyKey, MetadataKey newKey) async { + final intValue = await _readLegacyStoreInt(legacyKey.id); + if (intValue == null) return; -Future _readLegacyStoreString(Drift drift, int id) async { - final row = await (drift.storeEntity.select()..where((t) => t.id.equals(id))).getSingleOrNull(); - return row?.stringValue; -} + final boolValue = intValue != 0; + _cache[newKey] = boolValue; + _migratedStoreIds.add(legacyKey.id); + } -Future _readLegacyStoreInt(Drift drift, int id) async { - final row = await (drift.storeEntity.select()..where((t) => t.id.equals(id))).getSingleOrNull(); - return row?.intValue; -} + 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 _deleteLegacyStoreRows(Drift drift, List ids) async { - if (ids.isEmpty) return; - await (drift.storeEntity.delete()..where((t) => t.id.isIn(ids))).go(); + 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/primary_color_setting.dart b/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart index 22c9154981..617a9a0758 100644 --- a/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart +++ b/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart @@ -1,15 +1,14 @@ 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/constants/colors.dart'; +import 'package:immich_mobile/domain/models/metadata_key.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_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/theme/color_scheme.dart'; import 'package:immich_mobile/theme/dynamic_theme.dart'; -import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; class PrimaryColorSetting extends HookConsumerWidget { const PrimaryColorSetting({super.key}); @@ -17,18 +16,10 @@ class PrimaryColorSetting extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final themeProvider = ref.read(immichThemeProvider); + final themeConfig = ref.watch(appConfigProvider.select((config) => config.theme)); - final primaryColorSetting = useAppSettingsState(AppSettingsEnum.primaryColor); - final systemPrimaryColorSetting = useAppSettingsState(AppSettingsEnum.dynamicTheme); - - final currentPreset = useValueNotifier(ref.read(immichThemePresetProvider)); const tileSize = 55.0; - useValueChanged( - primaryColorSetting.value, - (_, __) => currentPreset.value = ImmichColorPreset.values.firstWhere((e) => e.name == primaryColorSetting.value), - ); - void popBottomSheet() { Future.delayed(const Duration(milliseconds: 200), () { Navigator.pop(context); @@ -36,23 +27,18 @@ class PrimaryColorSetting extends HookConsumerWidget { } onUseSystemColorChange(bool newValue) { - systemPrimaryColorSetting.value = newValue; - ref.watch(dynamicThemeSettingProvider.notifier).state = newValue; - ref.invalidate(immichThemeProvider); + ref.read(metadataProvider).write(MetadataKey.dynamicTheme, newValue); popBottomSheet(); } onPrimaryColorChange(ImmichColorPreset colorPreset) { - primaryColorSetting.value = colorPreset.name; - ref.watch(immichThemePresetProvider.notifier).state = colorPreset; - ref.invalidate(immichThemeProvider); + ref.read(metadataProvider).write(MetadataKey.primaryColor, colorPreset); //turn off system color setting - if (systemPrimaryColorSetting.value) { - onUseSystemColorChange(false); - } else { - popBottomSheet(); + if (themeConfig.dynamicTheme) { + ref.read(metadataProvider).write(MetadataKey.dynamicTheme, false); } + popBottomSheet(); } buildPrimaryColorTile({ @@ -122,7 +108,7 @@ class PrimaryColorSetting extends HookConsumerWidget { 'theme_setting_system_primary_color_title'.tr(), style: context.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.w500, height: 1.5), ), - value: systemPrimaryColorSetting.value, + value: themeConfig.dynamicTheme, onChanged: onUseSystemColorChange, ), ), @@ -140,7 +126,7 @@ class PrimaryColorSetting extends HookConsumerWidget { topColor: theme.light.primary, bottomColor: theme.dark.primary, tileSize: tileSize, - showSelector: currentPreset.value == preset && !systemPrimaryColorSetting.value, + showSelector: themeConfig.primaryColor == preset && !themeConfig.dynamicTheme, ), ); }).toList(), diff --git a/mobile/lib/widgets/settings/preference_settings/theme_setting.dart b/mobile/lib/widgets/settings/preference_settings/theme_setting.dart index 05eb65f16e..a011252061 100644 --- a/mobile/lib/widgets/settings/preference_settings/theme_setting.dart +++ b/mobile/lib/widgets/settings/preference_settings/theme_setting.dart @@ -5,9 +5,6 @@ import 'package:immich_mobile/domain/models/metadata_key.dart'; 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'; @@ -17,26 +14,15 @@ class ThemeSetting extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final currentTheme = useState(ref.read(immichThemeModeProvider)); + final currentTheme = useState(ref.read(appConfigProvider.select((config) => config.theme.mode))); 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) { - if (isDark) { - ref.watch(immichThemeModeProvider.notifier).state = ThemeMode.dark; - currentTheme.value = ThemeMode.dark; - } else { - ref.watch(immichThemeModeProvider.notifier).state = ThemeMode.light; - currentTheme.value = ThemeMode.light; - } + currentTheme.value = isDark ? ThemeMode.dark : ThemeMode.light; ref.read(metadataProvider).write(MetadataKey.themeMode, currentTheme.value); } @@ -44,25 +30,22 @@ class ThemeSetting extends HookConsumerWidget { if (isSystem) { currentTheme.value = ThemeMode.system; isSystemTheme.value = true; - ref.watch(immichThemeModeProvider.notifier).state = ThemeMode.system; } else { final currentSystemBrightness = context.platformBrightness; isSystemTheme.value = false; isDarkTheme.value = currentSystemBrightness == Brightness.dark; if (currentSystemBrightness == Brightness.light) { currentTheme.value = ThemeMode.light; - ref.watch(immichThemeModeProvider.notifier).state = ThemeMode.light; } else if (currentSystemBrightness == Brightness.dark) { currentTheme.value = ThemeMode.dark; - ref.watch(immichThemeModeProvider.notifier).state = ThemeMode.dark; } } ref.read(metadataProvider).write(MetadataKey.themeMode, currentTheme.value); } 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 +68,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/domain/services/log_service_test.dart b/mobile/test/domain/services/log_service_test.dart index 820e43ad72..ee596f449e 100644 --- a/mobile/test/domain/services/log_service_test.dart +++ b/mobile/test/domain/services/log_service_test.dart @@ -40,7 +40,7 @@ void main() { when(() => mockLogRepo.truncate(limit: any(named: 'limit'))).thenAnswer((_) async => {}); when(() => mockMetadataRepository.systemConfig).thenReturn(const SystemConfig(logLevel: LogLevel.fine)); - when(() => mockMetadataRepository.write(MetadataKey.logLevel, any())).thenAnswer((_) async {}); + when(() => mockMetadataRepository.write(MetadataKey.logLevel, any())).thenAnswer((_) async {}); when(() => mockLogRepo.getAll()).thenAnswer((_) async => []); when(() => mockLogRepo.insert(any())).thenAnswer((_) async => true); when(() => mockLogRepo.insertAll(any())).thenAnswer((_) async => true); @@ -71,7 +71,7 @@ void main() { test('Updates the log level via metadata repository', () { final captured = verify( - () => mockMetadataRepository.write(MetadataKey.logLevel, captureAny()), + () => mockMetadataRepository.write(MetadataKey.logLevel, captureAny()), ).captured.firstOrNull; expect(captured, LogLevel.shout); }); diff --git a/mobile/test/infrastructure/repositories/store_repository_test.dart b/mobile/test/infrastructure/repositories/store_repository_test.dart index 4cf1adc6b1..806cde9b75 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 { @@ -165,14 +165,14 @@ void main() { [ const StoreDto(StoreKey.version, _kTestVersion), StoreDto(StoreKey.backupFailedSince, _kTestBackupFailed), + const StoreDto(StoreKey.backupRequireWifi, _kTestBackupRequireWifi), const StoreDto(StoreKey.accessToken, _kTestAccessToken), - const StoreDto(StoreKey.colorfulInterface, _kTestColorfulInterface), ], [ const StoreDto(StoreKey.version, _kTestVersion + 10), StoreDto(StoreKey.backupFailedSince, _kTestBackupFailed), + const StoreDto(StoreKey.backupRequireWifi, _kTestBackupRequireWifi), const StoreDto(StoreKey.accessToken, _kTestAccessToken), - const StoreDto(StoreKey.colorfulInterface, _kTestColorfulInterface), ], ]), ),