feat(ui): add gear preset shortcuts

This commit is contained in:
2026-04-23 22:37:52 +02:00
parent ddaed084dc
commit 16ac66471a
2 changed files with 132 additions and 7 deletions

View File

@ -806,6 +806,63 @@ class _DeviceDetailsPageState extends ConsumerState<DeviceDetailsPage> {
_isFirmwareUpdateBusy ? null : _loadGearRatios,
onSave: _saveGearRatios,
presets: const [
GearRatioPreset(
name: 'Road',
description:
'Balanced 12-speed road gearing for steady cadence steps.',
ratios: [
0.50,
0.58,
0.67,
0.76,
0.86,
0.97,
1.09,
1.22,
1.36,
1.51,
1.67,
1.84,
],
),
GearRatioPreset(
name: 'Gravel',
description:
'Slightly lower gearing with smooth jumps for mixed terrain rides.',
ratios: [
0.46,
0.54,
0.62,
0.70,
0.79,
0.89,
1.00,
1.12,
1.25,
1.40,
1.57,
1.76,
],
),
GearRatioPreset(
name: 'MTB',
description:
'Lower climbing gears with wider top-end spacing for steep trails.',
ratios: [
0.42,
0.49,
0.57,
0.66,
0.76,
0.87,
1.00,
1.15,
1.32,
1.52,
1.75,
2.02,
],
),
GearRatioPreset(
name: 'KeAnt Classic',
description:

View File

@ -14,6 +14,42 @@ class GearRatioPreset {
final List<double> ratios;
}
class _QuickPresetButton extends StatelessWidget {
const _QuickPresetButton({
required this.label,
required this.onPressed,
this.icon,
});
final String label;
final VoidCallback onPressed;
final IconData? icon;
@override
Widget build(BuildContext context) {
final buttonStyle = OutlinedButton.styleFrom(
minimumSize: const Size(0, 42),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
);
if (icon != null) {
return OutlinedButton.icon(
onPressed: onPressed,
style: buttonStyle,
icon: Icon(icon, size: 18),
label: Text(label),
);
}
return OutlinedButton(
onPressed: onPressed,
style: buttonStyle,
child: Text(label),
);
}
}
class GearRatioEditorCard extends StatefulWidget {
const GearRatioEditorCard({
required this.ratios,
@ -60,6 +96,12 @@ class _GearRatioEditorCardState extends State<GearRatioEditorCard> {
List<double>? _stretchBase;
int _gearLayoutVersion = 0;
static const List<String> _quickPresetNames = [
'Road',
'Gravel',
'MTB',
];
List<double> _committed = const [];
List<double> _draft = const [];
int _committedDefaultGearIndex = 0;
@ -235,6 +277,25 @@ class _GearRatioEditorCardState extends State<GearRatioEditorCard> {
: const SizedBox.shrink(),
),
if (_isEditing) ...[
Padding(
padding: const EdgeInsets.fromLTRB(14, 0, 14, 10),
child: Wrap(
spacing: 8,
runSpacing: 8,
children: [
for (final presetName in _quickPresetNames)
_QuickPresetButton(
label: presetName,
onPressed: () => _applyNamedPreset(presetName),
),
_QuickPresetButton(
label: 'Load',
icon: Icons.folder_open_rounded,
onPressed: _openPresetPicker,
),
],
),
),
Padding(
padding: const EdgeInsets.fromLTRB(14, 0, 14, 8),
child: Row(
@ -251,12 +312,6 @@ class _GearRatioEditorCardState extends State<GearRatioEditorCard> {
});
},
),
const Spacer(),
OutlinedButton.icon(
onPressed: _openPresetPicker,
icon: const Icon(Icons.tune),
label: const Text('Load preset'),
),
],
),
),
@ -749,8 +804,21 @@ class _GearRatioEditorCardState extends State<GearRatioEditorCard> {
return;
}
_loadPreset(selected);
}
void _applyNamedPreset(String name) {
for (final preset in widget.presets) {
if (preset.name.toLowerCase() == name.toLowerCase()) {
_loadPreset(preset);
return;
}
}
}
void _loadPreset(GearRatioPreset preset) {
setState(() {
_draft = selected.ratios.map(_quantizeRatio).toList(growable: false);
_draft = preset.ratios.map(_quantizeRatio).toList(growable: false);
_draftDefaultGearIndex = _normalizeDefaultIndex(0, _draft.length);
if (_sortAscending) {
_sortDraft(animate: true);