migrate colorfulInterface

This commit is contained in:
shenlong-tanwen
2026-05-03 22:07:12 +07:00
parent ca95fbb47d
commit 296b696be9
9 changed files with 117 additions and 87 deletions
@@ -5,13 +5,25 @@ class ThemeConfig {
final ThemeMode mode; final ThemeMode mode;
final ImmichColorPreset primaryColor; final ImmichColorPreset primaryColor;
final bool dynamicTheme; 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, mode: mode ?? this.mode,
primaryColor: primaryColor ?? this.primaryColor, primaryColor: primaryColor ?? this.primaryColor,
dynamicTheme: dynamicTheme ?? this.dynamicTheme, dynamicTheme: dynamicTheme ?? this.dynamicTheme,
colorfulInterface: colorfulInterface ?? this.colorfulInterface,
); );
@override @override
@@ -20,11 +32,13 @@ class ThemeConfig {
(other is ThemeConfig && (other is ThemeConfig &&
other.mode == mode && other.mode == mode &&
other.primaryColor == primaryColor && other.primaryColor == primaryColor &&
other.dynamicTheme == dynamicTheme); other.dynamicTheme == dynamicTheme &&
other.colorfulInterface == colorfulInterface);
@override @override
int get hashCode => Object.hash(mode, primaryColor, dynamicTheme); int get hashCode => Object.hash(mode, primaryColor, dynamicTheme, colorfulInterface);
@override @override
String toString() => 'ThemeConfig(mode: $mode, primaryColor: $primaryColor, dynamicTheme: $dynamicTheme)'; String toString() =>
'ThemeConfig(mode: $mode, primaryColor: $primaryColor, dynamicTheme: $dynamicTheme, colorfulInterface: $colorfulInterface)';
} }
+2 -1
View File
@@ -17,7 +17,8 @@ enum MetadataKey<T extends Object> {
// Theme // Theme
primaryColor<ImmichColorPreset>(.appConfig, 'theme.primaryColor', .indigo, _EnumCodec(ImmichColorPreset.values)), 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)),
dynamicTheme<bool>(.appConfig, 'dynamicTheme', false), dynamicTheme<bool>(.appConfig, 'theme.dynamicTheme', false),
colorfulInterface<bool>(.appConfig, 'theme.colorfulInterface', true),
// Log // Log
logLevel<LogLevel>(.systemConfig, 'log.level', .info, _EnumCodec(LogLevel.values)); logLevel<LogLevel>(.systemConfig, 'log.level', .info, _EnumCodec(LogLevel.values));
+1 -3
View File
@@ -48,9 +48,6 @@ enum StoreKey<T> {
enableHapticFeedback<bool>._(126), enableHapticFeedback<bool>._(126),
customHeaders<String>._(127), customHeaders<String>._(127),
// theme settings
colorfulInterface<bool>._(130),
syncAlbums<bool>._(131), syncAlbums<bool>._(131),
// Auto endpoint switching // Auto endpoint switching
@@ -95,6 +92,7 @@ enum StoreKey<T> {
// 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), legacyPrimaryColor<String>._(128),
legacyDynamicTheme<bool>._(129), legacyDynamicTheme<bool>._(129),
legacyColorfulInterface<bool>._(130),
legacyThemeMode<String>._(102), legacyThemeMode<String>._(102),
legacyLogLevel<int>._(115); legacyLogLevel<int>._(115);
@@ -105,6 +105,7 @@ extension<T extends Object> on MetadataDomain<T> {
mode: repo._read(.themeMode), mode: repo._read(.themeMode),
primaryColor: repo._read(.primaryColor), primaryColor: repo._read(.primaryColor),
dynamicTheme: repo._read(.dynamicTheme), dynamicTheme: repo._read(.dynamicTheme),
colorfulInterface: repo._read(.colorfulInterface),
), ),
); );
case .systemConfig: case .systemConfig:
+3 -5
View File
@@ -1,9 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.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/providers/app_settings.provider.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.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/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';
@@ -18,9 +16,9 @@ final dynamicThemeSettingProvider = StateProvider<bool>(
(ref) => ref.watch(appConfigProvider.select((config) => config.theme.dynamicTheme)), (ref) => ref.watch(appConfigProvider.select((config) => config.theme.dynamicTheme)),
); );
final colorfulInterfaceSettingProvider = StateProvider<bool>((ref) { final colorfulInterfaceSettingProvider = StateProvider<bool>(
return ref.watch(appSettingsServiceProvider).getSetting(AppSettingsEnum.colorfulInterface); (ref) => ref.watch(appConfigProvider.select((config) => config.theme.colorfulInterface)),
}); );
// Provider for current selected theme // Provider for current selected theme
final immichThemeProvider = StateProvider<ImmichTheme>((ref) { final immichThemeProvider = StateProvider<ImmichTheme>((ref) {
@@ -4,7 +4,6 @@ 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),
colorfulInterface<bool>(StoreKey.colorfulInterface, "colorfulInterface", true),
tilesPerRow<int>(StoreKey.tilesPerRow, "tilesPerRow", 4), tilesPerRow<int>(StoreKey.tilesPerRow, "tilesPerRow", 4),
dynamicLayout<bool>(StoreKey.dynamicLayout, "dynamicLayout", false), dynamicLayout<bool>(StoreKey.dynamicLayout, "dynamicLayout", false),
groupAssetsBy<int>(StoreKey.groupAssetsBy, "groupBy", 0), groupAssetsBy<int>(StoreKey.groupAssetsBy, "groupBy", 0),
+71 -45
View File
@@ -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/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';
import 'package:immich_mobile/domain/services/log.service.dart';
import 'package:immich_mobile/entities/store.entity.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/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/infrastructure/repositories/network.repository.dart';
import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/api.service.dart';
@@ -41,51 +40,78 @@ Future<void> _migrateTo25() async {
} }
Future<void> _migrateTo26(Drift drift) async { Future<void> _migrateTo26(Drift drift) async {
final repo = MetadataRepository.instance; final migrator = _StoreMigrator(drift);
final migrated = <int>[]; await migrator.migrateEnumName(StoreKey.legacyThemeMode, MetadataKey.themeMode, ThemeMode.values);
await migrator.migrateEnumIndex(StoreKey.legacyLogLevel, MetadataKey.logLevel, LogLevel.values);
final themeMode = await _readLegacyStoreString(drift, StoreKey.legacyThemeMode.id); await migrator.migrateEnumName(StoreKey.legacyPrimaryColor, MetadataKey.primaryColor, ImmichColorPreset.values);
if (themeMode != null) { await migrator.migrateBool(StoreKey.legacyDynamicTheme, MetadataKey.dynamicTheme);
final mode = ThemeMode.values.firstWhere((m) => m.name == themeMode, orElse: () => ThemeMode.system); await migrator.migrateBool(StoreKey.legacyColorfulInterface, MetadataKey.colorfulInterface);
await repo.write(MetadataKey.themeMode, mode); await migrator.complete();
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);
} }
Future<String?> _readLegacyStoreString(Drift drift, int id) async { class _StoreMigrator {
final row = await (drift.storeEntity.select()..where((t) => t.id.equals(id))).getSingleOrNull(); final Drift _db;
return row?.stringValue; final Map<MetadataKey<Object>, Object> _cache = {};
} final List<int> _migratedStoreIds = [];
Future<int?> _readLegacyStoreInt(Drift drift, int id) async { _StoreMigrator(this._db);
final row = await (drift.storeEntity.select()..where((t) => t.id.equals(id))).getSingleOrNull();
return row?.intValue;
}
Future<void> _deleteLegacyStoreRows(Drift drift, List<int> ids) async { Future<void> migrateEnumIndex<T extends Enum>(StoreKey<int> legacyKey, MetadataKey<T> newKey, List<T> values) async {
if (ids.isEmpty) return; final index = await _readLegacyStoreInt(legacyKey.id);
await (drift.storeEntity.delete()..where((t) => t.id.isIn(ids))).go(); if (index == null) return;
final enumValue = values.elementAtOrNull(index) ?? newKey.defaultValue;
_cache[newKey] = enumValue;
_migratedStoreIds.add(legacyKey.id);
}
Future<void> migrateEnumName<T extends Enum>(
StoreKey<String> legacyKey,
MetadataKey<T> newKey,
List<T> 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<void> migrateBool(StoreKey<bool> legacyKey, MetadataKey<bool> newKey) async {
final intValue = await _readLegacyStoreInt(legacyKey.id);
if (intValue == null) return;
final boolValue = intValue != 0;
_cache[newKey] = boolValue;
_migratedStoreIds.add(legacyKey.id);
}
Future<void> 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<String?> _readLegacyStoreString(int id) async {
final row = await (_db.storeEntity.select()..where((t) => t.id.equals(id))).getSingleOrNull();
return row?.stringValue;
}
Future<int?> _readLegacyStoreInt(int id) async {
final row = await (_db.storeEntity.select()..where((t) => t.id.equals(id))).getSingleOrNull();
return row?.intValue;
}
Future<void> _deleteLegacyStoreRows(List<int> ids) async {
if (ids.isEmpty) return;
await (_db.storeEntity.delete()..where((t) => t.id.isIn(ids))).go();
}
} }
@@ -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/extensions/translate_extensions.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.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/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/preference_settings/primary_color_setting.dart';
import 'package:immich_mobile/widgets/settings/setting_group_title.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/widgets/settings/settings_switch_list_tile.dart';
@@ -20,13 +18,8 @@ class ThemeSetting extends HookConsumerWidget {
final currentTheme = useState(ref.read(immichThemeModeProvider)); final currentTheme = useState(ref.read(immichThemeModeProvider));
final isDarkTheme = useValueNotifier(currentTheme.value == ThemeMode.dark); final isDarkTheme = useValueNotifier(currentTheme.value == ThemeMode.dark);
final isSystemTheme = useValueNotifier(currentTheme.value == ThemeMode.system); final isSystemTheme = useValueNotifier(currentTheme.value == ThemeMode.system);
final colorfulInterface = useValueNotifier(
final applyThemeToBackgroundSetting = useAppSettingsState(AppSettingsEnum.colorfulInterface); ref.watch(appConfigProvider.select((config) => config.theme.colorfulInterface)),
final applyThemeToBackgroundProvider = useValueNotifier(ref.read(colorfulInterfaceSettingProvider));
useValueChanged(
applyThemeToBackgroundSetting.value,
(_, __) => applyThemeToBackgroundProvider.value = applyThemeToBackgroundSetting.value,
); );
void onThemeChange(bool isDark) { void onThemeChange(bool isDark) {
@@ -61,8 +54,8 @@ class ThemeSetting extends HookConsumerWidget {
} }
void onSurfaceColorSettingChange(bool useColorfulInterface) { void onSurfaceColorSettingChange(bool useColorfulInterface) {
applyThemeToBackgroundSetting.value = useColorfulInterface; ref.read(metadataProvider).write(MetadataKey.colorfulInterface, useColorfulInterface);
ref.watch(colorfulInterfaceSettingProvider.notifier).state = useColorfulInterface; colorfulInterface.value = useColorfulInterface;
} }
return Column( return Column(
@@ -85,7 +78,7 @@ class ThemeSetting extends HookConsumerWidget {
), ),
const PrimaryColorSetting(), const PrimaryColorSetting(),
SettingsSwitchListTile( SettingsSwitchListTile(
valueNotifier: applyThemeToBackgroundProvider, valueNotifier: colorfulInterface,
title: "theme_setting_colorful_interface_title".t(context: context), title: "theme_setting_colorful_interface_title".t(context: context),
subtitle: 'theme_setting_colorful_interface_subtitle'.t(context: context), subtitle: 'theme_setting_colorful_interface_subtitle'.t(context: context),
onChanged: onSurfaceColorSettingChange, onChanged: onSurfaceColorSettingChange,
@@ -14,7 +14,7 @@ import '../../fixtures/user.stub.dart';
const _kTestAccessToken = "#TestToken"; const _kTestAccessToken = "#TestToken";
final _kTestBackupFailed = DateTime(2025, 2, 20, 11, 45); final _kTestBackupFailed = DateTime(2025, 2, 20, 11, 45);
const _kTestVersion = 10; const _kTestVersion = 10;
const _kTestColorfulInterface = false; const _kTestBackupRequireWifi = false;
final _kTestUser = UserStub.admin; final _kTestUser = UserStub.admin;
Future<void> _populateStore(Drift db) async { Future<void> _populateStore(Drift db) async {
@@ -22,8 +22,8 @@ Future<void> _populateStore(Drift db) async {
batch.insert( batch.insert(
db.storeEntity, db.storeEntity,
StoreEntityCompanion( StoreEntityCompanion(
id: Value(StoreKey.colorfulInterface.id), id: Value(StoreKey.backupRequireWifi.id),
intValue: const Value(_kTestColorfulInterface ? 1 : 0), intValue: const Value(_kTestBackupRequireWifi ? 1 : 0),
stringValue: const Value(null), stringValue: const Value(null),
), ),
); );
@@ -93,11 +93,11 @@ void main() {
}); });
test('converts bool', () async { test('converts bool', () async {
bool? colorfulInterface = await sut.tryGet(StoreKey.colorfulInterface); bool? backupRequireWifi = await sut.tryGet(StoreKey.backupRequireWifi);
expect(colorfulInterface, isNull); expect(backupRequireWifi, isNull);
await sut.upsert(StoreKey.colorfulInterface, _kTestColorfulInterface); await sut.upsert(StoreKey.backupRequireWifi, _kTestBackupRequireWifi);
colorfulInterface = await sut.tryGet(StoreKey.colorfulInterface); backupRequireWifi = await sut.tryGet(StoreKey.backupRequireWifi);
expect(colorfulInterface, _kTestColorfulInterface); expect(backupRequireWifi, _kTestBackupRequireWifi);
}); });
test('converts user', () async { test('converts user', () async {
@@ -115,11 +115,11 @@ void main() {
}); });
test('delete()', () async { test('delete()', () async {
bool? isColorful = await sut.tryGet(StoreKey.colorfulInterface); bool? backupRequireWifi = await sut.tryGet(StoreKey.backupRequireWifi);
expect(isColorful, isFalse); expect(backupRequireWifi, isFalse);
await sut.delete(StoreKey.colorfulInterface); await sut.delete(StoreKey.backupRequireWifi);
isColorful = await sut.tryGet(StoreKey.colorfulInterface); backupRequireWifi = await sut.tryGet(StoreKey.backupRequireWifi);
expect(isColorful, isNull); expect(backupRequireWifi, isNull);
}); });
test('deleteAll()', () async { test('deleteAll()', () async {
@@ -166,13 +166,13 @@ void main() {
const StoreDto<Object>(StoreKey.version, _kTestVersion), const StoreDto<Object>(StoreKey.version, _kTestVersion),
StoreDto<Object>(StoreKey.backupFailedSince, _kTestBackupFailed), StoreDto<Object>(StoreKey.backupFailedSince, _kTestBackupFailed),
const StoreDto<Object>(StoreKey.accessToken, _kTestAccessToken), const StoreDto<Object>(StoreKey.accessToken, _kTestAccessToken),
const StoreDto<Object>(StoreKey.colorfulInterface, _kTestColorfulInterface), const StoreDto<Object>(StoreKey.backupRequireWifi, _kTestBackupRequireWifi),
], ],
[ [
const StoreDto<Object>(StoreKey.version, _kTestVersion + 10), const StoreDto<Object>(StoreKey.version, _kTestVersion + 10),
StoreDto<Object>(StoreKey.backupFailedSince, _kTestBackupFailed), StoreDto<Object>(StoreKey.backupFailedSince, _kTestBackupFailed),
const StoreDto<Object>(StoreKey.accessToken, _kTestAccessToken), const StoreDto<Object>(StoreKey.accessToken, _kTestAccessToken),
const StoreDto<Object>(StoreKey.colorfulInterface, _kTestColorfulInterface), const StoreDto<Object>(StoreKey.backupRequireWifi, _kTestBackupRequireWifi),
], ],
]), ]),
), ),