Add per-split rest length and hands-free auto-advance flow

Two per-split settings, with the global Settings values as defaults:

- restSeconds: Int? — per-split rest, used between sets and (in flow) between
  exercises; nil falls back to the global default.
- autoAdvance: Bool? — flow mode: finishing an exercise rests, then opens the
  next one hands-free, all the way through the split.

Both are optional, snapshotted onto WorkoutDocument at the start sites (no live
split link), and not schema-bumped — same degradation pattern as activityType.

A thin RunFlowView wrapper (iOS + watch) owns the on-screen log and swaps it via
.id(currentLogID) on hand-off, so the per-exercise ExerciseProgressView stays
per-logID and untouched; the between-exercise rest reuses the existing .rest
countdown as the terminal page. The mirror reuses the per-logID live channel:
the wrapper suppresses the boundary .ended teardown so it follows across
exercises, and ContentView re-keys the cover on frame.logID — no sync-bridge
changes.

Morning Wake-Up ships as a flowing 45s-work / 15s-rest routine.

New Rest & Pacing section in the split editor exposes both controls.
This commit is contained in:
2026-07-09 15:18:13 -04:00
parent 0f5f11e5e2
commit 90deb582fe
18 changed files with 468 additions and 92 deletions
+14 -11
View File
@@ -66,6 +66,8 @@ struct SplitDocument: Codable {
var updatedAt: Date
var exercises: [ExerciseDocument]
var activityType: Int?
var restSeconds: Int? = nil
var autoAdvance: Bool? = nil
}
// MARK: - Seed data (canonical source for Workouts/Resources/StarterSplits/*.json)
@@ -74,7 +76,7 @@ struct SplitDocument: Codable {
// (empty = nothing recorded yet), which lights up the machine-settings UI without
// the user having to flip the editor toggle first.
struct Ex { let name: String; var weight: Double = 0; var sets = 4; var reps = 10; var load = 1; var dur = 0; var machine = false }
struct Sp { let name: String; let color: String; let icon: String; let activity: Int; let ex: [Ex] }
struct Sp { let name: String; let color: String; let icon: String; let activity: Int; var restSeconds: Int? = nil; var autoAdvance: Bool? = nil; let ex: [Ex] }
let seeds: [Sp] = [
Sp(name: "Upper Body", color: "blue", icon: "figure.strengthtraining.traditional", activity: 0, ex: [
@@ -139,16 +141,16 @@ let seeds: [Sp] = [
Ex(name: "Glute Bridge", sets: 2, reps: 10, load: 0),
Ex(name: "Standing Calf Raise", sets: 2, reps: 12, load: 0),
]),
Sp(name: "Morning Wake-Up", color: "yellow", icon: "sun.max.fill", activity: 1, ex: [
Sp(name: "Morning Wake-Up", color: "yellow", icon: "sun.max.fill", activity: 1, restSeconds: 15, autoAdvance: true, ex: [
Ex(name: "March in Place", sets: 1, reps: 0, load: 2, dur: 45),
Ex(name: "Neck Rolls", sets: 1, reps: 5, load: 0),
Ex(name: "Arm Circles", sets: 1, reps: 10, load: 0),
Ex(name: "Torso Twist", sets: 1, reps: 10, load: 0),
Ex(name: "Standing Side Bend", sets: 1, reps: 8, load: 0),
Ex(name: "Hip Circles", sets: 1, reps: 8, load: 0),
Ex(name: "Leg Swings", sets: 1, reps: 10, load: 0),
Ex(name: "Standing Calf Raise", sets: 1, reps: 12, load: 0),
Ex(name: "Standing Forward Fold", sets: 1, reps: 0, load: 2, dur: 30),
Ex(name: "Neck Rolls", sets: 1, reps: 0, load: 2, dur: 45),
Ex(name: "Arm Circles", sets: 1, reps: 0, load: 2, dur: 45),
Ex(name: "Torso Twist", sets: 1, reps: 0, load: 2, dur: 45),
Ex(name: "Standing Side Bend", sets: 1, reps: 0, load: 2, dur: 45),
Ex(name: "Hip Circles", sets: 1, reps: 0, load: 2, dur: 45),
Ex(name: "Leg Swings", sets: 1, reps: 0, load: 2, dur: 45),
Ex(name: "Standing Calf Raise", sets: 1, reps: 0, load: 2, dur: 45),
Ex(name: "Standing Forward Fold", sets: 1, reps: 0, load: 2, dur: 45),
]),
Sp(name: "Morning Mobility", color: "pink", icon: "figure.yoga", activity: 1, ex: [
Ex(name: "Cat-Cow", sets: 1, reps: 10, load: 0),
@@ -201,7 +203,8 @@ for (order, sp) in seeds.enumerated() {
loadType: e.load, durationSeconds: e.dur,
machineSettings: e.machine ? [] : nil)
},
activityType: sp.activity)
activityType: sp.activity,
restSeconds: sp.restSeconds, autoAdvance: sp.autoAdvance)
let data = try encoder.encode(doc)
let file = outDir.appendingPathComponent("\(sp.name).split.json")
try data.write(to: file)