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:
2026-07-12 18:38:14 -04:00
parent d04526826c
commit df5c77eee1
15 changed files with 203 additions and 38 deletions
+18 -2
View File
@@ -80,11 +80,15 @@ final class Exercise {
// `Workout` stores `hrZoneSeconds` as a plain `[Double]?`. `nil` not a
// machine exercise; empty a machine exercise with nothing recorded yet.
var machineSettings: [MachineSetting]?
// The library exercise `name` was customized from; nil `name` IS the
// library name. Mirrors `ExerciseDocument.libraryName`.
var libraryName: String?
var routine: Routine?
init(id: String, name: String, order: Int, sets: Int, reps: Int, weight: Double,
loadType: Int, durationTotalSeconds: Int, machineSettings: [MachineSetting]? = nil) {
loadType: Int, durationTotalSeconds: Int, machineSettings: [MachineSetting]? = nil,
libraryName: String? = nil) {
self.id = id
self.name = name
self.order = order
@@ -94,6 +98,7 @@ final class Exercise {
self.loadType = loadType
self.durationTotalSeconds = durationTotalSeconds
self.machineSettings = machineSettings
self.libraryName = libraryName
}
var loadTypeEnum: LoadType {
@@ -101,6 +106,9 @@ final class Exercise {
set { loadType = newValue.rawValue }
}
/// The name to resolve figure/info/cues against see `ExerciseDocument.libraryExerciseName`.
var libraryExerciseName: String { libraryName ?? name }
/// One-line plan summary for list rows duration exercises show their hold time
/// ("3 × 45 sec"), unloaded ones drop the weight ("3 × 12 reps"), weighted ones
/// keep the full "3 × 10 × 40 lb".
@@ -270,6 +278,9 @@ final class WorkoutLog {
// Mirrors the document's per-log `updatedAt` (named to dodge a future
// SwiftData column collision). Reserved for H1's per-log merge; unused.
var logUpdatedAt: Date?
// Plan-time snapshot of the exercise's `libraryName`. Mirrors
// `WorkoutLogDocument.libraryName`.
var libraryName: String?
var workout: Workout?
@@ -278,7 +289,8 @@ final class WorkoutLog {
statusRaw: String, notes: String?, date: Date,
machineSettings: [MachineSetting]? = nil,
startedAt: Date? = nil, completedAt: Date? = nil,
setEntries: [SetEntry]? = nil, logUpdatedAt: Date? = nil) {
setEntries: [SetEntry]? = nil, logUpdatedAt: Date? = nil,
libraryName: String? = nil) {
self.id = id
self.exerciseName = exerciseName
self.order = order
@@ -296,6 +308,7 @@ final class WorkoutLog {
self.completedAt = completedAt
self.setEntries = setEntries
self.logUpdatedAt = logUpdatedAt
self.libraryName = libraryName
}
var status: WorkoutStatus {
@@ -308,6 +321,9 @@ final class WorkoutLog {
set { loadType = newValue.rawValue }
}
/// The name to resolve figure/info/cues against see `ExerciseDocument.libraryExerciseName`.
var libraryExerciseName: String { libraryName ?? exerciseName }
var durationMinutes: Int {
get { durationTotalSeconds / 60 }
set { durationTotalSeconds = newValue * 60 + durationSeconds }