diff --git a/lib/application.dart b/lib/application.dart index aec5b20..077d48c 100644 --- a/lib/application.dart +++ b/lib/application.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:lightmeter/data/ev_source/ev_source_type.dart'; +import 'package:lightmeter/data/models/theme_type.dart'; import 'package:lightmeter/data/permissions_service.dart'; import 'package:lightmeter/screens/settings/settings_screen.dart'; import 'package:provider/provider.dart'; @@ -52,27 +53,27 @@ class _ApplicationState extends State { child: Provider( create: (context) => PermissionsService(), child: StopTypeProvider( - child: MaterialApp( - theme: ThemeData( - useMaterial3: true, - colorScheme: lightColorScheme, + child: ThemeProvider( + initialPrimaryColor: const Color(0xFF2196f3), + builder: (context, child) => MaterialApp( + theme: context.watch(), + localizationsDelegates: const [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: S.delegate.supportedLocales, + builder: (context, child) => MediaQuery( + data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0), + child: child!, + ), + home: const MeteringFlow(), + routes: { + "metering": (context) => const MeteringFlow(), + "settings": (context) => const SettingsScreen(), + }, ), - localizationsDelegates: const [ - S.delegate, - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - supportedLocales: S.delegate.supportedLocales, - builder: (context, child) => MediaQuery( - data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0), - child: child!, - ), - home: const MeteringFlow(), - routes: { - "metering": (context) => const MeteringFlow(), - "settings": (context) => const SettingsScreen(), - }, ), ), ), diff --git a/lib/data/models/theme_type.dart b/lib/data/models/theme_type.dart new file mode 100644 index 0000000..94488f7 --- /dev/null +++ b/lib/data/models/theme_type.dart @@ -0,0 +1 @@ +enum ThemeType {light, dark, systemDefault} \ No newline at end of file diff --git a/lib/data/shared_prefs_service.dart b/lib/data/shared_prefs_service.dart index 012c04e..b975b62 100644 --- a/lib/data/shared_prefs_service.dart +++ b/lib/data/shared_prefs_service.dart @@ -1,4 +1,5 @@ import 'package:lightmeter/data/models/nd_value.dart'; +import 'package:lightmeter/data/models/theme_type.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'models/iso_value.dart'; @@ -7,6 +8,8 @@ class UserPreferencesService { static const _isoKey = "ISO"; static const _ndFilterKey = "ND"; + static const _themeTypeKey = "ThemeType"; + final SharedPreferences _sharedPreferences; UserPreferencesService(this._sharedPreferences); @@ -16,4 +19,7 @@ class UserPreferencesService { NdValue get ndFilter => ndValues.firstWhere((v) => v.value == (_sharedPreferences.getInt(_ndFilterKey) ?? 0)); set ndFilter(NdValue value) => _sharedPreferences.setInt(_ndFilterKey, value.value); + + ThemeType get themeType => ThemeType.values[_sharedPreferences.getInt(_themeTypeKey) ?? 0]; + set themeType(ThemeType value) => _sharedPreferences.setInt(_themeTypeKey, value.index); } diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index e39a45c..698d54e 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -19,5 +19,10 @@ "thirdStops": "1/3", "caffeine": "Caffeine", "keepsScreenOn": "Keeps screen on", - "haptics": "Haptics" + "haptics": "Haptics", + "theme": "Theme", + "chooseTheme": "Choose theme", + "themeLight": "Light", + "themeDark": "Dark", + "themeSystemDefault": "System default" } \ No newline at end of file diff --git a/lib/res/theme.dart b/lib/res/theme.dart index 440eb13..376e86c 100644 --- a/lib/res/theme.dart +++ b/lib/res/theme.dart @@ -1,22 +1,110 @@ import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.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'; -ColorScheme get lightColorScheme { - final scheme = Scheme.light(0xFF2196f3); - return ColorScheme.light( - primary: Color(scheme.primary), - onPrimary: Color(scheme.onPrimary), - primaryContainer: Color(scheme.primaryContainer), - onPrimaryContainer: Color(scheme.onPrimaryContainer), - secondary: Color(scheme.secondary), - onSecondary: Color(scheme.onSecondary), - error: Color(scheme.error), - onError: Color(scheme.onError), - background: Color(scheme.background), - onBackground: Color(scheme.onBackground), - surface: Color.alphaBlend(Color(scheme.primary).withOpacity(0.05), Color(scheme.background)), - onSurface: Color(scheme.onSurface), - surfaceVariant: Color.alphaBlend(Color(scheme.primary).withOpacity(0.5), Color(scheme.background)), - onSurfaceVariant: Color(scheme.onSurfaceVariant), - ); +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, + }); + + static ThemeProviderState of(BuildContext context) { + return context.findAncestorStateOfType()!; + } + + @override + State createState() => ThemeProviderState(); +} + +class ThemeProviderState extends State { + late ThemeType _themeType; + late Color _primaryColor = widget.initialPrimaryColor; + + @override + void initState() { + super.initState(); + _themeType = context.read().themeType; + } + + @override + Widget build(BuildContext context) { + return Provider.value( + value: _themeType, + child: Provider.value( + value: _themeFromColor( + _primaryColor, + _mapThemeTypeToBrightness(_themeType), + ), + builder: widget.builder, + child: widget.child, + ), + ); + } + + void setThemeType(ThemeType themeType) { + if (themeType == _themeType) { + return; + } + + setState(() { + _themeType = themeType; + }); + context.read().themeType = themeType; + } + + void setPrimaryColor(Color color) { + if (color == _primaryColor) { + return; + } + + setState(() { + _primaryColor = color; + }); + } + + Brightness _mapThemeTypeToBrightness(ThemeType themeType) { + switch (themeType) { + case ThemeType.light: + return Brightness.light; + case ThemeType.dark: + return Brightness.dark; + case ThemeType.systemDefault: + return SchedulerBinding.instance.platformDispatcher.platformBrightness; + } + } + + ThemeData _themeFromColor(Color color, Brightness brightness) { + final scheme = brightness == Brightness.light ? Scheme.light(color.value) : Scheme.dark(color.value); + final colorScheme = ColorScheme( + brightness: brightness, + primary: Color(scheme.primary), + onPrimary: Color(scheme.onPrimary), + primaryContainer: Color(scheme.primaryContainer), + onPrimaryContainer: Color(scheme.onPrimaryContainer), + secondary: Color(scheme.secondary), + onSecondary: Color(scheme.onSecondary), + error: Color(scheme.error), + onError: Color(scheme.onError), + background: Color(scheme.background), + onBackground: Color(scheme.onBackground), + surface: Color.alphaBlend(Color(scheme.primary).withOpacity(0.05), Color(scheme.background)), + onSurface: Color(scheme.onSurface), + 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/shared/dialog_picker.dart b/lib/screens/settings/components/shared/dialog_picker.dart new file mode 100644 index 0000000..03050ad --- /dev/null +++ b/lib/screens/settings/components/shared/dialog_picker.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'package:lightmeter/generated/l10n.dart'; +import 'package:lightmeter/res/dimens.dart'; + +class DialogPicker extends StatefulWidget { + final String title; + final T selectedValue; + final List values; + final String Function(BuildContext context, T value) titleAdapter; + + const DialogPicker({ + required this.title, + required this.selectedValue, + required this.values, + required this.titleAdapter, + super.key, + }); + + @override + State> createState() => _DialogPickerState(); +} + +class _DialogPickerState extends State> { + late T _selected = widget.selectedValue; + + @override + Widget build(BuildContext context) { + return AlertDialog( + titlePadding: const EdgeInsets.fromLTRB( + Dimens.paddingL, + Dimens.paddingL, + Dimens.paddingL, + Dimens.paddingM, + ), + title: Text(widget.title), + contentPadding: EdgeInsets.zero, + content: Column( + mainAxisSize: MainAxisSize.min, + children: widget.values + .map( + (e) => RadioListTile( + value: e, + groupValue: _selected, + title: Text(widget.titleAdapter(context, e)), + onChanged: (T? value) { + if (value != null) { + setState(() { + _selected = value; + }); + } + }, + ), + ) + .toList(), + ), + actionsPadding: const EdgeInsets.fromLTRB( + Dimens.paddingL, + Dimens.paddingM, + Dimens.paddingL, + Dimens.paddingL, + ), + actions: [ + TextButton( + onPressed: Navigator.of(context).pop, + child: Text(S.of(context).cancel), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(_selected), + child: Text(S.of(context).select), + ), + ], + ); + } +} diff --git a/lib/screens/settings/components/theme_type_tile.dart b/lib/screens/settings/components/theme_type_tile.dart new file mode 100644 index 0000000..bec8afb --- /dev/null +++ b/lib/screens/settings/components/theme_type_tile.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:lightmeter/data/models/theme_type.dart'; +import 'package:lightmeter/data/shared_prefs_service.dart'; +import 'package:lightmeter/generated/l10n.dart'; +import 'package:lightmeter/res/theme.dart'; +import 'package:lightmeter/utils/stop_type_provider.dart'; +import 'package:provider/provider.dart'; + +import 'shared/dialog_picker.dart'; + +class ThemeTypeListTile extends StatelessWidget { + const ThemeTypeListTile({super.key}); + + @override + Widget build(BuildContext context) { + return ListTile( + leading: const Icon(Icons.brightness_6), + title: Text(S.of(context).theme), + trailing: Text(_typeToString(context, context.watch())), + onTap: () { + showDialog( + context: context, + builder: (_) => DialogPicker( + title: S.of(context).chooseTheme, + selectedValue: context.read(), + values: ThemeType.values, + titleAdapter: _typeToString, + ), + ).then((value) { + if (value != null) { + ThemeProvider.of(context).setThemeType(value); + } + }); + }, + ); + } + + String _typeToString(BuildContext context, ThemeType themeType) { + switch (themeType) { + case ThemeType.light: + return S.of(context).themeLight; + case ThemeType.dark: + return S.of(context).themeDark; + case ThemeType.systemDefault: + return S.of(context).themeSystemDefault; + } + } +} diff --git a/lib/screens/settings/settings_screen.dart b/lib/screens/settings/settings_screen.dart index 38b38b6..b028f06 100644 --- a/lib/screens/settings/settings_screen.dart +++ b/lib/screens/settings/settings_screen.dart @@ -5,6 +5,7 @@ import 'package:lightmeter/res/dimens.dart'; import 'components/caffeine_tile.dart'; import 'components/haptics_tile.dart'; import 'components/fractional_stops/list_tile_fractional_stops.dart'; +import 'components/theme_type_tile.dart'; class SettingsScreen extends StatelessWidget { const SettingsScreen({super.key}); @@ -40,6 +41,7 @@ class SettingsScreen extends StatelessWidget { const FractionalStopsListTile(), const CaffeineListTile(), const HapticsListTile(), + const ThemeTypeListTile(), ], ), ),