Surface routine usage, starter-copy notices, and a grouped exercise picker

Routine detail gains a read-time Usage section (last trained, completed
workout count, linked schedules) resolved through the clone redirect, and
both it and the edit sheet now explain that editing a starter saves your
own copy. The add-exercise picker adopts the same curated category
sections and name/category/muscle search as the exercise library.

Claude-Session: https://claude.ai/code/session_01H8VxUX4ckjU3vRF5M4L5FV
This commit is contained in:
2026-07-15 11:14:24 -04:00
parent b6046bd0a9
commit 52f58cd79c
3 changed files with 116 additions and 34 deletions
@@ -12,6 +12,7 @@ import SwiftUI
struct ExercisePickerView: View {
@Environment(\.dismiss) private var dismiss
@State private var selectedExercises: Set<String> = []
@State private var searchText = ""
var onExerciseSelected: ([String]) -> Void
var allowMultiSelect: Bool = false
@@ -28,47 +29,34 @@ struct ExercisePickerView: View {
self.inRoutineCounts = inRoutineCounts
}
/// The same curated, searchable grouping `ExerciseLibraryView` uses, so picking an
/// exercise reads the same way as browsing the library.
private var sections: [ExerciseCatalogSection] {
ExerciseCatalog.sections(matching: searchText)
}
var body: some View {
NavigationStack {
List(ExerciseMotionLibrary.exerciseNames, id: \.self) { exerciseName in
if allowMultiSelect {
Button(action: {
if selectedExercises.contains(exerciseName) {
selectedExercises.remove(exerciseName)
} else {
selectedExercises.insert(exerciseName)
}
}) {
HStack {
Text(exerciseName)
Spacer()
if let count = inRoutineCounts[exerciseName], count > 0 {
Text("×\(count)")
.foregroundStyle(.secondary)
}
if selectedExercises.contains(exerciseName) {
Image(systemName: "checkmark")
.foregroundColor(.accentColor)
}
}
}
Group {
if sections.isEmpty {
ContentUnavailableView.search(text: searchText)
} else {
Button(action: {
onExerciseSelected([exerciseName])
dismiss()
}) {
HStack {
Text(exerciseName)
Spacer()
if let count = inRoutineCounts[exerciseName], count > 0 {
Text("×\(count)")
.foregroundStyle(.secondary)
List {
ForEach(sections, id: \.title) { section in
Section(section.title) {
ForEach(section.names, id: \.self) { exerciseName in
row(for: exerciseName)
}
}
}
}
}
}
.navigationTitle("Exercise Library")
.searchable(
text: $searchText,
placement: .navigationBarDrawer(displayMode: .always),
prompt: "Name, category, or muscle")
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") {
@@ -89,4 +77,44 @@ struct ExercisePickerView: View {
}
}
}
@ViewBuilder
private func row(for exerciseName: String) -> some View {
if allowMultiSelect {
Button(action: {
if selectedExercises.contains(exerciseName) {
selectedExercises.remove(exerciseName)
} else {
selectedExercises.insert(exerciseName)
}
}) {
HStack {
Text(exerciseName)
Spacer()
if let count = inRoutineCounts[exerciseName], count > 0 {
Text("×\(count)")
.foregroundStyle(.secondary)
}
if selectedExercises.contains(exerciseName) {
Image(systemName: "checkmark")
.foregroundColor(.accentColor)
}
}
}
} else {
Button(action: {
onExerciseSelected([exerciseName])
dismiss()
}) {
HStack {
Text(exerciseName)
Spacer()
if let count = inRoutineCounts[exerciseName], count > 0 {
Text("×\(count)")
.foregroundStyle(.secondary)
}
}
}
}
}
}