Add machine comfort settings and tighten the document schemas
Machine-based exercises now carry ordered name/value comfort settings (seat height, back-rest position, ...) on both ExerciseDocument and, as a plan-time snapshot, WorkoutLogDocument — the optional array doubles as the machine flag. Editable from the exercise editor's new Machine section and from the workout row's settings sheet, whose mid-workout edits write back to the originating split (workout saved first, so seed clone-on-edit repointing can't clobber the log edit). Schema tightening rides the same rev: splits bump to v2 (weight-reminder fields and the unused exercise category removed), workouts to v3 (the derived `completed` flag removed; status is the single source). Starter seeds regenerated at v2 with unchanged ULIDs; SwiftData cache schema bumped to rebuild. SCHEMA.md documents the shapes. Claude-Session: https://claude.ai/code/session_01LEoff8bXGBS83tK1c55Mf7
This commit is contained in:
@@ -63,51 +63,46 @@ struct SplitDetailView: View {
|
||||
.font(.caption)
|
||||
}
|
||||
|
||||
// One section per category (warm-up first). A split with no warm-ups keeps
|
||||
// the single plain "Exercises" section it always had.
|
||||
// Headerless — what the exercise list is needs no label; the section
|
||||
// itself keeps the visual separation.
|
||||
if split.exercisesArray.isEmpty {
|
||||
Section(header: Text("Exercises")) {
|
||||
Section {
|
||||
Text("No exercises added yet.")
|
||||
Button(action: { showingExerciseAddSheet.toggle() }) {
|
||||
ListItem(title: "Add Exercise")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let grouped = groupedExercises(for: split)
|
||||
ForEach(grouped, id: \.category) { group in
|
||||
Section(header: Text(grouped.count == 1 ? "Exercises" : group.category.displayName)) {
|
||||
ForEach(group.exercises) { item in
|
||||
ListItem(
|
||||
title: item.name,
|
||||
subtitle: item.planSummary(weightUnit: weightUnit)
|
||||
)
|
||||
.swipeActions {
|
||||
Button {
|
||||
itemToDelete = item
|
||||
} label: {
|
||||
Label("Delete", systemImage: "trash")
|
||||
}
|
||||
.tint(.red)
|
||||
Button {
|
||||
itemToEdit = item
|
||||
} label: {
|
||||
Label("Edit", systemImage: "pencil")
|
||||
}
|
||||
.tint(.indigo)
|
||||
}
|
||||
}
|
||||
.onMove { source, destination in
|
||||
moveExercises(in: group.category, from: source, to: destination)
|
||||
}
|
||||
|
||||
if group.category == grouped.last?.category {
|
||||
Section {
|
||||
ForEach(split.exercisesArray) { item in
|
||||
ListItem(
|
||||
title: item.name,
|
||||
subtitle: item.planSummary(weightUnit: weightUnit)
|
||||
)
|
||||
.swipeActions {
|
||||
Button {
|
||||
showingExerciseAddSheet = true
|
||||
itemToDelete = item
|
||||
} label: {
|
||||
ListItem(systemName: "plus.circle", title: "Add Exercise")
|
||||
Label("Delete", systemImage: "trash")
|
||||
}
|
||||
.tint(.red)
|
||||
Button {
|
||||
itemToEdit = item
|
||||
} label: {
|
||||
Label("Edit", systemImage: "pencil")
|
||||
}
|
||||
.tint(.indigo)
|
||||
}
|
||||
}
|
||||
.onMove { source, destination in
|
||||
moveExercises(from: source, to: destination)
|
||||
}
|
||||
|
||||
Button {
|
||||
showingExerciseAddSheet = true
|
||||
} label: {
|
||||
ListItem(systemName: "plus.circle", title: "Add Exercise")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -155,26 +150,14 @@ struct SplitDetailView: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// Exercises bucketed by category in display order, dropping empty buckets.
|
||||
private func groupedExercises(for split: Split) -> [(category: ExerciseCategory, exercises: [Exercise])] {
|
||||
let all = split.exercisesArray
|
||||
return ExerciseCategory.displayOrder.compactMap { category in
|
||||
let members = all.filter { $0.categoryEnum == category }
|
||||
return members.isEmpty ? nil : (category, members)
|
||||
}
|
||||
}
|
||||
|
||||
/// Reorder within one category's section, then renumber globally with the
|
||||
/// sections' display order (warm-ups first) as the canonical file order. Resolves
|
||||
/// the current split at call time so it follows a clone-on-edit.
|
||||
private func moveExercises(in category: ExerciseCategory, from source: IndexSet, to destination: Int) {
|
||||
/// Reorder and renumber. Resolves the current split at call time so it
|
||||
/// follows a clone-on-edit.
|
||||
private func moveExercises(from source: IndexSet, to destination: Int) {
|
||||
guard let split else { return }
|
||||
var groups = groupedExercises(for: split)
|
||||
guard let gi = groups.firstIndex(where: { $0.category == category }) else { return }
|
||||
groups[gi].exercises.move(fromOffsets: source, toOffset: destination)
|
||||
var ordered = split.exercisesArray
|
||||
ordered.move(fromOffsets: source, toOffset: destination)
|
||||
|
||||
var doc = SplitDocument(from: split)
|
||||
let ordered = groups.flatMap(\.exercises)
|
||||
doc.exercises = ordered.enumerated().map { i, ex in
|
||||
var ed = ExerciseDocument(from: ex)
|
||||
ed.order = i
|
||||
@@ -197,7 +180,7 @@ struct SplitDetailView: View {
|
||||
id: ULID.make(), name: exName, order: base + i,
|
||||
sets: 3, reps: 10, weight: 40,
|
||||
loadType: LoadType.weight.rawValue,
|
||||
durationSeconds: 0, weightLastUpdated: nil, weightReminderWeeks: 2
|
||||
durationSeconds: 0
|
||||
)
|
||||
}
|
||||
doc.exercises.append(contentsOf: newDocs)
|
||||
|
||||
Reference in New Issue
Block a user