Files
ZenXercise/lib/src/plans/views/step_editor_sheet.dart
T

219 lines
7.8 KiB
Dart
Raw Normal View History

import 'package:flutter/material.dart';
import '../../design/number_stepper.dart';
import '../../design/zen_style.dart';
import '../../localization/app_localizations.dart';
import '../models/plan.dart';
import '../plan_labels.dart';
/// Edits a Step, a Superset or a Superset member in a bottom sheet. Every
/// change is handed to [onChanged] right away; there is no separate save.
Future<void> showStepEditor<T>(
BuildContext context, {
required String title,
required T value,
required ValueChanged<T> onChanged,
}) => showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
showDragHandle: true,
builder: (context) => _StepEditorSheet<T>(title: title, initial: value, onChanged: onChanged),
);
class _StepEditorSheet<T> extends StatefulWidget {
const _StepEditorSheet({required this.title, required this.initial, required this.onChanged});
final String title;
final T initial;
final ValueChanged<T> onChanged;
@override
State<_StepEditorSheet<T>> createState() => _StepEditorSheetState<T>();
}
class _StepEditorSheetState<T> extends State<_StepEditorSheet<T>> {
late T _value = widget.initial;
void _set(Object value) {
setState(() => _value = value as T);
widget.onChanged(_value);
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
final text = Theme.of(context).textTheme;
final value = _value;
return SafeArea(
child: Padding(
padding: const EdgeInsets.fromLTRB(20, 0, 20, 16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(widget.title, style: text.titleLarge),
const SizedBox(height: 12),
...switch (value) {
WorkoutStep() => [
_setsStepper(l10n, value.sets, (n) => _set(value.copyWith(sets: n))),
_TargetEditor(
target: value.target,
onChanged: (t) => _set(value.copyWith(target: t)),
),
_WeightEditor(
weightKg: value.weightKg,
onChanged: (kg) => _set(value.copyWith(weightKg: () => kg)),
),
],
SupersetMember() => [
_TargetEditor(
target: value.target,
onChanged: (t) => _set(value.copyWith(target: t)),
),
_WeightEditor(
weightKg: value.weightKg,
onChanged: (kg) => _set(value.copyWith(weightKg: () => kg)),
),
],
Superset() => [
_setsStepper(l10n, value.sets, (n) => _set(value.copyWith(sets: n))),
SwitchListTile(
contentPadding: EdgeInsets.zero,
title: Text(l10n.restBetweenLabel, style: text.titleSmall),
subtitle: Text(l10n.restBetweenHint),
value: value.restBetween,
onChanged: (on) => _set(value.copyWith(restBetween: on)),
),
],
StretchingStep() => [
NumberStepper(
label: l10n.stepHold,
value: l10n.secondsShort(value.holdSeconds),
onDecrement: value.holdSeconds > 5
? () => _set(value.copyWith(holdSeconds: value.holdSeconds - 5))
: null,
onIncrement: value.holdSeconds < 300
? () => _set(value.copyWith(holdSeconds: value.holdSeconds + 5))
: null,
),
SwitchListTile(
contentPadding: EdgeInsets.zero,
title: Text(l10n.stepBothSides, style: text.titleSmall),
subtitle: Text(l10n.stepBothSidesHint),
value: value.bothSides,
onChanged: (on) => _set(value.copyWith(bothSides: on)),
),
NumberStepper(
label: l10n.stepRounds,
value: '${value.rounds}',
onDecrement: value.rounds > 1 ? () => _set(value.copyWith(rounds: value.rounds - 1)) : null,
onIncrement: value.rounds < 10 ? () => _set(value.copyWith(rounds: value.rounds + 1)) : null,
),
],
_ => const <Widget>[],
},
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
height: 48,
child: FilledButton(onPressed: () => Navigator.of(context).pop(), child: Text(l10n.done)),
),
],
),
),
);
}
Widget _setsStepper(AppLocalizations l10n, int sets, ValueChanged<int> onChanged) => NumberStepper(
label: l10n.stepSets,
value: '$sets',
onDecrement: sets > 1 ? () => onChanged(sets - 1) : null,
onIncrement: sets < 20 ? () => onChanged(sets + 1) : null,
);
}
/// Reps or time per set, switching between the two with sensible starting values.
class _TargetEditor extends StatelessWidget {
const _TargetEditor({required this.target, required this.onChanged});
final WorkoutTarget target;
final ValueChanged<WorkoutTarget> onChanged;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 8),
SegmentedButton<bool>(
showSelectedIcon: false,
segments: [
ButtonSegment(value: false, label: Text(l10n.stepReps)),
ButtonSegment(value: true, label: Text(l10n.stepTime)),
],
selected: {target.isTimed},
onSelectionChanged: (timed) =>
onChanged(timed.single ? const WorkoutTarget.seconds(30) : const WorkoutTarget.reps(10)),
),
if (target.isTimed)
NumberStepper(
label: l10n.stepTime,
value: l10n.secondsShort(target.seconds!),
onDecrement: target.seconds! > 5 ? () => onChanged(WorkoutTarget.seconds(target.seconds! - 5)) : null,
onIncrement: target.seconds! < 600 ? () => onChanged(WorkoutTarget.seconds(target.seconds! + 5)) : null,
)
else
NumberStepper(
label: l10n.stepReps,
value: '${target.reps}',
onDecrement: target.reps! > 1 ? () => onChanged(WorkoutTarget.reps(target.reps! - 1)) : null,
onIncrement: target.reps! < 100 ? () => onChanged(WorkoutTarget.reps(target.reps! + 1)) : null,
),
],
);
}
}
/// An optional target weight in 2.5 kg steps.
class _WeightEditor extends StatelessWidget {
const _WeightEditor({required this.weightKg, required this.onChanged});
static const step = 2.5;
final double? weightKg;
final ValueChanged<double?> onChanged;
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
final s = ZenStyle.of(context);
final kg = weightKg;
if (kg == null) {
return Padding(
padding: const EdgeInsets.only(top: 8),
child: OutlinedButton.icon(
onPressed: () => onChanged(20),
icon: const Icon(Icons.add_rounded),
label: Text(l10n.addWeight),
),
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
NumberStepper(
label: l10n.stepWeight,
value: l10n.weightKg(kgNumber(kg)),
onDecrement: kg >= step ? () => onChanged(kg - step) : null,
onIncrement: kg < 500 ? () => onChanged(kg + step) : null,
),
TextButton(
onPressed: () => onChanged(null),
style: TextButton.styleFrom(foregroundColor: s.inkMuted, padding: EdgeInsets.zero),
child: Text(l10n.removeWeight),
),
],
);
}
}