You've already forked immich
mirror of
https://github.com/immich-app/immich.git
synced 2025-07-17 15:47:54 +02:00
feat(mobile): add support for material themes (#11560)
* feat(mobile): add support for material themes Added support for custom theming and updated all elements accordingly. * fix(mobile): Restored immich brand colors to default theme * fix(mobile): make ListTile titles bold in settings main page * feat(mobile): update bottom nav and appbar colors * small tweaks --------- Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
@ -15,6 +15,9 @@ class PreferenceSetting extends StatelessWidget {
|
||||
HapticSetting(),
|
||||
];
|
||||
|
||||
return const SettingsSubPageScaffold(settings: preferenceSettings);
|
||||
return const SettingsSubPageScaffold(
|
||||
settings: preferenceSettings,
|
||||
showDivider: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,221 @@
|
||||
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/immich_colors.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/utils/immich_app_theme.dart';
|
||||
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
|
||||
|
||||
class PrimaryColorSetting extends HookConsumerWidget {
|
||||
const PrimaryColorSetting({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final themeProvider = ref.read(immichThemeProvider);
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
onUseSystemColorChange(bool newValue) {
|
||||
systemPrimaryColorSetting.value = newValue;
|
||||
ref.watch(dynamicThemeSettingProvider.notifier).state = newValue;
|
||||
ref.invalidate(immichThemeProvider);
|
||||
popBottomSheet();
|
||||
}
|
||||
|
||||
onPrimaryColorChange(ImmichColorPreset colorPreset) {
|
||||
primaryColorSetting.value = colorPreset.name;
|
||||
ref.watch(immichThemePresetProvider.notifier).state = colorPreset;
|
||||
ref.invalidate(immichThemeProvider);
|
||||
|
||||
//turn off system color setting
|
||||
if (systemPrimaryColorSetting.value) {
|
||||
onUseSystemColorChange(false);
|
||||
} else {
|
||||
popBottomSheet();
|
||||
}
|
||||
}
|
||||
|
||||
buildPrimaryColorTile({
|
||||
required Color topColor,
|
||||
required Color bottomColor,
|
||||
required double tileSize,
|
||||
required bool showSelector,
|
||||
}) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(4.0),
|
||||
child: Stack(
|
||||
children: [
|
||||
Container(
|
||||
height: tileSize,
|
||||
width: tileSize,
|
||||
decoration: BoxDecoration(
|
||||
color: bottomColor,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(100)),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
height: tileSize / 2,
|
||||
width: tileSize,
|
||||
decoration: BoxDecoration(
|
||||
color: topColor,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(100),
|
||||
topRight: Radius.circular(100),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (showSelector)
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(100)),
|
||||
color: Colors.grey[900]?.withOpacity(.4),
|
||||
),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(3),
|
||||
child: Icon(
|
||||
Icons.check_rounded,
|
||||
color: Colors.white,
|
||||
size: 25,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
bottomSheetContent() {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
"theme_setting_primary_color_title".tr(),
|
||||
style: context.textTheme.titleLarge,
|
||||
),
|
||||
),
|
||||
if (isDynamicThemeAvailable)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
margin: const EdgeInsets.only(top: 10),
|
||||
child: SwitchListTile.adaptive(
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(vertical: 6, horizontal: 20),
|
||||
dense: true,
|
||||
activeColor: context.primaryColor,
|
||||
tileColor: context.colorScheme.surfaceContainerHigh,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
),
|
||||
title: Text(
|
||||
'theme_setting_system_primary_color_title'.tr(),
|
||||
style: context.textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
value: systemPrimaryColorSetting.value,
|
||||
onChanged: onUseSystemColorChange,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Wrap(
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: ImmichColorPreset.values.map((themePreset) {
|
||||
var theme = themePreset.getTheme();
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => onPrimaryColorChange(themePreset),
|
||||
child: buildPrimaryColorTile(
|
||||
topColor: theme.light.primary,
|
||||
bottomColor: theme.dark.primary,
|
||||
tileSize: tileSize,
|
||||
showSelector: currentPreset.value == themePreset &&
|
||||
!systemPrimaryColorSetting.value,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return ListTile(
|
||||
onTap: () => showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (BuildContext ctx) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 0),
|
||||
child: bottomSheetContent(),
|
||||
);
|
||||
},
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
title: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"theme_setting_primary_color_title".tr(),
|
||||
style: context.textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"theme_setting_primary_color_subtitle".tr(),
|
||||
style: context.textTheme.bodyMedium
|
||||
?.copyWith(color: context.colorScheme.onSurfaceSecondary),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 5.0, horizontal: 8.0),
|
||||
child: buildPrimaryColorTile(
|
||||
topColor: themeProvider.light.primary,
|
||||
bottomColor: themeProvider.dark.primary,
|
||||
tileSize: 42.0,
|
||||
showSelector: false,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/widgets/settings/preference_settings/primary_color_setting.dart';
|
||||
import 'package:immich_mobile/widgets/settings/settings_sub_title.dart';
|
||||
import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart';
|
||||
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
|
||||
@ -16,11 +17,16 @@ class ThemeSetting extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final currentThemeString = useAppSettingsState(AppSettingsEnum.themeMode);
|
||||
final currentTheme = useValueNotifier(ref.read(immichThemeProvider));
|
||||
final currentTheme = useValueNotifier(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(
|
||||
currentThemeString.value,
|
||||
(_, __) => currentTheme.value = switch (currentThemeString.value) {
|
||||
@ -30,12 +36,18 @@ class ThemeSetting extends HookConsumerWidget {
|
||||
},
|
||||
);
|
||||
|
||||
useValueChanged(
|
||||
applyThemeToBackgroundSetting.value,
|
||||
(_, __) => applyThemeToBackgroundProvider.value =
|
||||
applyThemeToBackgroundSetting.value,
|
||||
);
|
||||
|
||||
void onThemeChange(bool isDark) {
|
||||
if (isDark) {
|
||||
ref.watch(immichThemeProvider.notifier).state = ThemeMode.dark;
|
||||
ref.watch(immichThemeModeProvider.notifier).state = ThemeMode.dark;
|
||||
currentThemeString.value = "dark";
|
||||
} else {
|
||||
ref.watch(immichThemeProvider.notifier).state = ThemeMode.light;
|
||||
ref.watch(immichThemeModeProvider.notifier).state = ThemeMode.light;
|
||||
currentThemeString.value = "light";
|
||||
}
|
||||
}
|
||||
@ -44,7 +56,7 @@ class ThemeSetting extends HookConsumerWidget {
|
||||
if (isSystem) {
|
||||
currentThemeString.value = "system";
|
||||
isSystemTheme.value = true;
|
||||
ref.watch(immichThemeProvider.notifier).state = ThemeMode.system;
|
||||
ref.watch(immichThemeModeProvider.notifier).state = ThemeMode.system;
|
||||
} else {
|
||||
final currentSystemBrightness =
|
||||
MediaQuery.platformBrightnessOf(context);
|
||||
@ -52,14 +64,20 @@ class ThemeSetting extends HookConsumerWidget {
|
||||
isDarkTheme.value = currentSystemBrightness == Brightness.dark;
|
||||
if (currentSystemBrightness == Brightness.light) {
|
||||
currentThemeString.value = "light";
|
||||
ref.watch(immichThemeProvider.notifier).state = ThemeMode.light;
|
||||
ref.watch(immichThemeModeProvider.notifier).state = ThemeMode.light;
|
||||
} else if (currentSystemBrightness == Brightness.dark) {
|
||||
currentThemeString.value = "dark";
|
||||
ref.watch(immichThemeProvider.notifier).state = ThemeMode.dark;
|
||||
ref.watch(immichThemeModeProvider.notifier).state = ThemeMode.dark;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void onSurfaceColorSettingChange(bool useColorfulInterface) {
|
||||
applyThemeToBackgroundSetting.value = useColorfulInterface;
|
||||
ref.watch(colorfulInterfaceSettingProvider.notifier).state =
|
||||
useColorfulInterface;
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@ -75,6 +93,13 @@ class ThemeSetting extends HookConsumerWidget {
|
||||
title: 'theme_setting_dark_mode_switch'.tr(),
|
||||
onChanged: onThemeChange,
|
||||
),
|
||||
const PrimaryColorSetting(),
|
||||
SettingsSwitchListTile(
|
||||
valueNotifier: applyThemeToBackgroundProvider,
|
||||
title: "theme_setting_colorful_interface_title".tr(),
|
||||
subtitle: 'theme_setting_colorful_interface_subtitle'.tr(),
|
||||
onChanged: onSurfaceColorSettingChange,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
Reference in New Issue
Block a user