ML-18 Implement primary color picker (#19)

* wip

* hide `DynamicColorListTile` if unavailable

* added color animation for `AnimatedDialog`

* adjusted some colors

* sync `AnimatedDialog` insets with material

* scroll to selected color
This commit is contained in:
Vadim 2023-02-01 00:24:26 +03:00 committed by GitHub
parent cb4e91cb0a
commit 9cfffc3377
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 369 additions and 138 deletions

View file

@ -9,8 +9,6 @@
}, },
"dart.lineLength": 100, "dart.lineLength": 100,
"[dart]": { "[dart]": {
"editor.formatOnSave": true,
"editor.formatOnType": true,
"editor.rulers": [ "editor.rulers": [
100, 100,
120, 120,

View file

@ -1,3 +1,4 @@
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'models/ev_source_type.dart'; import 'models/ev_source_type.dart';
@ -15,6 +16,7 @@ class UserPreferencesService {
static const _hapticsKey = "haptics"; static const _hapticsKey = "haptics";
static const _themeTypeKey = "themeType"; static const _themeTypeKey = "themeType";
static const _primaryColorKey = "primaryColor";
static const _dynamicColorKey = "dynamicColor"; static const _dynamicColorKey = "dynamicColor";
final SharedPreferences _sharedPreferences; final SharedPreferences _sharedPreferences;
@ -42,6 +44,9 @@ class UserPreferencesService {
ThemeType get themeType => ThemeType.values[_sharedPreferences.getInt(_themeTypeKey) ?? 0]; ThemeType get themeType => ThemeType.values[_sharedPreferences.getInt(_themeTypeKey) ?? 0];
set themeType(ThemeType value) => _sharedPreferences.setInt(_themeTypeKey, value.index); set themeType(ThemeType value) => _sharedPreferences.setInt(_themeTypeKey, value.index);
Color get primaryColor => Color(_sharedPreferences.getInt(_primaryColorKey) ?? 0xff2196f3);
set primaryColor(Color value) => _sharedPreferences.setInt(_primaryColorKey, value.value);
bool get dynamicColor => _sharedPreferences.getBool(_dynamicColorKey) ?? false; bool get dynamicColor => _sharedPreferences.getBool(_dynamicColorKey) ?? false;
set dynamicColor(bool value) => _sharedPreferences.setBool(_dynamicColorKey, value); set dynamicColor(bool value) => _sharedPreferences.setBool(_dynamicColorKey, value);
} }

View file

@ -36,10 +36,12 @@
"haptics": "Haptics", "haptics": "Haptics",
"theme": "Theme", "theme": "Theme",
"chooseTheme": "Choose theme", "chooseTheme": "Choose theme",
"dynamicColor": "Dynamic color",
"themeLight": "Light", "themeLight": "Light",
"themeDark": "Dark", "themeDark": "Dark",
"themeSystemDefault": "System default", "themeSystemDefault": "System default",
"dynamicColor": "Dynamic color",
"primaryColor": "Primary color",
"choosePrimaryColor": "Choose primary color",
"about": "About", "about": "About",
"sourceCode": "Source code", "sourceCode": "Source code",
"reportIssue": "Report an issue", "reportIssue": "Report an issue",

View file

@ -4,6 +4,7 @@ import 'package:flutter/scheduler.dart';
import 'package:lightmeter/data/models/dynamic_colors_state.dart'; import 'package:lightmeter/data/models/dynamic_colors_state.dart';
import 'package:lightmeter/data/models/theme_type.dart'; import 'package:lightmeter/data/models/theme_type.dart';
import 'package:lightmeter/data/shared_prefs_service.dart'; import 'package:lightmeter/data/shared_prefs_service.dart';
import 'package:lightmeter/res/dimens.dart';
import 'package:material_color_utilities/material_color_utilities.dart'; import 'package:material_color_utilities/material_color_utilities.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -20,6 +21,25 @@ class ThemeProvider extends StatefulWidget {
return context.findAncestorStateOfType<ThemeProviderState>()!; return context.findAncestorStateOfType<ThemeProviderState>()!;
} }
static const primaryColorsList = [
Color(0xfff44336),
Color(0xffe91e63),
Color(0xff9c27b0),
Color(0xff673ab7),
Color(0xff3f51b5),
Color(0xff2196f3),
Color(0xff03a9f4),
Color(0xff00bcd4),
Color(0xff009688),
Color(0xff4caf50),
Color(0xff8bc34a),
Color(0xffcddc39),
Color(0xffffeb3b),
Color(0xffffc107),
Color(0xffff9800),
Color(0xffff5722),
];
@override @override
State<ThemeProvider> createState() => ThemeProviderState(); State<ThemeProvider> createState() => ThemeProviderState();
} }
@ -29,7 +49,7 @@ class ThemeProviderState extends State<ThemeProvider> {
late final _themeTypeNotifier = ValueNotifier<ThemeType>(_prefs.themeType); late final _themeTypeNotifier = ValueNotifier<ThemeType>(_prefs.themeType);
late final _dynamicColorNotifier = ValueNotifier<bool>(_prefs.dynamicColor); late final _dynamicColorNotifier = ValueNotifier<bool>(_prefs.dynamicColor);
late final _primaryColorNotifier = ValueNotifier<Color>(const Color(0xFF2196f3)); late final _primaryColorNotifier = ValueNotifier<Color>(_prefs.primaryColor);
@override @override
void dispose() { void dispose() {
@ -80,6 +100,11 @@ class ThemeProviderState extends State<ThemeProvider> {
} }
} }
void setPrimaryColor(Color color) {
_primaryColorNotifier.value = color;
_prefs.primaryColor = color;
}
void enableDynamicColor(bool enable) { void enableDynamicColor(bool enable) {
_dynamicColorNotifier.value = enable; _dynamicColorNotifier.value = enable;
_prefs.dynamicColor = enable; _prefs.dynamicColor = enable;
@ -148,9 +173,33 @@ class _ThemeDataProvider extends StatelessWidget {
return ThemeData( return ThemeData(
useMaterial3: true, useMaterial3: true,
brightness: scheme.brightness, brightness: scheme.brightness,
primaryColor: primaryColor,
colorScheme: scheme, colorScheme: scheme,
appBarTheme: AppBarTheme(
elevation: 4,
color: scheme.surface,
surfaceTintColor: scheme.surfaceTint,
),
cardTheme: CardTheme(
clipBehavior: Clip.antiAlias,
color: scheme.surface,
elevation: 4,
margin: EdgeInsets.zero,
shadowColor: Colors.transparent,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(Dimens.borderRadiusL)),
surfaceTintColor: scheme.surfaceTint,
),
dialogBackgroundColor: scheme.surface, dialogBackgroundColor: scheme.surface,
dialogTheme: DialogTheme(backgroundColor: scheme.surface), dialogTheme: DialogTheme(
backgroundColor: scheme.surface,
surfaceTintColor: scheme.surfaceTint,
elevation: 6,
),
listTileTheme: ListTileThemeData(
style: ListTileStyle.list,
iconColor: scheme.onSurface,
textColor: scheme.onSurface,
),
scaffoldBackgroundColor: scheme.surface, scaffoldBackgroundColor: scheme.surface,
); );
} }

View file

@ -1,3 +1,5 @@
import 'package:flutter/material.dart';
/// `valueM` represents the base value. /// `valueM` represents the base value.
/// All other values differs by 8dp. /// All other values differs by 8dp.
class Dimens { class Dimens {
@ -13,6 +15,7 @@ class Dimens {
static const double grid56 = 56; static const double grid56 = 56;
static const double grid168 = 168; static const double grid168 = 168;
static const double paddingS = 8;
static const double paddingM = 16; static const double paddingM = 16;
static const double paddingL = 24; static const double paddingL = 24;
@ -31,4 +34,8 @@ class Dimens {
static const double cameraSliderTrackRadius = cameraSliderTrackHeight / 2; static const double cameraSliderTrackRadius = cameraSliderTrackHeight / 2;
static const double cameraSliderHandleSize = 32; static const double cameraSliderHandleSize = 32;
static const double cameraSliderHandleIconSize = cameraSliderHandleSize * 2 / 3; static const double cameraSliderHandleIconSize = cameraSliderHandleSize * 2 / 3;
// Dialog
// Taken from `Dialog` documentation
static const EdgeInsets dialogMargin = EdgeInsets.symmetric(horizontal: 40.0, vertical: 24.0);
} }

View file

@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:lightmeter/res/dimens.dart'; import 'package:lightmeter/res/dimens.dart';
import 'package:lightmeter/screens/metering/components/bottom_controls/components/shared/widget_circle_filled.dart'; import 'package:lightmeter/screens/shared/filled_circle/widget_circle_filled.dart';
class MeteringMeasureButton extends StatefulWidget { class MeteringMeasureButton extends StatefulWidget {
final double size; final double size;

View file

@ -1,31 +0,0 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/res/dimens.dart';
import 'package:lightmeter/screens/metering/components/bottom_controls/components/shared/widget_circle_filled.dart';
class MeteringSecondaryButton extends StatelessWidget {
final IconData icon;
final VoidCallback onPressed;
const MeteringSecondaryButton({
required this.icon,
required this.onPressed,
super.key,
});
@override
Widget build(BuildContext context) {
return Center(
child: FilledCircle(
color: Theme.of(context).colorScheme.surfaceVariant,
size: Dimens.grid48,
child: Center(
child: IconButton(
onPressed: onPressed,
color: Theme.of(context).colorScheme.onSurface,
icon: Icon(icon),
),
),
),
);
}
}

View file

@ -0,0 +1,39 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/res/dimens.dart';
import 'widget_bottom_controls.dart';
class MeteringBottomControlsProvider extends StatelessWidget {
final VoidCallback? onSwitchEvSourceType;
final VoidCallback onMeasure;
final VoidCallback onSettings;
const MeteringBottomControlsProvider({
required this.onSwitchEvSourceType,
required this.onMeasure,
required this.onSettings,
super.key,
});
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
return IconButtonTheme(
data: IconButtonThemeData(
style: ButtonStyle(
backgroundColor: MaterialStatePropertyAll(scheme.surface),
elevation: const MaterialStatePropertyAll(4),
iconColor: MaterialStatePropertyAll(scheme.onSurface),
shadowColor: const MaterialStatePropertyAll(Colors.transparent),
surfaceTintColor: MaterialStatePropertyAll(scheme.surfaceTint),
fixedSize: const MaterialStatePropertyAll(Size(Dimens.grid48, Dimens.grid48)),
),
),
child: MeteringBottomControls(
onSwitchEvSourceType: onSwitchEvSourceType,
onMeasure: onMeasure,
onSettings: onSettings,
),
);
}
}

View file

@ -4,7 +4,6 @@ import 'package:lightmeter/res/dimens.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'components/measure_button/widget_button_measure.dart'; import 'components/measure_button/widget_button_measure.dart';
import 'components/secondary_button/widget_button_secondary.dart';
class MeteringBottomControls extends StatelessWidget { class MeteringBottomControls extends StatelessWidget {
final VoidCallback? onSwitchEvSourceType; final VoidCallback? onSwitchEvSourceType;
@ -36,11 +35,13 @@ class MeteringBottomControls extends StatelessWidget {
children: [ children: [
if (onSwitchEvSourceType != null) if (onSwitchEvSourceType != null)
Expanded( Expanded(
child: MeteringSecondaryButton( child: Center(
onPressed: onSwitchEvSourceType!, child: IconButton(
icon: context.watch<EvSourceType>() != EvSourceType.camera onPressed: onSwitchEvSourceType,
? Icons.camera_rear icon: Icon(context.watch<EvSourceType>() != EvSourceType.camera
: Icons.wb_incandescent, ? Icons.camera_rear
: Icons.wb_incandescent),
),
), ),
) )
else else
@ -49,9 +50,11 @@ class MeteringBottomControls extends StatelessWidget {
onTap: onMeasure, onTap: onMeasure,
), ),
Expanded( Expanded(
child: MeteringSecondaryButton( child: Center(
onPressed: onSettings, child: IconButton(
icon: Icons.settings, onPressed: onSettings,
icon: const Icon(Icons.settings),
),
), ),
), ),
], ],

View file

@ -35,6 +35,8 @@ class AnimatedDialogState extends State<AnimatedDialog> with SingleTickerProvide
late final Animation<double> _borderRadiusAnimation; late final Animation<double> _borderRadiusAnimation;
late final Animation<double> _closedOpacityAnimation; late final Animation<double> _closedOpacityAnimation;
late final Animation<double> _openedOpacityAnimation; late final Animation<double> _openedOpacityAnimation;
late final Animation<Color?> _foregroundColorAnimation;
late final Animation<double> _elevationAnimation;
bool _isDialogShown = false; bool _isDialogShown = false;
@ -91,8 +93,10 @@ class AnimatedDialogState extends State<AnimatedDialog> with SingleTickerProvide
begin: _closedSize, begin: _closedSize,
end: widget.openedSize ?? end: widget.openedSize ??
Size( Size(
mediaQuery.size.width - mediaQuery.padding.horizontal - Dimens.paddingM * 2, mediaQuery.size.width -
mediaQuery.size.height - mediaQuery.padding.vertical - Dimens.paddingM * 2, mediaQuery.padding.horizontal -
Dimens.dialogMargin.horizontal,
mediaQuery.size.height - mediaQuery.padding.vertical - Dimens.dialogMargin.vertical,
), ),
); );
_sizeAnimation = _sizeTween.animate(_defaultCurvedAnimation); _sizeAnimation = _sizeTween.animate(_defaultCurvedAnimation);
@ -112,6 +116,20 @@ class AnimatedDialogState extends State<AnimatedDialog> with SingleTickerProvide
}); });
} }
@override
void didChangeDependencies() {
super.didChangeDependencies();
_foregroundColorAnimation = ColorTween(
begin: Theme.of(context).colorScheme.primaryContainer,
end: Theme.of(context).colorScheme.surface,
).animate(_defaultCurvedAnimation);
_elevationAnimation = Tween<double>(
begin: 0,
end: Theme.of(context).dialogTheme.elevation!,
).animate(_defaultCurvedAnimation);
}
@override @override
void dispose() { void dispose() {
_animationController.dispose(); _animationController.dispose();
@ -125,13 +143,7 @@ class AnimatedDialogState extends State<AnimatedDialog> with SingleTickerProvide
onTap: _openDialog, onTap: _openDialog,
child: Opacity( child: Opacity(
opacity: _isDialogShown ? 0 : 1, opacity: _isDialogShown ? 0 : 1,
child: ClipRRect( child: widget.child ?? widget.closedChild,
borderRadius: BorderRadius.circular(Dimens.borderRadiusM),
child: ColoredBox(
color: Theme.of(context).colorScheme.primaryContainer,
child: widget.child ?? widget.closedChild,
),
),
), ),
); );
} }
@ -151,6 +163,8 @@ class AnimatedDialogState extends State<AnimatedDialog> with SingleTickerProvide
sizeAnimation: _sizeAnimation, sizeAnimation: _sizeAnimation,
offsetAnimation: _offsetAnimation, offsetAnimation: _offsetAnimation,
borderRadiusAnimation: _borderRadiusAnimation, borderRadiusAnimation: _borderRadiusAnimation,
foregroundColorAnimation: _foregroundColorAnimation,
elevationAnimation: _elevationAnimation,
onDismiss: close, onDismiss: close,
builder: widget.closedChild != null && widget.openedChild != null builder: widget.closedChild != null && widget.openedChild != null
? (_) => _AnimatedSwitcher( ? (_) => _AnimatedSwitcher(
@ -195,6 +209,8 @@ class _AnimatedOverlay extends StatelessWidget {
final Animation<Size?> sizeAnimation; final Animation<Size?> sizeAnimation;
final Animation<Size?> offsetAnimation; final Animation<Size?> offsetAnimation;
final Animation<double> borderRadiusAnimation; final Animation<double> borderRadiusAnimation;
final Animation<Color?> foregroundColorAnimation;
final Animation<double> elevationAnimation;
final VoidCallback onDismiss; final VoidCallback onDismiss;
final Widget? child; final Widget? child;
final Widget Function(BuildContext context)? builder; final Widget Function(BuildContext context)? builder;
@ -205,6 +221,8 @@ class _AnimatedOverlay extends StatelessWidget {
required this.sizeAnimation, required this.sizeAnimation,
required this.offsetAnimation, required this.offsetAnimation,
required this.borderRadiusAnimation, required this.borderRadiusAnimation,
required this.foregroundColorAnimation,
required this.elevationAnimation,
required this.onDismiss, required this.onDismiss,
this.child, this.child,
this.builder, this.builder,
@ -236,7 +254,9 @@ class _AnimatedOverlay extends StatelessWidget {
child: ClipRRect( child: ClipRRect(
borderRadius: BorderRadius.circular(borderRadiusAnimation.value), borderRadius: BorderRadius.circular(borderRadiusAnimation.value),
child: Material( child: Material(
color: Theme.of(context).colorScheme.primaryContainer, elevation: elevationAnimation.value,
surfaceTintColor: Theme.of(context).colorScheme.surfaceTint,
color: foregroundColorAnimation.value,
child: builder?.call(context) ?? child, child: builder?.call(context) ?? child,
), ),
), ),

View file

@ -57,37 +57,34 @@ class _PhotographyValuePickerDialogState<T extends PhotographyValue>
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
ColoredBox( Column(
color: Theme.of(context).colorScheme.primaryContainer, children: [
child: Column( Padding(
children: [ padding: const EdgeInsets.fromLTRB(
Padding( Dimens.paddingL,
padding: const EdgeInsets.fromLTRB( Dimens.paddingL,
Dimens.paddingL, Dimens.paddingL,
Dimens.paddingL, Dimens.paddingM,
Dimens.paddingL,
Dimens.paddingM,
),
child: Column(
children: [
Text(
widget.title,
style: Theme.of(context).textTheme.headlineSmall!,
),
const SizedBox(height: Dimens.grid16),
Text(
widget.subtitle,
style: Theme.of(context).textTheme.bodyMedium!,
),
],
),
), ),
Divider( child: Column(
color: Theme.of(context).colorScheme.onPrimaryContainer, children: [
height: 0, Text(
widget.title,
style: Theme.of(context).textTheme.headlineSmall!,
),
const SizedBox(height: Dimens.grid16),
Text(
widget.subtitle,
style: Theme.of(context).textTheme.bodyMedium!,
),
],
), ),
], ),
), Divider(
color: Theme.of(context).colorScheme.onSurface,
height: 0,
),
],
), ),
Expanded( Expanded(
child: ListView.builder( child: ListView.builder(
@ -117,35 +114,32 @@ class _PhotographyValuePickerDialogState<T extends PhotographyValue>
), ),
), ),
), ),
ColoredBox( Column(
color: Theme.of(context).colorScheme.primaryContainer, children: [
child: Column( Divider(
children: [ color: Theme.of(context).colorScheme.onSurface,
Divider( height: 0,
color: Theme.of(context).colorScheme.onPrimaryContainer, ),
height: 0, Padding(
padding: const EdgeInsets.all(Dimens.paddingL),
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.max,
children: [
const Spacer(),
TextButton(
onPressed: widget.onCancel,
child: Text(S.of(context).cancel),
),
const SizedBox(width: Dimens.grid16),
TextButton(
onPressed: () => widget.onSelect(_selectedValue),
child: Text(S.of(context).select),
),
],
), ),
Padding( ),
padding: const EdgeInsets.all(Dimens.paddingL), ],
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.max,
children: [
const Spacer(),
TextButton(
onPressed: widget.onCancel,
child: Text(S.of(context).cancel),
),
const SizedBox(width: Dimens.grid16),
TextButton(
onPressed: () => widget.onSelect(_selectedValue),
child: Text(S.of(context).select),
),
],
),
),
],
),
), ),
], ],
); );

View file

@ -119,7 +119,7 @@ class _NdValueTile extends StatelessWidget {
onChanged: onChanged, onChanged: onChanged,
closedChild: ReadingValueContainer.singleValue( closedChild: ReadingValueContainer.singleValue(
value: ReadingValue( value: ReadingValue(
label: S.of(context).iso, label: S.of(context).nd,
value: value.value.toString(), value: value.value.toString(),
), ),
), ),

View file

@ -5,7 +5,7 @@ import 'package:lightmeter/data/models/photography_values/photography_value.dart
import 'package:lightmeter/environment.dart'; import 'package:lightmeter/environment.dart';
import 'package:lightmeter/providers/ev_source_type_provider.dart'; import 'package:lightmeter/providers/ev_source_type_provider.dart';
import 'components/bottom_controls/widget_bottom_controls.dart'; import 'components/bottom_controls/provider_bottom_controls.dart';
import 'components/camera_container/provider_container_camera.dart'; import 'components/camera_container/provider_container_camera.dart';
import 'components/light_sensor_container/provider_container_light_sensor.dart'; import 'components/light_sensor_container/provider_container_light_sensor.dart';
import 'bloc_metering.dart'; import 'bloc_metering.dart';
@ -57,7 +57,7 @@ class _MeteringScreenState extends State<MeteringScreen> {
), ),
), ),
), ),
MeteringBottomControls( MeteringBottomControlsProvider(
onSwitchEvSourceType: context.read<Environment>().hasLightSensor onSwitchEvSourceType: context.read<Environment>().hasLightSensor
? EvSourceTypeProvider.of(context).toggleType ? EvSourceTypeProvider.of(context).toggleType
: null, : null,

View file

@ -9,20 +9,6 @@ class DynamicColorListTile extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (context.read<DynamicColorState>() == DynamicColorState.unavailable) {
return Opacity(
opacity: 0.5,
child: IgnorePointer(
child: SwitchListTile(
secondary: const Icon(Icons.colorize),
title: Text(S.of(context).dynamicColor),
value: false,
enableFeedback: false,
onChanged: (value) {},
),
),
);
}
return SwitchListTile( return SwitchListTile(
secondary: const Icon(Icons.colorize), secondary: const Icon(Icons.colorize),
title: Text(S.of(context).dynamicColor), title: Text(S.of(context).dynamicColor),

View file

@ -0,0 +1,114 @@
import 'package:flutter/material.dart';
import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/providers/theme_provider.dart';
import 'package:lightmeter/res/dimens.dart';
import 'package:lightmeter/screens/shared/filled_circle/widget_circle_filled.dart';
class PrimaryColorDialogPicker extends StatefulWidget {
const PrimaryColorDialogPicker({super.key});
@override
State<PrimaryColorDialogPicker> createState() => _PrimaryColorDialogPickerState();
}
class _PrimaryColorDialogPickerState extends State<PrimaryColorDialogPicker> {
late Color _selected = Theme.of(context).primaryColor;
late final ScrollController _scrollController = ScrollController(
initialScrollOffset:
ThemeProvider.primaryColorsList.indexOf(_selected) * (Dimens.grid48 + Dimens.grid8),
);
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AlertDialog(
titlePadding: const EdgeInsets.fromLTRB(
Dimens.paddingL,
Dimens.paddingL,
Dimens.paddingL,
Dimens.paddingM,
),
title: Text(S.of(context).choosePrimaryColor),
content: SizedBox(
height: Dimens.grid48,
width: double.maxFinite,
child: ListView.separated(
controller: _scrollController,
scrollDirection: Axis.horizontal,
padding: EdgeInsets.zero,
separatorBuilder: (_, __) => const SizedBox(width: Dimens.grid8),
itemCount: ThemeProvider.primaryColorsList.length,
itemBuilder: (_, index) {
final color = ThemeProvider.primaryColorsList[index];
return _SelectableColorItem(
color: color,
selected: color.value == _selected.value,
onTap: () {
setState(() {
_selected = color;
});
},
);
},
),
),
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).save),
),
],
);
}
}
class _SelectableColorItem extends StatelessWidget {
final Color color;
final bool selected;
final VoidCallback onTap;
_SelectableColorItem({
required this.color,
required this.selected,
required this.onTap,
}) : super(key: ValueKey(color));
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: FilledCircle(
size: Dimens.grid48,
color: color,
child: AnimatedSwitcher(
duration: Dimens.durationS,
child: selected
? Icon(
Icons.check,
color: ThemeData.estimateBrightnessForColor(color) == Brightness.light
? Colors.black
: Colors.white,
)
: null,
),
),
);
}
}

View file

@ -0,0 +1,40 @@
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/providers/theme_provider.dart';
import 'components/primary_color_picker_dialog/widget_dialog_picker_primary_color.dart';
class PrimaryColorListTile extends StatelessWidget {
const PrimaryColorListTile({super.key});
@override
Widget build(BuildContext context) {
if (context.watch<DynamicColorState>() == DynamicColorState.enabled) {
return Opacity(
opacity: 0.5,
child: IgnorePointer(
child: ListTile(
leading: const Icon(Icons.palette),
title: Text(S.of(context).primaryColor),
),
),
);
}
return ListTile(
leading: const Icon(Icons.palette),
title: Text(S.of(context).primaryColor),
onTap: () {
showDialog<Color>(
context: context,
builder: (_) => const PrimaryColorDialogPicker(),
).then((value) {
if (value != null) {
ThemeProvider.of(context).setPrimaryColor(value);
}
});
},
);
}
}

View file

@ -20,10 +20,7 @@ class SettingsSection extends StatelessWidget {
Dimens.paddingM, Dimens.paddingM,
Dimens.paddingM, Dimens.paddingM,
), ),
child: Material( child: Card(
clipBehavior: Clip.antiAlias,
borderRadius: BorderRadius.circular(Dimens.borderRadiusL),
color: Theme.of(context).colorScheme.primaryContainer,
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(vertical: Dimens.paddingM), padding: const EdgeInsets.symmetric(vertical: Dimens.paddingM),
child: Column( child: Column(
@ -34,7 +31,10 @@ class SettingsSection extends StatelessWidget {
padding: const EdgeInsets.only(left: Dimens.paddingM), padding: const EdgeInsets.only(left: Dimens.paddingM),
child: Text( child: Text(
title, title,
style: Theme.of(context).textTheme.labelLarge, style: Theme.of(context)
.textTheme
.labelLarge
?.copyWith(color: Theme.of(context).colorScheme.onSurface),
), ),
), ),
...children, ...children,

View file

@ -1,9 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:lightmeter/data/models/dynamic_colors_state.dart';
import 'package:lightmeter/generated/l10n.dart'; import 'package:lightmeter/generated/l10n.dart';
import 'package:lightmeter/res/dimens.dart'; import 'package:lightmeter/res/dimens.dart';
import 'package:provider/provider.dart';
import 'components/calibration/widget_list_tile_calibration.dart'; import 'components/calibration/widget_list_tile_calibration.dart';
import 'components/haptics/provider_list_tile_haptics.dart'; import 'components/haptics/provider_list_tile_haptics.dart';
import 'components/primary_color/widget_list_tile_primary_color.dart';
import 'components/report_issue/widget_list_tile_report_issue.dart'; import 'components/report_issue/widget_list_tile_report_issue.dart';
import 'components/shared/settings_section/widget_settings_section.dart'; import 'components/shared/settings_section/widget_settings_section.dart';
import 'components/source_code/widget_list_tile_source_code.dart'; import 'components/source_code/widget_list_tile_source_code.dart';
@ -63,9 +66,11 @@ class SettingsScreen extends StatelessWidget {
), ),
SettingsSection( SettingsSection(
title: S.of(context).theme, title: S.of(context).theme,
children: const [ children: [
ThemeTypeListTile(), const ThemeTypeListTile(),
DynamicColorListTile(), const PrimaryColorListTile(),
if (context.read<DynamicColorState>() != DynamicColorState.unavailable)
const DynamicColorListTile(),
], ],
), ),
SettingsSection( SettingsSection(