Add the exercise reference library, animated exercise figures, and exercise categories

Exercise Library/ holds per-exercise reference docs (setup, cues,
mistakes, progressions) with SVG visuals and a Python-rendered motion
pipeline; Workouts/ExerciseFigure renders the bundled *.motion.json
rigs as animated stick figures on the exercise screen. Exercises gain
a warm-up/main-circuit category, timed exercises display hold time via
planSummary, and a completed exercise reopens to a check screen instead
of its timers.
This commit is contained in:
2026-07-06 01:15:52 -04:00
parent ddd5631ef2
commit 7274f155e9
79 changed files with 2521 additions and 11 deletions
@@ -79,6 +79,11 @@ struct ExerciseProgressView: View {
/// transient TabView snap-to-0, so it isn't reset on open.
@State private var startsResumed: Bool
/// True when the exercise was already completed when this screen opened it shows a
/// static Completed page instead of dropping back into the timer flow. Fixed at init
/// so completing the exercise from inside the flow doesn't swap the page mid-dismiss.
@State private var startsCompleted: Bool
/// Forces the starting page (used only by the DEBUG screenshot host). When set it
/// also suppresses the Ready page so the index is a plain work/rest cycle offset.
private let debugInitialPage: Int?
@@ -102,6 +107,7 @@ struct ExerciseProgressView: View {
// set and re-asserts that page past the TabView's snap-to-0 on first layout.
let ready = debugInitialPage == nil
_startsResumed = State(initialValue: ready && !notStarted)
_startsCompleted = State(initialValue: ready && log?.status == WorkoutStatus.completed.rawValue)
let base = ready ? 1 : 0
// Resume on the first unfinished set's work page (clamped to the last set).
@@ -182,6 +188,14 @@ struct ExerciseProgressView: View {
}
var body: some View {
if startsCompleted {
CompletedPhaseView()
} else {
flowBody
}
}
private var flowBody: some View {
TabView(selection: $currentPage) {
ForEach(0..<totalPages, id: \.self) { index in
page(for: index)
@@ -727,6 +741,22 @@ private extension View {
}
}
// MARK: - Completed Phase
/// Shown instead of the run flow when the exercise was already completed on open.
private struct CompletedPhaseView: View {
var body: some View {
VStack(spacing: 8) {
Image(systemName: "checkmark.circle.fill")
.font(.system(size: 56))
.foregroundStyle(.green)
Text("Completed")
.font(.system(.title3, design: .rounded, weight: .heavy))
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
// MARK: - Ready Phase
private struct ReadyPhaseView: View {