Files
workouts/Workouts/Views/Exercises/ExercisePickerView.swift
T
rzen 52f58cd79c 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
2026-07-15 11:14:24 -04:00

121 lines
4.3 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// ExercisePickerView.swift
// Workouts
//
// Created by rzen on 7/13/25 at 7:17 PM.
//
// Copyright 2025 Rouslan Zenetl. All Rights Reserved.
//
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
/// How many times each exercise already appears in the routine being edited —
/// shown as a "×N" badge so picking it again (for interval-style segments like
/// a treadmill's warmup/brisk/cooldown) doesn't read as a mistake. Keyed by
/// library name. Empty for callers with no notion of "already in this routine".
var inRoutineCounts: [String: Int] = [:]
init(onExerciseSelected: @escaping ([String]) -> Void, allowMultiSelect: Bool = false,
inRoutineCounts: [String: Int] = [:]) {
self.onExerciseSelected = onExerciseSelected
self.allowMultiSelect = allowMultiSelect
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 {
Group {
if sections.isEmpty {
ContentUnavailableView.search(text: searchText)
} else {
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") {
dismiss()
}
}
if allowMultiSelect {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Select") {
if !selectedExercises.isEmpty {
onExerciseSelected(Array(selectedExercises))
dismiss()
}
}
.disabled(selectedExercises.isEmpty)
}
}
}
}
}
@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)
}
}
}
}
}
}