feat(train): interval plans, synced and sealed, with an editor and templates (ADR 0030, D42)

Train's server gains the synced `plan` table, a sealed payload alone;
the migration `plans` only adds it, its space index and the foreign key
to its sync space, and a person's deletion takes it with their space.

The app keeps plans as `plan` rows sealed for their row: a name, what
they are for, whether a step off target is told, and blocks of steps
repeated, each ending after a time, a distance or a lap tap and aiming
at a pace, speed, heart rate or zone, or the rower's split, watts or
stroke rate. Home's Plans tab lists them, a new one starts from nothing
or from a template (4×4 minutes, Tabata, 10×400 m), the editor changes
steps on a sheet, and a deletion asks first and can be undone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-28 22:35:22 +01:00
commit f406ddcc59
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -1468,6 +1468,426 @@ abstract class AppLocalizations {
/// **'Days'**
String get homeDays;
/// No description provided for @homePlans.
///
/// In en, this message translates to:
/// **'Plans'**
String get homePlans;
/// No description provided for @plansNew.
///
/// In en, this message translates to:
/// **'New plan'**
String get plansNew;
/// No description provided for @plansNewEmpty.
///
/// In en, this message translates to:
/// **'From nothing'**
String get plansNewEmpty;
/// No description provided for @plansEmptyTitle.
///
/// In en, this message translates to:
/// **'No plans yet'**
String get plansEmptyTitle;
/// No description provided for @plansEmptyHint.
///
/// In en, this message translates to:
/// **'A plan\'s steps lead you through intervals as you record, with a tone, a vibration and a voice at each change. Start from a template, or from nothing.'**
String get plansEmptyHint;
/// No description provided for @planLocked.
///
/// In en, this message translates to:
/// **'A plan that doesn\'t open here'**
String get planLocked;
/// No description provided for @planTemplateFourByFour.
///
/// In en, this message translates to:
/// **'4×4 minutes'**
String get planTemplateFourByFour;
/// No description provided for @planTemplateTabata.
///
/// In en, this message translates to:
/// **'Tabata'**
String get planTemplateTabata;
/// No description provided for @planTemplateFourHundreds.
///
/// In en, this message translates to:
/// **'10×400 m'**
String get planTemplateFourHundreds;
/// No description provided for @planTitle.
///
/// In en, this message translates to:
/// **'Plan'**
String get planTitle;
/// No description provided for @planNewTitle.
///
/// In en, this message translates to:
/// **'New plan'**
String get planNewTitle;
/// No description provided for @planName.
///
/// In en, this message translates to:
/// **'Name'**
String get planName;
/// No description provided for @planActivity.
///
/// In en, this message translates to:
/// **'For'**
String get planActivity;
/// No description provided for @planActivityAny.
///
/// In en, this message translates to:
/// **'Any activity'**
String get planActivityAny;
/// No description provided for @planOffTarget.
///
/// In en, this message translates to:
/// **'Tell me when I\'m off target'**
String get planOffTarget;
/// No description provided for @planOffTargetHint.
///
/// In en, this message translates to:
/// **'A short cue at most every 30 seconds.'**
String get planOffTargetHint;
/// No description provided for @planSave.
///
/// In en, this message translates to:
/// **'Save'**
String get planSave;
/// No description provided for @planNotSaved.
///
/// In en, this message translates to:
/// **'The plan wasn\'t saved.'**
String get planNotSaved;
/// No description provided for @planAddStep.
///
/// In en, this message translates to:
/// **'Add a step'**
String get planAddStep;
/// No description provided for @planAddRepeat.
///
/// In en, this message translates to:
/// **'Add a repeat'**
String get planAddRepeat;
/// No description provided for @planRepeat.
///
/// In en, this message translates to:
/// **'{count}×'**
String planRepeat(int count);
/// No description provided for @planFewer.
///
/// In en, this message translates to:
/// **'Fewer rounds'**
String get planFewer;
/// No description provided for @planMore.
///
/// In en, this message translates to:
/// **'More rounds'**
String get planMore;
/// No description provided for @planBlockMenu.
///
/// In en, this message translates to:
/// **'More'**
String get planBlockMenu;
/// No description provided for @planMoveUp.
///
/// In en, this message translates to:
/// **'Move up'**
String get planMoveUp;
/// No description provided for @planMoveDown.
///
/// In en, this message translates to:
/// **'Move down'**
String get planMoveDown;
/// No description provided for @planRemove.
///
/// In en, this message translates to:
/// **'Remove'**
String get planRemove;
/// No description provided for @planDelete.
///
/// In en, this message translates to:
/// **'Delete'**
String get planDelete;
/// No description provided for @planDeleteTitle.
///
/// In en, this message translates to:
/// **'Delete this plan?'**
String get planDeleteTitle;
/// No description provided for @planDeleteMessage.
///
/// In en, this message translates to:
/// **'It\'s deleted on all your devices; workouts recorded with it keep their steps.'**
String get planDeleteMessage;
/// No description provided for @planDeleted.
///
/// In en, this message translates to:
/// **'Plan deleted'**
String get planDeleted;
/// No description provided for @planSteps.
///
/// In en, this message translates to:
/// **'{count, plural, =1{1 step} other{{count} steps}}'**
String planSteps(int count);
/// No description provided for @stepTitle.
///
/// In en, this message translates to:
/// **'Step'**
String get stepTitle;
/// No description provided for @stepKind.
///
/// In en, this message translates to:
/// **'Kind'**
String get stepKind;
/// No description provided for @stepWarmUp.
///
/// In en, this message translates to:
/// **'Warm-up'**
String get stepWarmUp;
/// No description provided for @stepWork.
///
/// In en, this message translates to:
/// **'Work'**
String get stepWork;
/// No description provided for @stepRecovery.
///
/// In en, this message translates to:
/// **'Recovery'**
String get stepRecovery;
/// No description provided for @stepCoolDown.
///
/// In en, this message translates to:
/// **'Cool-down'**
String get stepCoolDown;
/// No description provided for @stepEnds.
///
/// In en, this message translates to:
/// **'Ends'**
String get stepEnds;
/// No description provided for @stepEndTime.
///
/// In en, this message translates to:
/// **'After a time'**
String get stepEndTime;
/// No description provided for @stepEndDistance.
///
/// In en, this message translates to:
/// **'After a distance'**
String get stepEndDistance;
/// No description provided for @stepEndLap.
///
/// In en, this message translates to:
/// **'With a lap tap'**
String get stepEndLap;
/// No description provided for @stepTimeLabel.
///
/// In en, this message translates to:
/// **'Time (min:s)'**
String get stepTimeLabel;
/// No description provided for @stepMetresLabel.
///
/// In en, this message translates to:
/// **'Metres'**
String get stepMetresLabel;
/// No description provided for @stepTarget.
///
/// In en, this message translates to:
/// **'Target'**
String get stepTarget;
/// No description provided for @stepZone.
///
/// In en, this message translates to:
/// **'Zone'**
String get stepZone;
/// No description provided for @stepDone.
///
/// In en, this message translates to:
/// **'Done'**
String get stepDone;
/// No description provided for @stepMinutes.
///
/// In en, this message translates to:
/// **'{minutes} min'**
String stepMinutes(int minutes);
/// No description provided for @stepSeconds.
///
/// In en, this message translates to:
/// **'{seconds} s'**
String stepSeconds(int seconds);
/// No description provided for @stepMinutesSeconds.
///
/// In en, this message translates to:
/// **'{minutes} min {seconds} s'**
String stepMinutesSeconds(int minutes, int seconds);
/// No description provided for @stepMetres.
///
/// In en, this message translates to:
/// **'{metres} m'**
String stepMetres(int metres);
/// No description provided for @stepKilometres.
///
/// In en, this message translates to:
/// **'{km} km'**
String stepKilometres(String km);
/// No description provided for @stepUntilLap.
///
/// In en, this message translates to:
/// **'until a lap tap'**
String get stepUntilLap;
/// No description provided for @targetNone.
///
/// In en, this message translates to:
/// **'None'**
String get targetNone;
/// No description provided for @targetFrom.
///
/// In en, this message translates to:
/// **'From'**
String get targetFrom;
/// No description provided for @targetTo.
///
/// In en, this message translates to:
/// **'To'**
String get targetTo;
/// No description provided for @targetKindPace.
///
/// In en, this message translates to:
/// **'Pace (per km)'**
String get targetKindPace;
/// No description provided for @targetKindSpeed.
///
/// In en, this message translates to:
/// **'Speed (km/h)'**
String get targetKindSpeed;
/// No description provided for @targetKindHeartRate.
///
/// In en, this message translates to:
/// **'Heart rate (bpm)'**
String get targetKindHeartRate;
/// No description provided for @targetKindHeartZone.
///
/// In en, this message translates to:
/// **'Heart-rate zone'**
String get targetKindHeartZone;
/// No description provided for @targetKindSplit.
///
/// In en, this message translates to:
/// **'Split (per 500 m)'**
String get targetKindSplit;
/// No description provided for @targetKindWatts.
///
/// In en, this message translates to:
/// **'Power (W)'**
String get targetKindWatts;
/// No description provided for @targetKindStrokeRate.
///
/// In en, this message translates to:
/// **'Stroke rate (per min)'**
String get targetKindStrokeRate;
/// No description provided for @targetPace.
///
/// In en, this message translates to:
/// **'{from}–{to} per km'**
String targetPace(String from, String to);
/// No description provided for @targetSpeed.
///
/// In en, this message translates to:
/// **'{from}–{to} km/h'**
String targetSpeed(String from, String to);
/// No description provided for @targetHeartRate.
///
/// In en, this message translates to:
/// **'{from}–{to} bpm'**
String targetHeartRate(String from, String to);
/// No description provided for @targetZone.
///
/// In en, this message translates to:
/// **'Zone {zone}'**
String targetZone(int zone);
/// No description provided for @targetSplit.
///
/// In en, this message translates to:
/// **'{from}–{to} per 500 m'**
String targetSplit(String from, String to);
/// No description provided for @targetWatts.
///
/// In en, this message translates to:
/// **'{from}–{to} W'**
String targetWatts(String from, String to);
/// No description provided for @targetStrokeRate.
///
/// In en, this message translates to:
/// **'{from}–{to} strokes/min'**
String targetStrokeRate(String from, String to);
/// No description provided for @daysEmptyTitle.
///
/// In en, this message translates to: