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:
Pruthvi Bugidi 2024-08-06 19:50:27 +05:30 committed by GitHub
parent 20262209ce
commit 0eacdf93eb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
65 changed files with 944 additions and 563 deletions

View file

@ -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()),

View file

@ -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,

View file

@ -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(

View file

@ -15,6 +15,9 @@ class PreferenceSetting extends StatelessWidget {
HapticSetting(),
];
return const SettingsSubPageScaffold(settings: preferenceSettings);
return const SettingsSubPageScaffold(
settings: preferenceSettings,
showDivider: true,
);
}
}

View file

@ -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,
),
),
],
),
);
}
}

View file

@ -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,
),
],
);
}

View file

@ -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)),

View file

@ -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,

View file

@ -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,