import 'package:flutter/cupertino.dart'; import '../../about/external/data/RoutineStep.dart'; import '../../about/internal/application/RoutineKind.dart'; import '../../about/internal/application/StepMode.dart'; import '../../about/internal/application/TextType.dart'; import '../../designs/buttons/Buttons.dart'; import '../../designs/input/CountStepper.dart'; import '../../designs/input/InputFields.dart'; import '../../designs/text/Text.dart'; import '../../utils/Colors.dart'; import '../../utils/CommonUtils.dart'; import '../../utils/Validators.dart'; import 'StepEditor.dart'; class StepEditorState extends State { final GlobalKey _formKey = GlobalKey(); final TextEditingController _title = TextEditingController(); final TextEditingController _cue = TextEditingController(); late RoutineStep _step; bool get _editing { return widget.step != null; } @override void initState() { super.initState(); _step = widget.step == null ? _defaultsFor(widget.kind) : RoutineStep.fromJson(widget.step!.toJson()); _title.text = _step.title; _cue.text = _step.cue; } /// A new step starts as the kind's most common shape, so most edits are /// just a title. RoutineStep _defaultsFor(RoutineKind kind) { switch (kind) { case RoutineKind.Workout: return RoutineStep( mode: StepMode.Reps, sets: 3, reps: 10, timerSeconds: 40, restSeconds: 60); case RoutineKind.Meditation: return RoutineStep(mode: StepMode.Timed, timerSeconds: 300); case RoutineKind.MustDo: return RoutineStep(mode: StepMode.Check, timerSeconds: 120); } } @override Widget build(BuildContext context) { return Padding( padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), child: Container( constraints: BoxConstraints( maxHeight: MediaQuery.of(context).size.height * 0.9, ), decoration: const BoxDecoration( color: colorSheetBackground, borderRadius: BorderRadius.only( topLeft: Radius.circular(28), topRight: Radius.circular(28), ), ), child: SingleChildScrollView( padding: const EdgeInsets.fromLTRB(24, 24, 24, 28), child: Form( key: _formKey, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ text(_editing ? "EDIT STEP" : "NEW STEP", 9, TextType.Bold, color: kindColor(widget.kind), letterSpacing: 1.2), const SizedBox(height: 10), text(_title.text.isEmpty ? "What happens here?" : _title.text, 24, TextType.Light, color: colorPrimaryDark, height: 1.2), const SizedBox(height: 20), inputField( "Title", _title, hint: _titleHint(), validator: Validators.title, onChanged: _onTitleChanged, ), const SizedBox(height: 20), text("MODE", 10, TextType.Bold, color: colorGrey2, letterSpacing: 0.8), const SizedBox(height: 8), segmentedSelector( options: StepMode.values, selected: _step.mode, label: stepModeLabel, onSelected: _onModeSelected, activeColor: kindColor(widget.kind), ), const SizedBox(height: 8), text(_modeDescription(_step.mode), 12, TextType.Regular, color: colorGrey2, height: 1.5), const SizedBox(height: 20), ..._numbers(), const SizedBox(height: 20), inputField( "Cue", _cue, hint: widget.kind == RoutineKind.Meditation ? "In 4 · hold 4 · out 4" : "One line to read mid-step (optional)", ), const SizedBox(height: 16), _summaryLine(), const SizedBox(height: 20), roundedCornerButton( _editing ? "Update step" : "Add step", _onSave, background: colorPrimaryDark, icon: _editing ? CupertinoIcons.checkmark : CupertinoIcons.add, ), if (_editing) ...[ const SizedBox(height: 6), Center( child: textButton("Remove step", _onRemove, color: colorDestructive, textSize: 13), ), ], ], ), ), ), ), ); } String _titleHint() { switch (widget.kind) { case RoutineKind.Workout: return "Deadlifts, plank, warm up…"; case RoutineKind.Meditation: return "Settle, breathe, body scan…"; case RoutineKind.MustDo: return "Make the bed, plan tomorrow…"; } } String _modeDescription(StepMode mode) { switch (mode) { case StepMode.Timed: return "The clock runs down and the next beat starts on its own."; case StepMode.Reps: return "You end each set when the reps are done. Rest is timed."; case StepMode.Check: return "A task. You tick it; the clock records how long it took."; } } /// The steppers that apply to the chosen mode. List _numbers() { final List rows = []; switch (_step.mode) { case StepMode.Timed: rows.add(_pair( countStepper("Duration", formatSeconds(_step.timerSeconds), onMinus: _onDurationMinus, onPlus: _onDurationPlus, canMinus: _step.timerSeconds > 5), countStepper("Sets", "${_step.sets}", onMinus: _onSetsMinus, onPlus: _onSetsPlus, canMinus: _step.sets > 1), )); break; case StepMode.Reps: rows.add(_pair( countStepper("Reps", "${_step.reps}", onMinus: _onRepsMinus, onPlus: _onRepsPlus, canMinus: _step.reps > 1), countStepper("Sets", "${_step.sets}", onMinus: _onSetsMinus, onPlus: _onSetsPlus, canMinus: _step.sets > 1), )); rows.add(const SizedBox(height: 10)); rows.add(countStepper("Est. per set", formatSeconds(_step.timerSeconds), onMinus: _onDurationMinus, onPlus: _onDurationPlus, canMinus: _step.timerSeconds > 5)); break; case StepMode.Check: rows.add(countStepper("Estimate", formatSeconds(_step.timerSeconds), onMinus: _onDurationMinus, onPlus: _onDurationPlus, canMinus: _step.timerSeconds > 5)); break; } if (_step.mode != StepMode.Check) { rows.add(const SizedBox(height: 10)); rows.add(countStepper( "Rest after each set", _step.restSeconds == 0 ? "None" : formatSeconds(_step.restSeconds), onMinus: _onRestMinus, onPlus: _onRestPlus, canMinus: _step.restSeconds > 0, )); } return rows; } Widget _pair(Widget left, Widget right) { return Row( children: [ Expanded(child: left), const SizedBox(width: 10), Expanded(child: right), ], ); } Widget _summaryLine() { return Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), decoration: BoxDecoration( color: kindBackground(widget.kind), borderRadius: BorderRadius.circular(12), ), child: Row( children: [ Icon(stepModeIcon(_step.mode), size: 15, color: kindColor(widget.kind)), const SizedBox(width: 10), Expanded( child: text(stepSummary(_step), 12, TextType.Medium, color: colorPrimaryDark), ), text("~${formatSeconds(_step.plannedSeconds)}", 12, TextType.Bold, color: kindColor(widget.kind)), ], ), ); } // ── Handlers ────────────────────────────────────────────────────────────── void _onTitleChanged(String value) { setState(() {}); } void _onModeSelected(StepMode mode) { setState(() { _step.mode = mode; if (mode == StepMode.Reps && _step.reps == 0) { _step.reps = 10; } if (mode == StepMode.Check) { _step.sets = 1; _step.restSeconds = 0; } }); } /// Steps get coarser as the duration grows: 5s near the bottom, a minute /// once you are into meditation lengths. int _durationIncrement(int seconds) { if (seconds < 60) { return 5; } if (seconds < 180) { return 15; } if (seconds < 600) { return 30; } return 60; } void _onDurationMinus() { setState(() { final int next = _step.timerSeconds - _durationIncrement(_step.timerSeconds - 1); _step.timerSeconds = next < 5 ? 5 : next; }); } void _onDurationPlus() { setState(() { _step.timerSeconds = _step.timerSeconds + _durationIncrement(_step.timerSeconds); }); } void _onSetsMinus() { setState(() { _step.sets = _step.sets > 1 ? _step.sets - 1 : 1; }); } void _onSetsPlus() { setState(() { _step.sets = _step.sets + 1; }); } void _onRepsMinus() { setState(() { _step.reps = _step.reps > 1 ? _step.reps - 1 : 1; }); } void _onRepsPlus() { setState(() { _step.reps = _step.reps + 1; }); } void _onRestMinus() { setState(() { final int next = _step.restSeconds - 15; _step.restSeconds = next < 0 ? 0 : next; }); } void _onRestPlus() { setState(() { _step.restSeconds = _step.restSeconds + 15; }); } void _onSave() { if (_formKey.currentState?.validate() != true) { return; } _step.title = _title.text.trim(); _step.cue = _cue.text.trim(); Navigator.pop(context, _step); } void _onRemove() { Navigator.pop(context, false); } @override void dispose() { _title.dispose(); _cue.dispose(); super.dispose(); } }