diff --git a/lib/data/models/dynamic_colors_state.dart b/lib/data/models/dynamic_colors_state.dart new file mode 100644 index 0000000..a9fb712 --- /dev/null +++ b/lib/data/models/dynamic_colors_state.dart @@ -0,0 +1 @@ +enum DynamicColorsState { unavailable, enabled, disabled } diff --git a/lib/data/shared_prefs_service.dart b/lib/data/shared_prefs_service.dart index 71d7c0c..2e67fa4 100644 --- a/lib/data/shared_prefs_service.dart +++ b/lib/data/shared_prefs_service.dart @@ -10,6 +10,7 @@ class UserPreferencesService { static const _hapticsKey = "haptics"; static const _themeTypeKey = "themeType"; + static const _dynamicColorsKey = "dynamicColors"; final SharedPreferences _sharedPreferences; @@ -26,4 +27,7 @@ class UserPreferencesService { ThemeType get themeType => ThemeType.values[_sharedPreferences.getInt(_themeTypeKey) ?? 0]; set themeType(ThemeType value) => _sharedPreferences.setInt(_themeTypeKey, value.index); + + bool get dynamicColors => _sharedPreferences.getBool(_dynamicColorsKey) ?? false; + set dynamicColors(bool value) => _sharedPreferences.setBool(_dynamicColorsKey, value); } diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index f605eb2..b8dcb5d 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -20,6 +20,7 @@ "haptics": "Haptics", "theme": "Theme", "chooseTheme": "Choose theme", + "dynamicColors": "Dynamic colors", "themeLight": "Light", "themeDark": "Dark", "themeSystemDefault": "System default", diff --git a/lib/res/theme.dart b/lib/res/theme.dart index 0fc2f0c..85196d6 100644 --- a/lib/res/theme.dart +++ b/lib/res/theme.dart @@ -1,6 +1,7 @@ import 'package:dynamic_color/dynamic_color.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; +import 'package:lightmeter/data/models/dynamic_colors_state.dart'; import 'package:lightmeter/data/models/theme_type.dart'; import 'package:lightmeter/data/shared_prefs_service.dart'; import 'package:material_color_utilities/material_color_utilities.dart'; @@ -8,11 +9,9 @@ import 'package:material_color_utilities/material_color_utilities.dart'; import 'package:provider/provider.dart'; class ThemeProvider extends StatefulWidget { - final Widget? child; final TransitionBuilder? builder; const ThemeProvider({ - this.child, this.builder, super.key, }); @@ -26,59 +25,50 @@ class ThemeProvider extends StatefulWidget { } class ThemeProviderState extends State { - late ThemeType _themeType; - Color _primaryColor = const Color(0xFF2196f3); - bool _allowDynamicColors = false; - - bool get allowDynamicColors => _allowDynamicColors; + late final _themeTypeNotifier = ValueNotifier(context.read().themeType); + late final _dynamicColorsNotifier = ValueNotifier(context.read().dynamicColors); + late final _primaryColorNotifier = ValueNotifier(const Color(0xFF2196f3)); @override - void initState() { - super.initState(); - _themeType = context.read().themeType; + void dispose() { + _themeTypeNotifier.dispose(); + _dynamicColorsNotifier.dispose(); + _primaryColorNotifier.dispose(); + super.dispose(); } @override Widget build(BuildContext context) { - return DynamicColorBuilder( - builder: (lightDynamic, darkDynamic) { - _allowDynamicColors = lightDynamic != null && darkDynamic != null; - if (_allowDynamicColors) { - final dynamicColorScheme = _themeBrightness == Brightness.light ? lightDynamic : darkDynamic; - if (dynamicColorScheme != null) { - _primaryColor = dynamicColorScheme.primary; - } - } - return Provider.value( - value: _themeType, - child: Provider.value( - value: _themeFromColorScheme( - _colorSchemeFromColor( - _primaryColor, - _themeBrightness, + return ValueListenableBuilder( + valueListenable: _themeTypeNotifier, + builder: (_, themeType, __) => Provider.value( + value: themeType, + child: ValueListenableBuilder( + valueListenable: _dynamicColorsNotifier, + builder: (_, useDynamicColors, __) => _DynamicColorsProvider( + useDynamicColors: useDynamicColors, + themeBrightness: _themeBrightness, + builder: (_, dynamicPrimaryColor) => ValueListenableBuilder( + valueListenable: _primaryColorNotifier, + builder: (_, primaryColor, __) => _ThemeDataProvider( + primaryColor: dynamicPrimaryColor ?? primaryColor, + brightness: _themeBrightness, + builder: widget.builder, ), ), - builder: widget.builder, - child: widget.child, ), - ); - }, + ), + ), ); } void setThemeType(ThemeType themeType) { - if (themeType == _themeType) { - return; - } - - setState(() { - _themeType = themeType; - }); + _themeTypeNotifier.value = themeType; context.read().themeType = themeType; } Brightness get _themeBrightness { - switch (_themeType) { + switch (_themeTypeNotifier.value) { case ThemeType.light: return Brightness.light; case ThemeType.dark: @@ -88,18 +78,84 @@ class ThemeProviderState extends State { } } - void setPrimaryColor(Color color) { - if (color == _primaryColor) { - return; - } + void enableDynamicColors(bool enable) { + _dynamicColorsNotifier.value = enable; + context.read().dynamicColors = enable; + } +} - setState(() { - _primaryColor = color; - }); +class _DynamicColorsProvider extends StatelessWidget { + final bool useDynamicColors; + final Brightness themeBrightness; + final Widget Function(BuildContext context, Color? primaryColor) builder; + + const _DynamicColorsProvider({ + required this.useDynamicColors, + required this.themeBrightness, + required this.builder, + }); + + @override + Widget build(BuildContext context) { + return DynamicColorBuilder( + builder: (lightDynamic, darkDynamic) { + late final DynamicColorsState state; + late final Color? dynamicPrimaryColor; + if (lightDynamic != null && darkDynamic != null) { + if (useDynamicColors) { + dynamicPrimaryColor = (themeBrightness == Brightness.light ? lightDynamic : darkDynamic).primary; + state = DynamicColorsState.enabled; + } else { + dynamicPrimaryColor = null; + state = DynamicColorsState.disabled; + } + } else { + dynamicPrimaryColor = null; + state = DynamicColorsState.unavailable; + } + return Provider.value( + value: state, + child: builder(context, dynamicPrimaryColor), + ); + }, + ); + } +} + +class _ThemeDataProvider extends StatelessWidget { + final Color primaryColor; + final Brightness brightness; + final TransitionBuilder? builder; + + const _ThemeDataProvider({ + required this.primaryColor, + required this.brightness, + required this.builder, + }); + + @override + Widget build(BuildContext context) { + return Provider.value( + value: _themeFromColorScheme(_colorSchemeFromColor()), + builder: builder, + ); } - ColorScheme _colorSchemeFromColor(Color color, Brightness brightness) { - final scheme = brightness == Brightness.light ? Scheme.light(color.value) : Scheme.dark(color.value); + ThemeData _themeFromColorScheme(ColorScheme scheme) { + return ThemeData( + useMaterial3: true, + bottomAppBarColor: scheme.surface, + brightness: scheme.brightness, + colorScheme: scheme, + dialogBackgroundColor: scheme.surface, + dialogTheme: DialogTheme(backgroundColor: scheme.surface), + scaffoldBackgroundColor: scheme.surface, + toggleableActiveColor: scheme.primary, + ); + } + + ColorScheme _colorSchemeFromColor() { + final scheme = brightness == Brightness.light ? Scheme.light(primaryColor.value) : Scheme.dark(primaryColor.value); return ColorScheme( brightness: brightness, primary: Color(scheme.primary), @@ -118,17 +174,4 @@ class ThemeProviderState extends State { onSurfaceVariant: Color(scheme.onSurfaceVariant), ); } - - ThemeData _themeFromColorScheme(ColorScheme scheme) { - return ThemeData( - useMaterial3: true, - bottomAppBarColor: scheme.surface, - brightness: scheme.brightness, - colorScheme: scheme, - dialogBackgroundColor: scheme.surface, - dialogTheme: DialogTheme(backgroundColor: scheme.surface), - scaffoldBackgroundColor: scheme.surface, - toggleableActiveColor: scheme.primary, - ); - } } diff --git a/lib/screens/settings/components/theme/components/widget_list_tile_dynamic_colors.dart b/lib/screens/settings/components/theme/components/widget_list_tile_dynamic_colors.dart new file mode 100644 index 0000000..d5cb9b1 --- /dev/null +++ b/lib/screens/settings/components/theme/components/widget_list_tile_dynamic_colors.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:lightmeter/data/models/dynamic_colors_state.dart'; +import 'package:lightmeter/generated/l10n.dart'; +import 'package:lightmeter/res/theme.dart'; + +class DynamicColorsListTile extends StatelessWidget { + const DynamicColorsListTile({super.key}); + + @override + Widget build(BuildContext context) { + return SwitchListTile( + secondary: const Icon(Icons.colorize), + title: Text(S.of(context).dynamicColors), + value: context.watch() == DynamicColorsState.enabled, + onChanged: ThemeProvider.of(context).enableDynamicColors, + ); + } +} diff --git a/lib/screens/settings/components/widget_list_tile_theme_type.dart b/lib/screens/settings/components/theme/components/widget_list_tile_theme_type.dart similarity index 93% rename from lib/screens/settings/components/widget_list_tile_theme_type.dart rename to lib/screens/settings/components/theme/components/widget_list_tile_theme_type.dart index 34f4e43..9a90cc9 100644 --- a/lib/screens/settings/components/widget_list_tile_theme_type.dart +++ b/lib/screens/settings/components/theme/components/widget_list_tile_theme_type.dart @@ -2,10 +2,9 @@ import 'package:flutter/material.dart'; import 'package:lightmeter/data/models/theme_type.dart'; import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/res/theme.dart'; +import 'package:lightmeter/screens/settings/components/shared/widget_dialog_picker.dart'; import 'package:provider/provider.dart'; -import 'shared/widget_dialog_picker.dart'; - class ThemeTypeListTile extends StatelessWidget { const ThemeTypeListTile({super.key}); diff --git a/lib/screens/settings/components/theme/widget_settings_theme.dart b/lib/screens/settings/components/theme/widget_settings_theme.dart new file mode 100644 index 0000000..ed7cfa9 --- /dev/null +++ b/lib/screens/settings/components/theme/widget_settings_theme.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; +import 'package:lightmeter/data/models/dynamic_colors_state.dart'; +import 'package:provider/provider.dart'; + +import 'components/widget_list_tile_dynamic_colors.dart'; +import 'components/widget_list_tile_theme_type.dart'; + +class ThemeSettings extends StatelessWidget { + const ThemeSettings({super.key}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + const ThemeTypeListTile(), + if (context.read() != DynamicColorsState.unavailable) const DynamicColorsListTile(), + ], + ); + } +} diff --git a/lib/screens/settings/screen_settings.dart b/lib/screens/settings/screen_settings.dart index b040f3e..6406f94 100644 --- a/lib/screens/settings/screen_settings.dart +++ b/lib/screens/settings/screen_settings.dart @@ -4,7 +4,7 @@ import 'package:lightmeter/res/dimens.dart'; import 'components/haptics/provider_list_tile_haptics.dart'; import 'components/widget_list_tile_fractional_stops.dart'; -import 'components/widget_list_tile_theme_type.dart'; +import 'components/theme/widget_settings_theme.dart'; import 'components/widget_label_version.dart'; class SettingsScreen extends StatelessWidget { @@ -41,7 +41,7 @@ class SettingsScreen extends StatelessWidget { [ const StopTypeListTile(), const HapticsListTileProvider(), - const ThemeTypeListTile(), + const ThemeSettings(), ], ), ),