Files
GroundedHelper/frontend/lib/Grounded/see/routine/StepEditorState.dart
alvocool d3431ca5de Switch to Inter, round the corners softly and make the nav switch tabs
Inter replaces General Sans (Inter Display for titles and big numbers).
Corners come back from before the squaring commit, scaled to a soft set
(16 cards, 14 buttons, 12 rows, 8 chips) and applied to the screens built
since. The nav is now a cluster of round frosted buttons that switches
Today, Routines, Goals, Groups and Progress in place inside Home: no
pushed pages, no back arrows, and Android back returns to Today.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KhQh614DMeYoQEH9SoMRUw
2026-11-09 07:08:46 +03:00

363 lines
10 KiB
Dart

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<StepEditor> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
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(16),
topRight: Radius.circular(16),
),
),
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<StepMode>(
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<Widget> _numbers() {
final List<Widget> rows = <Widget>[];
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(10),
),
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();
}
}