Surface machine settings in the exercise library and refine machine rigs

Library detail screens now lead with the user's recorded machine settings
(per-split when they disagree, empty-state card for machine-based entries)
and append the weight progression chart. Starter seeds mark machine
exercises with an empty machineSettings list so the settings UI lights up
before first use. The figure rig gains a frontal body profile for face-on
machines, props that can ride mid joints (knees/elbows), and an
alternating four-frame Bird Dog loop.

Claude-Session: https://claude.ai/code/session_01LEoff8bXGBS83tK1c55Mf7
This commit is contained in:
2026-07-06 18:35:15 -04:00
parent ff1ffbb1f6
commit 669ecf1259
76 changed files with 733 additions and 268 deletions
+74 -3
View File
@@ -12,6 +12,7 @@ import SwiftData
import Charts
struct ExerciseView: View {
@Environment(SyncEngine.self) private var sync
@Environment(AppServices.self) private var services
let workout: Workout
@@ -23,6 +24,7 @@ struct ExerciseView: View {
@State private var doc: WorkoutDocument
@State private var showingPlanEdit = false
@State private var showingNotesEdit = false
@State private var showingMachineSettings = false
/// `seedDoc` lets the caller hand over an in-memory document (e.g. the parent's
/// working copy right after adding an exercise) so the screen doesn't wait on
@@ -41,6 +43,9 @@ struct ExerciseView: View {
var body: some View {
Group {
if let log {
// Deliberately NOT the half-and-half figure layout: this screen is
// dense with the plan, notes, settings, and the progression chart
// the form guide lives in the run flow and the exercise library.
content(for: log)
} else {
// The just-added log hasn't reached the cache yet; refresh shortly.
@@ -54,6 +59,16 @@ struct ExerciseView: View {
.sheet(isPresented: $showingNotesEdit) {
NotesEditView(workout: workout, logID: logID)
}
.sheet(isPresented: $showingMachineSettings) {
if let log {
MachineSettingsSheet(
exerciseName: log.exerciseName,
initialSettings: log.machineSettings ?? []
) { updated in
applyMachineSettings(updated)
}
}
}
.onAppear {
refreshDocIfNeeded()
// Take over this run: the watch parks and locks it while we're editing here.
@@ -94,6 +109,42 @@ struct ExerciseView: View {
}
}
// MARK: - Machine Settings shown when the log carries settings, or for
// any exercise the authored library identifies as machine-based, so the
// section is discoverable before settings are first recorded.
if log.machineSettings != nil
|| ExerciseInfoLibrary.info(for: log.exerciseName)?.isMachineBased == true {
let settings = log.machineSettings ?? []
Section {
if settings.isEmpty {
Text("No settings recorded")
.foregroundColor(.secondary)
.italic()
} else {
ForEach(Array(settings.enumerated()), id: \.offset) { _, setting in
HStack(alignment: .firstTextBaseline) {
Text(setting.name)
.foregroundStyle(.secondary)
Spacer()
Text(setting.value)
.fontWeight(.semibold)
.monospacedDigit()
}
}
}
} header: {
HStack {
Text("Machine Settings")
Spacer()
Button("Edit") {
showingMachineSettings = true
}
.font(.subheadline)
.textCase(.none)
}
}
}
// MARK: - Notes Section (Read-only with Edit button)
Section {
if let notes = log.notes, !notes.isEmpty {
@@ -116,9 +167,12 @@ struct ExerciseView: View {
}
}
// MARK: - Progress Tracking Chart
Section(header: Text("Progress Tracking")) {
WeightProgressionChartView(exerciseName: log.exerciseName)
// MARK: - Progress Tracking Chart (weighted exercises only a weight
// chart is meaningless for bodyweight/timed logs)
if LoadType(rawValue: log.loadType) == .weight {
Section(header: Text("Progress Tracking")) {
WeightProgressionChartView(exerciseName: log.exerciseName)
}
}
}
// Pull plan/notes edits made in the sheets back into the live doc.
@@ -130,6 +184,23 @@ struct ExerciseView: View {
}
}
/// Apply edited machine settings: update the log's plan-time snapshot (persisted
/// with the workout), then write the same settings back to the originating
/// split's exercise as the durable default same sequencing as the workout
/// list's machine sheet.
private func applyMachineSettings(_ settings: [MachineSetting]) {
guard let i = doc.logs.firstIndex(where: { $0.id == logID }) else { return }
doc.logs[i].machineSettings = settings
doc.updatedAt = Date()
let snapshot = doc
let exerciseName = doc.logs[i].exerciseName
let splitID = doc.splitID
Task {
await sync.save(workout: snapshot)
await sync.writeBackMachineSettings(settings, exerciseName: exerciseName, splitID: splitID)
}
}
/// If the requested log isn't in the working doc yet (just-added race), pull a
/// fresh copy from the cache entity once it catches up.
private func refreshDocIfNeeded() {