import SwiftUI /// The bundled exercise-library browser: a Mac-native two-pane layout over the same /// `ExerciseCatalog` sections and search the iOS Library tab uses /// (`Workouts/Views/Exercises/ExerciseLibraryView.swift`), but browse-only — no /// machine-settings editor, no weight-progression chart, no workout history. The left /// pane is a searchable, section-grouped list with a single selection; the right pane /// shows the selected exercise's animated figure and reference info, the same /// components/entry points iOS uses (`ExerciseFigureSlot`, `ExerciseInfoContent`). struct MacExercisesBrowserView: View { @State private var searchText = "" @State private var selection: String? private var sections: [ExerciseCatalogSection] { ExerciseCatalog.sections(matching: searchText) } var body: some View { HSplitView { list .frame(minWidth: 220, idealWidth: 260, maxWidth: 340) detail .frame(minWidth: 420, maxWidth: .infinity, maxHeight: .infinity) } .navigationTitle("Exercises") .searchable(text: $searchText, prompt: "Name, category, or muscle") } // MARK: - List @ViewBuilder private var list: some View { if sections.isEmpty { ContentUnavailableView.search(text: searchText) } else { List(selection: $selection) { ForEach(sections, id: \.title) { section in Section(section.title) { ForEach(section.names, id: \.self) { name in Text(name) .tag(name) } } } } } } // MARK: - Detail @ViewBuilder private var detail: some View { if let selection { MacExerciseDetailView(exerciseName: selection) // Reseed the detail's own state when the selection changes. .id(selection) } else { ContentUnavailableView( "Select an Exercise", systemImage: "figure.strengthtraining.traditional", description: Text("Choose an exercise from the list to see how it's performed.") ) } } } /// One library exercise, browse-only: the animated form-guide figure (when a bundled /// motion rig matches the name) over the authored reference copy (when a bundled /// `info.md` matches). Either — or both — can be absent for a given exercise, so each /// half degrades gracefully instead of leaving blank space, mirroring how the iOS /// detail screen simply omits what it doesn't have (`ExerciseLibraryDetailView`). private struct MacExerciseDetailView: View { let exerciseName: String /// Cheap presence check — a name-set lookup against the bundle enumeration /// `ExerciseMotionLibrary.exerciseNames` already did once, rather than /// `resources(for:)`'s two bundle reads + two JSON decodes just to answer a Bool. private var hasFigure: Bool { ExerciseMotionLibrary.exerciseNames.contains(exerciseName) } var body: some View { let info = ExerciseInfoLibrary.info(for: exerciseName) let hasFigure = hasFigure ScrollView { VStack(alignment: .leading, spacing: 20) { if hasFigure || info == nil { figureArea(hasFigure: hasFigure) } if let info { ExerciseInfoContent(info: info) } else if hasFigure { Text("No reference guide written for this exercise yet.") .font(.callout) .foregroundStyle(.secondary) } } .padding(24) .frame(maxWidth: .infinity, alignment: .leading) } .navigationTitle(exerciseName) } /// The figure, sized generously for the Mac detail pane, or — when neither a /// motion rig nor reference info exists for this name — a single combined empty /// state rather than two stacked placeholders. @ViewBuilder private func figureArea(hasFigure: Bool) -> some View { if hasFigure { ExerciseFigureSlot(exerciseName: exerciseName) .frame(maxWidth: .infinity) .frame(height: 260) } else { ContentUnavailableView( exerciseName, systemImage: "figure.strengthtraining.traditional", description: Text("No animated guide or reference info is available for this exercise yet.") ) .frame(maxWidth: .infinity) } } }