diff --git a/lib/application.dart b/lib/application.dart index a8a27db..7a5c822 100644 --- a/lib/application.dart +++ b/lib/application.dart @@ -30,20 +30,21 @@ class Application extends StatelessWidget { return MultiProvider( providers: [ Provider(create: (_) => UserPreferencesService(snapshot.data!)), - Provider(create: (_) => HapticsService()), + Provider(create: (_) => const HapticsService()), + Provider(create: (_) => PermissionsService()), Provider.value(value: evSource), ], - child: Provider( - create: (_) => PermissionsService(), - child: StopTypeProvider( - child: ThemeProvider( - initialPrimaryColor: const Color(0xFF2196f3), - builder: (context, child) => AnnotatedRegion( + child: StopTypeProvider( + child: ThemeProvider( + builder: (context, _) { + final systemIconsBrightness = + ThemeData.estimateBrightnessForColor(context.watch().colorScheme.onSurface); + return AnnotatedRegion( value: SystemUiOverlayStyle( statusBarColor: Colors.transparent, - statusBarIconBrightness: - ThemeData.estimateBrightnessForColor(context.watch().colorScheme.onSurface), + statusBarIconBrightness: systemIconsBrightness, systemNavigationBarColor: context.watch().colorScheme.surface, + systemNavigationBarIconBrightness: systemIconsBrightness, ), child: MaterialApp( theme: context.watch(), @@ -64,8 +65,8 @@ class Application extends StatelessWidget { "settings": (context) => const SettingsScreen(), }, ), - ), - ), + ); + }, ), ), ); diff --git a/lib/data/haptics_service.dart b/lib/data/haptics_service.dart index 0b6e7c3..aa7511b 100644 --- a/lib/data/haptics_service.dart +++ b/lib/data/haptics_service.dart @@ -1,6 +1,8 @@ import 'package:vibration/vibration.dart'; class HapticsService { + const HapticsService(); + Future quickVibration() async => _tryVibrate(duration: 25, amplitude: 96); Future responseVibration() async => _tryVibrate(duration: 50, amplitude: 128); 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 376e86c..85196d6 100644 --- a/lib/res/theme.dart +++ b/lib/res/theme.dart @@ -1,5 +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'; @@ -7,13 +9,9 @@ import 'package:material_color_utilities/material_color_utilities.dart'; import 'package:provider/provider.dart'; class ThemeProvider extends StatefulWidget { - final Color initialPrimaryColor; - final Widget? child; final TransitionBuilder? builder; const ThemeProvider({ - required this.initialPrimaryColor, - this.child, this.builder, super.key, }); @@ -27,53 +25,50 @@ class ThemeProvider extends StatefulWidget { } class ThemeProviderState extends State { - late ThemeType _themeType; - late Color _primaryColor = widget.initialPrimaryColor; + 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 Provider.value( - value: _themeType, - child: Provider.value( - value: _themeFromColor( - _primaryColor, - _mapThemeTypeToBrightness(_themeType), + 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; } - void setPrimaryColor(Color color) { - if (color == _primaryColor) { - return; - } - - setState(() { - _primaryColor = color; - }); - } - - Brightness _mapThemeTypeToBrightness(ThemeType themeType) { - switch (themeType) { + Brightness get _themeBrightness { + switch (_themeTypeNotifier.value) { case ThemeType.light: return Brightness.light; case ThemeType.dark: @@ -83,9 +78,85 @@ class ThemeProviderState extends State { } } - ThemeData _themeFromColor(Color color, Brightness brightness) { - final scheme = brightness == Brightness.light ? Scheme.light(color.value) : Scheme.dark(color.value); - final colorScheme = ColorScheme( + void enableDynamicColors(bool enable) { + _dynamicColorsNotifier.value = enable; + context.read().dynamicColors = enable; + } +} + +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, + ); + } + + 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), onPrimary: Color(scheme.onPrimary), @@ -102,9 +173,5 @@ class ThemeProviderState extends State { surfaceVariant: Color.alphaBlend(Color(scheme.primary).withOpacity(0.5), Color(scheme.background)), onSurfaceVariant: Color(scheme.onSurfaceVariant), ); - return ThemeData( - useMaterial3: true, - colorScheme: colorScheme, - ); } } 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 956a47c..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 { @@ -13,7 +13,6 @@ class SettingsScreen extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( - backgroundColor: Theme.of(context).colorScheme.surface, body: SafeArea( top: false, child: CustomScrollView( @@ -42,7 +41,7 @@ class SettingsScreen extends StatelessWidget { [ const StopTypeListTile(), const HapticsListTileProvider(), - const ThemeTypeListTile(), + const ThemeSettings(), ], ), ), diff --git a/pubspec.yaml b/pubspec.yaml index d916082..77f6b62 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -9,6 +9,7 @@ environment: dependencies: camera: 0.10.0+4 exif: 3.1.2 + dynamic_color: 1.5.4 flutter: sdk: flutter flutter_bloc: 8.1.1 @@ -16,7 +17,7 @@ dependencies: sdk: flutter intl: 0.17.0 intl_utils: 2.8.1 - material_color_utilities: 0.2.0 + material_color_utilities: 0.1.5 package_info_plus: 3.0.2 permission_handler: 10.2.0 provider: 6.0.4 @@ -28,10 +29,10 @@ dev_dependencies: flutter_launcher_icons: 0.11.0 flutter_lints: 2.0.0 flutter_native_splash: 2.2.16 - test: 1.21.6 + test: 1.22.2 dependency_overrides: - material_color_utilities: 0.2.0 + test_api: 0.4.12 flutter: uses-material-design: true