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:
@@ -71,6 +71,19 @@ struct ExerciseDocument: Codable, Sendable, Equatable, Identifiable {
|
||||
/// empty → a machine exercise with nothing recorded yet. This optionality
|
||||
/// doubles as the machine-based flag — there is no separate Bool.
|
||||
var machineSettings: [MachineSetting]? = nil
|
||||
/// The library exercise `name` was customized from (e.g. "Treadmill" renamed to
|
||||
/// "Brisk Walk 10 min" for one segment of an interval routine); nil → `name` IS
|
||||
/// the library name. Optional and deliberately NOT schema-bumped, same rationale
|
||||
/// as `RoutineDocument.activityType`: an older app dropping it on rewrite only
|
||||
/// strands the figure/info link for the renamed exercise, which is preferable to
|
||||
/// quarantining the user's whole routine.
|
||||
var libraryName: String? = nil
|
||||
}
|
||||
|
||||
extension ExerciseDocument {
|
||||
/// The name to resolve figure/info/cues against — `name` itself unless it was
|
||||
/// customized, in which case the library exercise it derives from.
|
||||
var libraryExerciseName: String { libraryName ?? name }
|
||||
}
|
||||
|
||||
/// A single free-form comfort setting for a machine-based exercise. `value` is a
|
||||
@@ -251,9 +264,18 @@ struct WorkoutLogDocument: Codable, Sendable, Equatable, Identifiable {
|
||||
/// on two devices merges by the newer `updatedAt`). Nothing writes it yet;
|
||||
/// absent in older files.
|
||||
var updatedAt: Date? = nil
|
||||
/// Plan-time snapshot of the exercise's `libraryName` (like sets/reps/weight).
|
||||
/// Optional and deliberately NOT schema-bumped, same rationale as
|
||||
/// `RoutineDocument.activityType`: an older app dropping it on rewrite only
|
||||
/// strands the figure/info link for a renamed exercise, which is preferable to
|
||||
/// quarantining the whole workout.
|
||||
var libraryName: String? = nil
|
||||
}
|
||||
|
||||
extension WorkoutLogDocument {
|
||||
/// The name to resolve figure/info/cues against — see `ExerciseDocument.libraryExerciseName`.
|
||||
var libraryExerciseName: String { libraryName ?? exerciseName }
|
||||
|
||||
/// Build a fresh plan-time log from a routine's exercise. Snapshots the plan
|
||||
/// fields (sets/reps/weight/duration) *and* the machine comfort settings, so a
|
||||
/// running workout carries the exercise's settings as its own editable copy —
|
||||
@@ -274,7 +296,8 @@ extension WorkoutLogDocument {
|
||||
status: WorkoutStatus.notStarted.rawValue,
|
||||
notes: nil,
|
||||
date: date,
|
||||
machineSettings: exercise.machineSettings
|
||||
machineSettings: exercise.machineSettings,
|
||||
libraryName: exercise.libraryName
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -17,7 +17,7 @@ extension ExerciseDocument {
|
||||
init(from e: Exercise) {
|
||||
self.init(id: e.id, name: e.name, order: e.order, sets: e.sets, reps: e.reps,
|
||||
weight: e.weight, loadType: e.loadType, durationSeconds: e.durationTotalSeconds,
|
||||
machineSettings: e.machineSettings)
|
||||
machineSettings: e.machineSettings, libraryName: e.libraryName)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,8 @@ extension WorkoutLogDocument {
|
||||
status: log.statusRaw, notes: log.notes, date: log.date,
|
||||
machineSettings: log.machineSettings,
|
||||
startedAt: log.startedAt, completedAt: log.completedAt,
|
||||
setEntries: log.setEntries, updatedAt: log.logUpdatedAt)
|
||||
setEntries: log.setEntries, updatedAt: log.logUpdatedAt,
|
||||
libraryName: log.libraryName)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,7 +165,7 @@ enum CacheMapper {
|
||||
private static func makeExercise(_ d: ExerciseDocument) -> Exercise {
|
||||
Exercise(id: d.id, name: d.name, order: d.order, sets: d.sets, reps: d.reps, weight: d.weight,
|
||||
loadType: d.loadType, durationTotalSeconds: d.durationSeconds,
|
||||
machineSettings: d.machineSettings)
|
||||
machineSettings: d.machineSettings, libraryName: d.libraryName)
|
||||
}
|
||||
|
||||
private static func apply(_ d: ExerciseDocument, to e: Exercise) {
|
||||
@@ -176,6 +177,7 @@ enum CacheMapper {
|
||||
e.loadType = d.loadType
|
||||
e.durationTotalSeconds = d.durationSeconds
|
||||
e.machineSettings = d.machineSettings
|
||||
e.libraryName = d.libraryName
|
||||
}
|
||||
|
||||
// MARK: Workout
|
||||
@@ -228,7 +230,7 @@ enum CacheMapper {
|
||||
currentStateIndex: d.currentStateIndex, statusRaw: d.status,
|
||||
notes: d.notes, date: d.date, machineSettings: d.machineSettings,
|
||||
startedAt: d.startedAt, completedAt: d.completedAt,
|
||||
setEntries: d.setEntries, logUpdatedAt: d.updatedAt)
|
||||
setEntries: d.setEntries, logUpdatedAt: d.updatedAt, libraryName: d.libraryName)
|
||||
}
|
||||
|
||||
private static func apply(_ d: WorkoutLogDocument, to l: WorkoutLog) {
|
||||
@@ -248,6 +250,7 @@ enum CacheMapper {
|
||||
l.completedAt = d.completedAt
|
||||
l.setEntries = d.setEntries
|
||||
l.logUpdatedAt = d.updatedAt
|
||||
l.libraryName = d.libraryName
|
||||
}
|
||||
|
||||
// MARK: Schedule
|
||||
|
||||
Reference in New Issue
Block a user