Let routines rename exercises and repeat them for interval segments
An exercise's name is now a per-routine display name: a new optional libraryName on ExerciseDocument (snapshotted onto WorkoutLogDocument at plan time) keeps the link to the bundled library exercise, and every figure/info/cue lookup resolves libraryName ?? name. Deliberately not schema-bumped — an older app dropping the key only strands the figure link, same rationale as activityType. Cache schema bumped to 9 for the new columns. The picker no longer filters out exercises already in the routine (an "×N" badge marks them instead), exercise rows gain a leading Duplicate swipe that clones an entry in place, and the edit sheet gets a Name field with the library exercise shown read-only above it. Claude-Session: https://claude.ai/code/session_01H8VxUX4ckjU3vRF5M4L5FV
This commit is contained in:
@@ -220,12 +220,17 @@ struct ExerciseProgressView: View {
|
||||
}?.id
|
||||
}
|
||||
|
||||
/// The next not-yet-resolved log after this one, in plan order — the flow hand-off
|
||||
/// target's full document, so both its display and library names are reachable.
|
||||
/// Nil when this is the last unfinished exercise.
|
||||
private var nextLog: WorkoutLogDocument? {
|
||||
guard let id = nextUnfinishedLogID else { return nil }
|
||||
return doc.logs.first { $0.id == id }
|
||||
}
|
||||
|
||||
/// Name of the next not-yet-resolved exercise (the flow hand-off target), or nil when
|
||||
/// this is the last one. Drives the between-exercise rest's "Coming up …" preview.
|
||||
private var nextExerciseName: String? {
|
||||
guard let id = nextUnfinishedLogID else { return nil }
|
||||
return doc.logs.first { $0.id == id }?.exerciseName
|
||||
}
|
||||
private var nextExerciseName: String? { nextLog?.exerciseName }
|
||||
|
||||
/// The between-exercise rest — flow mode's terminal page when another exercise follows.
|
||||
/// This is the only rest whose "next" is a *different* exercise, so it's the one that
|
||||
@@ -235,12 +240,22 @@ struct ExerciseProgressView: View {
|
||||
}
|
||||
|
||||
/// Which exercise the bottom-half figure should show: the *next* one while resting
|
||||
/// between exercises (so you see what's coming up), otherwise this exercise.
|
||||
/// between exercises (so you see what's coming up), otherwise this exercise. This
|
||||
/// is the display name (the headline); `figureLibraryExerciseName` is its figure/
|
||||
/// info counterpart.
|
||||
private var figureExerciseName: String {
|
||||
if isOnBetweenExerciseRest, let next = nextExerciseName { return next }
|
||||
return log?.exerciseName ?? ""
|
||||
}
|
||||
|
||||
/// The library name to resolve the bottom-half figure against — the same
|
||||
/// this-exercise-or-next-during-rest choice as `figureExerciseName`, but the
|
||||
/// library name the animation/info actually key on.
|
||||
private var figureLibraryExerciseName: String {
|
||||
if isOnBetweenExerciseRest, let next = nextLog { return next.libraryExerciseName }
|
||||
return log?.libraryExerciseName ?? ""
|
||||
}
|
||||
|
||||
/// Offset of the work/rest cycle: `1` when a Ready page leads, else `0`.
|
||||
private var base: Int { showsReady ? 1 : 0 }
|
||||
|
||||
@@ -333,21 +348,22 @@ struct ExerciseProgressView: View {
|
||||
}
|
||||
|
||||
/// The figure half of the split, with the big-type exercise headline above the
|
||||
/// figure in landscape.
|
||||
/// figure in landscape. `display` names the headline; `figure` is the library
|
||||
/// name the animated rig is keyed on — they diverge once an exercise is renamed.
|
||||
@ViewBuilder
|
||||
private func figureHalf(_ name: String) -> some View {
|
||||
private func figureHalf(display: String, figure: String) -> some View {
|
||||
if isLandscape {
|
||||
VStack(spacing: 0) {
|
||||
Text(name)
|
||||
Text(display)
|
||||
.font(.system(size: landscapeNameFontSize, weight: .bold, design: .rounded))
|
||||
.lineLimit(1)
|
||||
.minimumScaleFactor(0.5)
|
||||
.padding(.horizontal)
|
||||
.padding(.top, 8)
|
||||
ExerciseFigureSlot(exerciseName: name)
|
||||
ExerciseFigureSlot(exerciseName: figure)
|
||||
}
|
||||
} else {
|
||||
ExerciseFigureSlot(exerciseName: name)
|
||||
ExerciseFigureSlot(exerciseName: figure)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -358,14 +374,14 @@ struct ExerciseProgressView: View {
|
||||
// the paged flow would be, so the form-guide figure stays on screen.
|
||||
splitLayout {
|
||||
CompletedPhaseView(log: log)
|
||||
figureHalf(log?.exerciseName ?? "")
|
||||
figureHalf(display: log?.exerciseName ?? "", figure: log?.libraryExerciseName ?? "")
|
||||
}
|
||||
.navigationTitle(navBarTitle)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
} else if startsSkipped {
|
||||
splitLayout {
|
||||
SkippedPhaseView()
|
||||
figureHalf(log?.exerciseName ?? "")
|
||||
figureHalf(display: log?.exerciseName ?? "", figure: log?.libraryExerciseName ?? "")
|
||||
}
|
||||
.navigationTitle(navBarTitle)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
@@ -403,7 +419,7 @@ struct ExerciseProgressView: View {
|
||||
// resting between exercises (a live preview of what's coming up), otherwise this
|
||||
// one. The name swap re-loads the figure and carries through the hand-off, so it
|
||||
// stays put as the next exercise takes over.
|
||||
figureHalf(figureExerciseName)
|
||||
figureHalf(display: figureExerciseName, figure: figureLibraryExerciseName)
|
||||
}
|
||||
.navigationTitle(navBarTitle)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
@@ -839,7 +855,7 @@ struct ExerciseProgressView: View {
|
||||
private func announceCue() {
|
||||
guard speakExerciseCues, spokenCueStyle.readsInstructions,
|
||||
let announcer = speechAnnouncer, let log else { return }
|
||||
guard let info = ExerciseInfoLibrary.info(for: log.exerciseName) else { return }
|
||||
guard let info = ExerciseInfoLibrary.info(for: log.libraryExerciseName) else { return }
|
||||
announcer.speak(info.spokenScript(name: log.exerciseName, brief: true))
|
||||
}
|
||||
|
||||
|
||||
@@ -119,7 +119,7 @@ struct ExerciseView: View {
|
||||
// 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 {
|
||||
|| ExerciseInfoLibrary.info(for: log.libraryExerciseName)?.isMachineBased == true {
|
||||
let settings = log.machineSettings ?? []
|
||||
Section {
|
||||
if settings.isEmpty {
|
||||
|
||||
@@ -365,7 +365,7 @@ struct WorkoutLogListView: View {
|
||||
id: ULID.make(), name: name, order: doc.logs.count,
|
||||
sets: recent.sets, reps: recent.reps, weight: recent.weight,
|
||||
loadType: recent.loadType, durationSeconds: recent.durationTotalSeconds,
|
||||
machineSettings: recent.machineSettings
|
||||
machineSettings: recent.machineSettings, libraryName: recent.libraryName
|
||||
)
|
||||
}
|
||||
let info = ExerciseInfoLibrary.info(for: name)
|
||||
|
||||
Reference in New Issue
Block a user