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:
@@ -22,8 +22,9 @@ import UIKit
|
||||
/// reaches a **Finish** page offering **One More** (append a bonus set) and **Done**
|
||||
/// (which also auto-fires after a configurable countdown, completing the exercise).
|
||||
///
|
||||
/// The paged flow occupies the **top half** of the screen; the bottom half is reserved
|
||||
/// blank for a later iteration. A row of phase dots tracks progress: purple for work,
|
||||
/// The paged flow occupies the **top half** of the screen; the bottom half shows the
|
||||
/// animated form-guide figure when the exercise has a bundled motion
|
||||
/// (`ExerciseFigureSlot`). A row of phase dots tracks progress: purple for work,
|
||||
/// teal for rest, with the current phase drawn as a wider dash.
|
||||
struct ExerciseProgressView: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@@ -85,6 +86,11 @@ struct ExerciseProgressView: View {
|
||||
/// the 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
|
||||
|
||||
init(doc: Binding<WorkoutDocument>, logID: String, onChange: @escaping () -> Void, onLive: @escaping (LiveProgress) -> Void = { _ in }, onLiveEnded: @escaping () -> Void = {}, incomingFrame: LiveProgress? = nil) {
|
||||
self._doc = doc
|
||||
self.logID = logID
|
||||
@@ -102,6 +108,7 @@ struct ExerciseProgressView: View {
|
||||
// the TabView's snap-to-0 on first layout.
|
||||
let notStarted = (log?.status ?? WorkoutStatus.notStarted.rawValue) == WorkoutStatus.notStarted.rawValue
|
||||
_startsResumed = State(initialValue: !notStarted)
|
||||
_startsCompleted = State(initialValue: log?.status == WorkoutStatus.completed.rawValue)
|
||||
|
||||
let base = 1
|
||||
// Resume on the first unfinished set's work page (clamped to the last set).
|
||||
@@ -181,6 +188,16 @@ struct ExerciseProgressView: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if startsCompleted {
|
||||
CompletedPhaseView()
|
||||
.navigationTitle(log?.exerciseName ?? "")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
} else {
|
||||
flowBody
|
||||
}
|
||||
}
|
||||
|
||||
private var flowBody: some View {
|
||||
VStack(spacing: 0) {
|
||||
// Paged flow — top half.
|
||||
TabView(selection: $currentPage) {
|
||||
@@ -198,9 +215,9 @@ struct ExerciseProgressView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// Reserved space for a later iteration (set log, history, chart, …).
|
||||
Color.clear
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
// Bottom half: the looping form-guide figure when a bundled motion
|
||||
// matches this exercise; empty space otherwise.
|
||||
ExerciseFigureSlot(exerciseName: log?.exerciseName ?? "")
|
||||
}
|
||||
.navigationTitle(log?.exerciseName ?? "")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
@@ -677,6 +694,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: 14) {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.font(.system(size: 96))
|
||||
.foregroundStyle(.green)
|
||||
Text("Completed")
|
||||
.font(.system(.title, design: .rounded, weight: .heavy))
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Ready Phase
|
||||
|
||||
private struct ReadyPhaseView: View {
|
||||
|
||||
@@ -387,7 +387,7 @@ struct SplitExercisePickerSheet: View {
|
||||
VStack(alignment: .leading) {
|
||||
Text(exercise.name)
|
||||
.foregroundColor(.primary)
|
||||
Text("\(exercise.sets) × \(exercise.reps) × \(weightUnit.format(exercise.weight))")
|
||||
Text(exercise.planSummary(weightUnit: weightUnit))
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user