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:
2026-07-06 16:29:44 -04:00
parent fce8fa4c17
commit 2c1e4759ae
33 changed files with 1132 additions and 586 deletions
+51 -13
View File
@@ -10,12 +10,19 @@
import SwiftUI
import SwiftData
/// The Splits library screen, pushed from Settings Library: a two-column grid of
/// split tiles. Tapping a tile opens `SplitDetailView`; long-pressing offers Delete
/// (the sole split-delete affordance); the toolbar's + adds a new split. Starter
/// splits are seeded automatically on the true first run (`SyncEngine.autoSeedIfEmpty`),
/// so an empty library just invites creating one.
struct SplitListView: View {
@Environment(SyncEngine.self) private var sync
@Environment(\.modelContext) private var modelContext
@Query(sort: \Split.order) private var splits: [Split]
@State private var showingAddSheet = false
@State private var splitToDelete: Split?
var body: some View {
ScrollView {
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 16) {
@@ -25,6 +32,13 @@ struct SplitListView: View {
} label: {
SplitItem(split: split)
}
.contextMenu {
Button(role: .destructive) {
splitToDelete = split
} label: {
Label("Delete", systemImage: "trash")
}
}
}
}
.padding()
@@ -32,20 +46,44 @@ struct SplitListView: View {
.overlay {
if splits.isEmpty {
ContentUnavailableView(
label: {
Label("No Splits Yet", systemImage: "dumbbell.fill")
},
description: {
Text("Create a split to organize your workout routine.")
},
actions: {
Button("Add Starter Splits") {
Task { await SplitSeeder.seedDefaults(into: modelContext, using: sync) }
}
.buttonStyle(.borderedProminent)
}
"No Splits Yet",
systemImage: "dumbbell.fill",
description: Text("Create a split to organize your workout routine.")
)
}
}
.navigationTitle("Splits")
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button {
showingAddSheet = true
} label: {
Image(systemName: "plus")
}
.accessibilityLabel("Add Split")
}
}
.sheet(isPresented: $showingAddSheet) {
SplitAddEditView(split: nil)
}
.confirmationDialog(
"Delete Split?",
isPresented: Binding(
get: { splitToDelete != nil },
set: { if !$0 { splitToDelete = nil } }
),
titleVisibility: .visible,
presenting: splitToDelete
) { split in
Button("Delete", role: .destructive) {
Task { await sync.delete(split: split) }
splitToDelete = nil
}
Button("Cancel", role: .cancel) {
splitToDelete = nil
}
} message: { split in
Text("This will permanently delete \"\(split.name)\" and all its exercises. Past workouts are kept.")
}
}
}