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..df090e9dc7 100644 --- a/mobile/lib/domain/models/config/theme_config.dart +++ b/mobile/lib/domain/models/config/theme_config.dart @@ -1,18 +1,22 @@ import 'package:flutter/material.dart'; +import 'package:immich_mobile/constants/colors.dart'; class ThemeConfig { final ThemeMode mode; + final ImmichColorPreset primaryColor; - const ThemeConfig({this.mode = .system}); + const ThemeConfig({this.mode = .system, this.primaryColor = .indigo}); - ThemeConfig copyWith({ThemeMode? mode}) => .new(mode: mode ?? this.mode); + ThemeConfig copyWith({ThemeMode? mode, ImmichColorPreset? primaryColor}) => + .new(mode: mode ?? this.mode, primaryColor: primaryColor ?? this.primaryColor); @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); @override - int get hashCode => mode.hashCode; + int get hashCode => Object.hash(mode, primaryColor); @override - String toString() => 'ThemeConfig(mode: $mode)'; + String toString() => 'ThemeConfig(mode: $mode, primaryColor: $primaryColor)'; } diff --git a/mobile/lib/domain/models/metadata_key.dart b/mobile/lib/domain/models/metadata_key.dart index 2e122e6c5d..943a86a3b5 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,11 @@ enum MetadataDomain { } enum MetadataKey { + // Theme + primaryColor(.appConfig, 'theme.primaryColor', .indigo, _EnumCodec(ImmichColorPreset.values)), themeMode(.appConfig, 'theme.mode', .system, _EnumCodec(ThemeMode.values)), + + // 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..5d47c05f8f 100644 --- a/mobile/lib/domain/models/store.model.dart +++ b/mobile/lib/domain/models/store.model.dart @@ -49,7 +49,6 @@ enum StoreKey { customHeaders._(127), // theme settings - primaryColor._(128), dynamicTheme._(129), colorfulInterface._(130), @@ -95,6 +94,7 @@ enum StoreKey { syncMigrationStatus._(1013), // Legacy keys that have been migrated to the new metadata store + legacyPrimaryColor._(128), legacyThemeMode._(102), legacyLogLevel._(115); diff --git a/mobile/lib/infrastructure/repositories/metadata.repository.dart b/mobile/lib/infrastructure/repositories/metadata.repository.dart index d0e0ab2501..2b0704e35b 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 @@ -100,7 +100,9 @@ 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)), + ); case .systemConfig: repo._systemConfig = .new(logLevel: repo._read(.logLevel)); } 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..df6a24e902 100644 --- a/mobile/lib/providers/theme.provider.dart +++ b/mobile/lib/providers/theme.provider.dart @@ -7,24 +7,12 @@ 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 immichThemePresetProvider = StateProvider( + (ref) => ref.watch(appConfigProvider.select((config) => config.theme.primaryColor)), +); final dynamicThemeSettingProvider = StateProvider((ref) { return ref.watch(appSettingsServiceProvider).getSetting(AppSettingsEnum.dynamicTheme); @@ -36,7 +24,7 @@ final colorfulInterfaceSettingProvider = StateProvider((ref) { // Provider for current selected theme final immichThemeProvider = StateProvider((ref) { - final primaryColorPreset = ref.read(immichThemePresetProvider); + final primaryColorPreset = ref.watch(immichThemePresetProvider); final useSystemColor = ref.watch(dynamicThemeSettingProvider); final useColorfulInterface = ref.watch(colorfulInterfaceSettingProvider); final ImmichTheme? dynamicTheme = DynamicTheme.theme; diff --git a/mobile/lib/services/app_settings.service.dart b/mobile/lib/services/app_settings.service.dart index bd3d512edf..1f3bf9c719 100644 --- a/mobile/lib/services/app_settings.service.dart +++ b/mobile/lib/services/app_settings.service.dart @@ -1,11 +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), diff --git a/mobile/lib/utils/migration.dart b/mobile/lib/utils/migration.dart index dcb6f5a44c..377a92e21e 100644 --- a/mobile/lib/utils/migration.dart +++ b/mobile/lib/utils/migration.dart @@ -2,6 +2,7 @@ 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'; @@ -57,6 +58,13 @@ Future _migrateTo26(Drift drift) async { 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); + } + await _deleteLegacyStoreRows(drift, migrated); } 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..67a723591a 100644 --- a/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart +++ b/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart @@ -1,10 +1,11 @@ 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'; @@ -18,17 +19,11 @@ class PrimaryColorSetting extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final themeProvider = ref.read(immichThemeProvider); - final primaryColorSetting = useAppSettingsState(AppSettingsEnum.primaryColor); + final currentPreset = ref.watch(appConfigProvider.select((config) => config.theme.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); @@ -43,9 +38,7 @@ class PrimaryColorSetting extends HookConsumerWidget { } 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) { @@ -140,7 +133,7 @@ class PrimaryColorSetting extends HookConsumerWidget { topColor: theme.light.primary, bottomColor: theme.dark.primary, tileSize: tileSize, - showSelector: currentPreset.value == preset && !systemPrimaryColorSetting.value, + showSelector: currentPreset == preset && !systemPrimaryColorSetting.value, ), ); }).toList(), 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); });