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
121 lines
4.3 KiB
Swift
121 lines
4.3 KiB
Swift
//
|
||
// 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)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|