// // ExerciseListView.swift // Workouts // // Created by rzen on 7/18/25 at 8:38 AM. // // Copyright 2025 Rouslan Zenetl. All Rights Reserved. // import IndieSync import SwiftUI import SwiftData struct ExerciseListView: View { @Environment(SyncEngine.self) private var sync @Environment(\.dismiss) private var dismiss // Resolve the routine by id, not a captured entity: editing a seed's exercise from // here clones the seed (new identity, old entity deleted), which would dangle a // stored `Routine`. `currentRoutineID` follows that swap. @State private var routineID: String @Query private var routines: [Routine] @State private var showingAddSheet: Bool = false @State private var itemToEdit: Exercise? = nil @State private var itemToDelete: Exercise? = nil @AppStorage("weightUnit") private var weightUnit: WeightUnit = .lb init(routine: Routine) { _routineID = State(initialValue: routine.id) } private var routine: Routine? { let id = sync.currentRoutineID(for: routineID) return routines.first { $0.id == id } } var body: some View { Group { if let routine { content(for: routine) } else { ContentUnavailableView("Routine Unavailable", systemImage: "dumbbell") .task { dismiss() } } } } @ViewBuilder private func content(for routine: Routine) -> some View { Form { let sortedExercises = routine.exercisesArray if !sortedExercises.isEmpty { ForEach(sortedExercises) { item in ListItem( title: item.name, subtitle: "\(item.sets) × \(item.reps) × \(weightUnit.format(item.weight))" ) .swipeActions { Button { itemToDelete = item } label: { Label("Delete", systemImage: "trash") } .tint(.red) Button { itemToEdit = item } label: { Label("Edit", systemImage: "pencil") } .tint(.indigo) } } .onMove(perform: moveExercises) Button { showingAddSheet = true } label: { ListItem(title: "Add Exercise") } } else { Text("No exercises added yet.") Button(action: { showingAddSheet.toggle() }) { ListItem(title: "Add Exercise") } } } .navigationTitle(routine.name) .sheet(isPresented: $showingAddSheet) { ExercisePickerView(onExerciseSelected: { exerciseNames in addExercises(names: exerciseNames) }, allowMultiSelect: true) } .sheet(item: $itemToEdit) { item in ExerciseAddEditView(exercise: item, routine: routine) } .confirmationDialog( "Delete Exercise?", isPresented: Binding( get: { itemToDelete != nil }, set: { if !$0 { itemToDelete = nil } } ), titleVisibility: .visible, presenting: itemToDelete ) { item in Button("Delete", role: .destructive) { deleteExercise(item) itemToDelete = nil } Button("Cancel", role: .cancel) { itemToDelete = nil } } message: { item in Text("Remove \"\(item.name)\" from this routine?") } } // MARK: - Helpers private func moveExercises(from source: IndexSet, to destination: Int) { guard let routine else { return } var exercises = routine.exercisesArray exercises.move(fromOffsets: source, toOffset: destination) var doc = RoutineDocument(from: routine) doc.exercises = exercises.enumerated().map { i, ex in var ed = ExerciseDocument(from: ex) ed.order = i return ed } doc.updatedAt = Date() Task { await sync.save(routine: doc) } } private func addExercises(names: [String]) { guard let routine else { return } var doc = RoutineDocument(from: routine) let existingNames = Set(doc.exercises.map { $0.name }) let base = doc.exercises.count let newDocs = names .filter { !existingNames.contains($0) } .enumerated() .map { i, exName -> ExerciseDocument in // Seed from the library's authored `**Defaults:**` when available, // falling back to a plain 3×10 weighted guess; weight always starts // at 0 (there's no prior lift to seed it from). let defaults = ExerciseInfoLibrary.info(for: exName)?.defaults return ExerciseDocument( id: ULID.make(), name: exName, order: base + i, sets: defaults?.sets ?? 3, reps: defaults?.reps ?? 10, weight: 0, loadType: (defaults?.loadType ?? .weight).rawValue, durationSeconds: defaults?.durationSeconds ?? 0 ) } doc.exercises.append(contentsOf: newDocs) doc.updatedAt = Date() Task { await sync.save(routine: doc) } } private func deleteExercise(_ exercise: Exercise) { guard let routine else { return } var doc = RoutineDocument(from: routine) doc.exercises.removeAll { $0.id == exercise.id } for i in doc.exercises.indices { doc.exercises[i].order = i } doc.updatedAt = Date() Task { await sync.save(routine: doc) } } }