mirror of
https://github.com/immich-app/immich
synced 2025-11-07 17:27:20 +00: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:
parent
20262209ce
commit
0eacdf93eb
65 changed files with 944 additions and 563 deletions
|
|
@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart';
|
|||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
|
||||
class CustomeProxyHeaderSettings extends StatelessWidget {
|
||||
|
|
@ -20,8 +21,8 @@ class CustomeProxyHeaderSettings extends StatelessWidget {
|
|||
),
|
||||
subtitle: Text(
|
||||
"headers_settings_tile_subtitle".tr(),
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
color: context.colorScheme.onSurfaceSecondary,
|
||||
),
|
||||
),
|
||||
onTap: () => context.pushRoute(const HeaderSettingsRoute()),
|
||||
|
|
|
|||
|
|
@ -40,9 +40,7 @@ class LanguageSettings extends HookConsumerWidget {
|
|||
),
|
||||
),
|
||||
backgroundColor: WidgetStatePropertyAll<Color>(
|
||||
context.isDarkTheme
|
||||
? Colors.grey[900]!
|
||||
: context.scaffoldBackgroundColor,
|
||||
context.colorScheme.surfaceContainer,
|
||||
),
|
||||
),
|
||||
menuHeight: context.height * 0.5,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import 'package:flutter_hooks/flutter_hooks.dart' show useEffect, useState;
|
|||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/entities/duplicated_asset.entity.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||
import 'package:immich_mobile/providers/db.provider.dart';
|
||||
|
||||
class LocalStorageSettings extends HookConsumerWidget {
|
||||
|
|
@ -35,10 +36,10 @@ class LocalStorageSettings extends HookConsumerWidget {
|
|||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
).tr(args: ["${cacheItemCount.value}"]),
|
||||
subtitle: const Text(
|
||||
subtitle: Text(
|
||||
"cache_settings_duplicated_assets_subtitle",
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
color: context.colorScheme.onSurfaceSecondary,
|
||||
),
|
||||
).tr(),
|
||||
trailing: TextButton(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||
|
||||
class SettingsButtonListTile extends StatelessWidget {
|
||||
final IconData icon;
|
||||
|
|
@ -39,7 +40,12 @@ class SettingsButtonListTile extends StatelessWidget {
|
|||
children: [
|
||||
if (subtileText != null) const SizedBox(height: 4),
|
||||
if (subtileText != null)
|
||||
Text(subtileText!, style: context.textTheme.bodyMedium),
|
||||
Text(
|
||||
subtileText!,
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
color: context.colorScheme.onSurfaceSecondary,
|
||||
),
|
||||
),
|
||||
if (subtitle != null) subtitle!,
|
||||
const SizedBox(height: 6),
|
||||
ElevatedButton(onPressed: onButtonTap, child: Text(buttonText)),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||
|
||||
class SettingsSwitchListTile extends StatelessWidget {
|
||||
final ValueNotifier<bool> valueNotifier;
|
||||
|
|
@ -54,7 +55,9 @@ class SettingsSwitchListTile extends StatelessWidget {
|
|||
? Text(
|
||||
subtitle!,
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
color: enabled ? null : context.themeData.disabledColor,
|
||||
color: enabled
|
||||
? context.colorScheme.onSurfaceSecondary
|
||||
: context.themeData.disabledColor,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter/services.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||
import 'package:immich_mobile/utils/http_ssl_cert_override.dart';
|
||||
|
||||
class SslClientCertSettings extends StatefulWidget {
|
||||
|
|
@ -40,7 +41,9 @@ class _SslClientCertSettingsState extends State<SslClientCertSettings> {
|
|||
children: [
|
||||
Text(
|
||||
"client_cert_subtitle".tr(),
|
||||
style: context.textTheme.bodyMedium,
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
color: context.colorScheme.onSurfaceSecondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 6,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue