migrate primary color

This commit is contained in:
shenlong-tanwen
2026-05-03 21:16:46 +07:00
parent 6fdb841732
commit 68a718fab4
12 changed files with 41 additions and 46 deletions
-3
View File
@@ -2,9 +2,6 @@ import 'package:flutter/material.dart';
enum ImmichColorPreset { indigo, deepPurple, pink, red, orange, yellow, lime, green, cyan, slateGray } 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 immichBrandColorLight = Color(0xFF4150AF);
const Color immichBrandColorDark = Color(0xFFACCBFA); const Color immichBrandColorDark = Color(0xFFACCBFA);
const Color whiteOpacity75 = Color.fromRGBO(255, 255, 255, 0.75); const Color whiteOpacity75 = Color.fromRGBO(255, 255, 255, 0.75);
@@ -1,18 +1,22 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:immich_mobile/constants/colors.dart';
class ThemeConfig { class ThemeConfig {
final ThemeMode mode; 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 @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 @override
int get hashCode => mode.hashCode; int get hashCode => Object.hash(mode, primaryColor);
@override @override
String toString() => 'ThemeConfig(mode: $mode)'; String toString() => 'ThemeConfig(mode: $mode, primaryColor: $primaryColor)';
} }
@@ -1,5 +1,6 @@
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.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/app_config.dart';
import 'package:immich_mobile/domain/models/config/system_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/log.model.dart';
@@ -13,7 +14,11 @@ enum MetadataDomain<T extends Object> {
} }
enum MetadataKey<T extends Object> { enum MetadataKey<T extends Object> {
// Theme
primaryColor<ImmichColorPreset>(.appConfig, 'theme.primaryColor', .indigo, _EnumCodec(ImmichColorPreset.values)),
themeMode<ThemeMode>(.appConfig, 'theme.mode', .system, _EnumCodec(ThemeMode.values)), themeMode<ThemeMode>(.appConfig, 'theme.mode', .system, _EnumCodec(ThemeMode.values)),
// Log
logLevel<LogLevel>(.systemConfig, 'log.level', .info, _EnumCodec(LogLevel.values)); logLevel<LogLevel>(.systemConfig, 'log.level', .info, _EnumCodec(LogLevel.values));
final MetadataDomain domain; final MetadataDomain domain;
+1 -1
View File
@@ -49,7 +49,6 @@ enum StoreKey<T> {
customHeaders<String>._(127), customHeaders<String>._(127),
// theme settings // theme settings
primaryColor<String>._(128),
dynamicTheme<bool>._(129), dynamicTheme<bool>._(129),
colorfulInterface<bool>._(130), colorfulInterface<bool>._(130),
@@ -95,6 +94,7 @@ enum StoreKey<T> {
syncMigrationStatus<String>._(1013), syncMigrationStatus<String>._(1013),
// Legacy keys that have been migrated to the new metadata store // Legacy keys that have been migrated to the new metadata store
legacyPrimaryColor<String>._(128),
legacyThemeMode<String>._(102), legacyThemeMode<String>._(102),
legacyLogLevel<int>._(115); legacyLogLevel<int>._(115);
@@ -47,7 +47,7 @@ class MetadataRepository extends DriftDatabaseRepository {
T _read<T extends Object>(MetadataKey<T> key) => (_cache[key] as T?) ?? key.defaultValue; T _read<T extends Object>(MetadataKey<T> key) => (_cache[key] as T?) ?? key.defaultValue;
Future<void> write<T extends Object>(MetadataKey<T> key, T value) async { Future<void> write<T extends Object, U extends T>(MetadataKey<T> key, U value) async {
if (_read(key) == value) return; if (_read(key) == value) return;
await _db await _db
@@ -100,7 +100,9 @@ extension<T extends Object> on MetadataDomain<T> {
void rebuild(MetadataRepository repo) { void rebuild(MetadataRepository repo) {
switch (this) { switch (this) {
case .appConfig: 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: case .systemConfig:
repo._systemConfig = .new(logLevel: repo._read(.logLevel)); repo._systemConfig = .new(logLevel: repo._read(.logLevel));
} }
@@ -6,8 +6,8 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/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/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/generated/codegen_loader.g.dart'; import 'package:immich_mobile/generated/codegen_loader.g.dart';
@@ -35,7 +35,7 @@ class BootstrapErrorWidget extends StatelessWidget {
@override @override
Widget build(BuildContext _) { Widget build(BuildContext _) {
final immichTheme = defaultColorPreset.themeOfPreset; final immichTheme = MetadataKey.primaryColor.defaultValue.themeOfPreset;
return EasyLocalization( return EasyLocalization(
supportedLocales: locales.values.toList(), supportedLocales: locales.values.toList(),
@@ -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/domain/models/config/system_config.dart';
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart';
final metadataProvider = Provider<MetadataRepository>((_) => MetadataRepository.instance); final metadataProvider = Provider.autoDispose<MetadataRepository>((_) => MetadataRepository.instance);
final appConfigProvider = Provider.autoDispose<AppConfig>((ref) { final appConfigProvider = Provider.autoDispose<AppConfig>((ref) {
final repo = ref.watch(metadataProvider); final repo = ref.watch(metadataProvider);
+4 -16
View File
@@ -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/color_scheme.dart';
import 'package:immich_mobile/theme/dynamic_theme.dart'; import 'package:immich_mobile/theme/dynamic_theme.dart';
import 'package:immich_mobile/theme/theme_data.dart'; import 'package:immich_mobile/theme/theme_data.dart';
import 'package:immich_mobile/utils/debug_print.dart';
final immichThemeModeProvider = StateProvider<ThemeMode>((ref) => ref.watch(appConfigProvider).theme.mode); final immichThemeModeProvider = StateProvider<ThemeMode>((ref) => ref.watch(appConfigProvider).theme.mode);
final immichThemePresetProvider = StateProvider<ImmichColorPreset>((ref) { final immichThemePresetProvider = StateProvider<ImmichColorPreset>(
final appSettingsProvider = ref.watch(appSettingsServiceProvider); (ref) => ref.watch(appConfigProvider.select((config) => config.theme.primaryColor)),
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<bool>((ref) { final dynamicThemeSettingProvider = StateProvider<bool>((ref) {
return ref.watch(appSettingsServiceProvider).getSetting(AppSettingsEnum.dynamicTheme); return ref.watch(appSettingsServiceProvider).getSetting(AppSettingsEnum.dynamicTheme);
@@ -36,7 +24,7 @@ final colorfulInterfaceSettingProvider = StateProvider<bool>((ref) {
// Provider for current selected theme // Provider for current selected theme
final immichThemeProvider = StateProvider<ImmichTheme>((ref) { final immichThemeProvider = StateProvider<ImmichTheme>((ref) {
final primaryColorPreset = ref.read(immichThemePresetProvider); final primaryColorPreset = ref.watch(immichThemePresetProvider);
final useSystemColor = ref.watch(dynamicThemeSettingProvider); final useSystemColor = ref.watch(dynamicThemeSettingProvider);
final useColorfulInterface = ref.watch(colorfulInterfaceSettingProvider); final useColorfulInterface = ref.watch(colorfulInterfaceSettingProvider);
final ImmichTheme? dynamicTheme = DynamicTheme.theme; final ImmichTheme? dynamicTheme = DynamicTheme.theme;
@@ -1,11 +1,9 @@
import 'package:immich_mobile/constants/colors.dart';
import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart';
enum AppSettingsEnum<T> { enum AppSettingsEnum<T> {
loadPreview<bool>(StoreKey.loadPreview, "loadPreview", true), loadPreview<bool>(StoreKey.loadPreview, "loadPreview", true),
loadOriginal<bool>(StoreKey.loadOriginal, "loadOriginal", false), loadOriginal<bool>(StoreKey.loadOriginal, "loadOriginal", false),
primaryColor<String>(StoreKey.primaryColor, "primaryColor", defaultColorPresetName),
dynamicTheme<bool>(StoreKey.dynamicTheme, "dynamicTheme", false), dynamicTheme<bool>(StoreKey.dynamicTheme, "dynamicTheme", false),
colorfulInterface<bool>(StoreKey.colorfulInterface, "colorfulInterface", true), colorfulInterface<bool>(StoreKey.colorfulInterface, "colorfulInterface", true),
tilesPerRow<int>(StoreKey.tilesPerRow, "tilesPerRow", 4), tilesPerRow<int>(StoreKey.tilesPerRow, "tilesPerRow", 4),
+8
View File
@@ -2,6 +2,7 @@ import 'dart:async';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:flutter/material.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/log.model.dart';
import 'package:immich_mobile/domain/models/metadata_key.dart'; import 'package:immich_mobile/domain/models/metadata_key.dart';
import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart';
@@ -57,6 +58,13 @@ Future<void> _migrateTo26(Drift drift) async {
migrated.add(StoreKey.legacyLogLevel.id); 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); await _deleteLegacyStoreRows(drift, migrated);
} }
@@ -1,10 +1,11 @@
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/colors.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/build_context_extensions.dart';
import 'package:immich_mobile/extensions/theme_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/providers/theme.provider.dart';
import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/theme/color_scheme.dart'; import 'package:immich_mobile/theme/color_scheme.dart';
@@ -18,17 +19,11 @@ class PrimaryColorSetting extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final themeProvider = ref.read(immichThemeProvider); 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 systemPrimaryColorSetting = useAppSettingsState(AppSettingsEnum.dynamicTheme);
final currentPreset = useValueNotifier(ref.read(immichThemePresetProvider));
const tileSize = 55.0; const tileSize = 55.0;
useValueChanged(
primaryColorSetting.value,
(_, __) => currentPreset.value = ImmichColorPreset.values.firstWhere((e) => e.name == primaryColorSetting.value),
);
void popBottomSheet() { void popBottomSheet() {
Future.delayed(const Duration(milliseconds: 200), () { Future.delayed(const Duration(milliseconds: 200), () {
Navigator.pop(context); Navigator.pop(context);
@@ -43,9 +38,7 @@ class PrimaryColorSetting extends HookConsumerWidget {
} }
onPrimaryColorChange(ImmichColorPreset colorPreset) { onPrimaryColorChange(ImmichColorPreset colorPreset) {
primaryColorSetting.value = colorPreset.name; ref.read(metadataProvider).write(MetadataKey.primaryColor, colorPreset);
ref.watch(immichThemePresetProvider.notifier).state = colorPreset;
ref.invalidate(immichThemeProvider);
//turn off system color setting //turn off system color setting
if (systemPrimaryColorSetting.value) { if (systemPrimaryColorSetting.value) {
@@ -140,7 +133,7 @@ class PrimaryColorSetting extends HookConsumerWidget {
topColor: theme.light.primary, topColor: theme.light.primary,
bottomColor: theme.dark.primary, bottomColor: theme.dark.primary,
tileSize: tileSize, tileSize: tileSize,
showSelector: currentPreset.value == preset && !systemPrimaryColorSetting.value, showSelector: currentPreset == preset && !systemPrimaryColorSetting.value,
), ),
); );
}).toList(), }).toList(),
@@ -40,7 +40,7 @@ void main() {
when(() => mockLogRepo.truncate(limit: any(named: 'limit'))).thenAnswer((_) async => {}); when(() => mockLogRepo.truncate(limit: any(named: 'limit'))).thenAnswer((_) async => {});
when(() => mockMetadataRepository.systemConfig).thenReturn(const SystemConfig(logLevel: LogLevel.fine)); when(() => mockMetadataRepository.systemConfig).thenReturn(const SystemConfig(logLevel: LogLevel.fine));
when(() => mockMetadataRepository.write<LogLevel>(MetadataKey.logLevel, any())).thenAnswer((_) async {}); when(() => mockMetadataRepository.write<LogLevel, LogLevel>(MetadataKey.logLevel, any())).thenAnswer((_) async {});
when(() => mockLogRepo.getAll()).thenAnswer((_) async => []); when(() => mockLogRepo.getAll()).thenAnswer((_) async => []);
when(() => mockLogRepo.insert(any())).thenAnswer((_) async => true); when(() => mockLogRepo.insert(any())).thenAnswer((_) async => true);
when(() => mockLogRepo.insertAll(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', () { test('Updates the log level via metadata repository', () {
final captured = verify( final captured = verify(
() => mockMetadataRepository.write<LogLevel>(MetadataKey.logLevel, captureAny()), () => mockMetadataRepository.write<LogLevel, LogLevel>(MetadataKey.logLevel, captureAny()),
).captured.firstOrNull; ).captured.firstOrNull;
expect(captured, LogLevel.shout); expect(captured, LogLevel.shout);
}); });