// // ExerciseAddEditView.swift // Workouts // // Created by rzen on 7/15/25 at 7:12 AM. // // Copyright 2025 Rouslan Zenetl. All Rights Reserved. // import SwiftUI import SwiftData struct ExerciseAddEditView: View { @Environment(SyncEngine.self) private var sync @Environment(\.dismiss) private var dismiss @State private var showingExercisePicker = false // The exercise entity provides initial values (read-only). let exercise: Exercise // The parent routine is needed to rebuild and save the RoutineDocument. let routine: Routine // Local editable state @State private var exerciseName: String // The library exercise this row derives from — nil only when `exerciseName` // doesn't (yet) match any bundled exercise. Set alongside `exerciseName` when // the picker is used; carried through unchanged when the user just retypes the // name in the TextField, so a rename keeps its figure/info link. @State private var libraryName: String? @State private var loadType: LoadType @State private var minutes: Int @State private var seconds: Int @State private var weightTens: Int @State private var weightOnes: Int @State private var reps: Int @State private var sets: Int // Machine comfort settings. `machineEnabled` mirrors the optionality of the // stored `machineSettings` — ON persists a (possibly empty) array, OFF persists // nil. `machineSettings` holds the ordered rows while the toggle is on. @State private var machineEnabled: Bool @State private var machineSettings: [MachineSetting] @AppStorage("weightUnit") private var weightUnit: WeightUnit = .lb init(exercise: Exercise, routine: Routine) { self.exercise = exercise self.routine = routine // The tens/ones pickers step whole values; a fractional stored weight // shows (and saves back) its whole part until UX #3's UI lands. let w = Int(exercise.weight) _exerciseName = State(initialValue: exercise.name) _libraryName = State(initialValue: exercise.libraryName ?? (ExerciseMotionLibrary.exerciseNames.contains(exercise.name) ? exercise.name : nil)) _loadType = State(initialValue: exercise.loadTypeEnum) _minutes = State(initialValue: exercise.durationMinutes) _seconds = State(initialValue: exercise.durationSeconds) _weightTens = State(initialValue: (w / 10) * 10) _weightOnes = State(initialValue: w % 10) _reps = State(initialValue: exercise.reps) _sets = State(initialValue: exercise.sets) _machineEnabled = State(initialValue: exercise.machineSettings != nil) _machineSettings = State(initialValue: exercise.machineSettings ?? []) } var body: some View { NavigationStack { Form { Section( header: Text("Exercise"), footer: Text("You can rename this exercise for this routine — for example \"Warmup Walk\" for Treadmill. The animated figure and exercise guide still come from the original exercise.") ) { if exerciseName.isEmpty { Button(action: { showingExercisePicker = true }) { HStack { Text("Select Exercise") Spacer() Image(systemName: "chevron.right") .foregroundColor(.gray) } } } else { if let libraryName { ListItem(title: libraryName) } TextField("Name", text: $exerciseName) } } Section(header: Text("Sets/Reps")) { HStack(alignment: .bottom) { VStack(alignment: .center) { Text("Sets") Picker("", selection: $sets) { ForEach(0..<20) { s in Text("\(s)").tag(s) } } .frame(height: 100) .pickerStyle(.wheel) } VStack(alignment: .center) { Text("Reps") Picker("", selection: $reps) { ForEach(0..<100) { r in Text("\(r)").tag(r) } } .frame(height: 100) .pickerStyle(.wheel) } } } Section( header: Text("Load Type"), footer: Text("For bodyweight exercises choose None. For resistance or weight training select Weight. For exercises that are time oriented (like plank or meditation) select Time.") ) { Picker("", selection: $loadType) { ForEach(LoadType.allCases, id: \.self) { load in Text(load.name) .tag(load) } } .pickerStyle(.segmented) } if loadType == .weight { Section(header: Text("Weight")) { HStack { Picker("", selection: $weightTens) { ForEach(0..<100) { lbs in Text("\(lbs * 10)").tag(lbs * 10) } } .frame(height: 100) .pickerStyle(.wheel) Picker("", selection: $weightOnes) { ForEach(0..<10) { lbs in Text("\(lbs)").tag(lbs) } } .frame(height: 100) .pickerStyle(.wheel) Text(weightUnit.abbreviation) } } } if loadType == .duration { Section(header: Text("Duration")) { HStack { Picker("Minutes", selection: $minutes) { ForEach(0..<60) { minute in Text("\(minute) min").tag(minute) } } .pickerStyle(.wheel) Picker("Seconds", selection: $seconds) { ForEach(0..<60) { second in Text("\(second) sec").tag(second) } } .pickerStyle(.wheel) } } } Section( header: Text("Machine"), footer: Text("Turn on for machine-based exercises to record comfort settings (seat height, back-rest incline, pin position…). During a workout you can adjust these and update this default.") ) { Toggle("Machine-based", isOn: $machineEnabled.animation()) if machineEnabled { MachineSettingsEditor(settings: $machineSettings) } } } .sheet(isPresented: $showingExercisePicker) { ExercisePickerView { exerciseNames in let picked = exerciseNames.first ?? "Unnamed" exerciseName = picked libraryName = picked } } .navigationTitle(exerciseName.isEmpty ? "New Exercise" : exerciseName) .toolbar { ToolbarItem(placement: .navigationBarLeading) { Button("Cancel") { dismiss() } } ToolbarItem(placement: .navigationBarTrailing) { Button("Save") { saveExercise() dismiss() } } } } } private func saveExercise() { let newWeight = weightTens + weightOnes let durationSecs = minutes * 60 + seconds // An emptied name falls back to the library exercise (or, failing that, the // prior name) rather than saving a blank display name. let trimmedName = exerciseName.trimmingCharacters(in: .whitespacesAndNewlines) let finalName = trimmedName.isEmpty ? (libraryName ?? exercise.name) : trimmedName var doc = RoutineDocument(from: routine) if let idx = doc.exercises.firstIndex(where: { $0.id == exercise.id }) { doc.exercises[idx].name = finalName // nil when the name matches the library exercise (unrenamed) — keeps an // unrenamed exercise byte-identical on disk. doc.exercises[idx].libraryName = (libraryName == finalName) ? nil : libraryName doc.exercises[idx].sets = sets doc.exercises[idx].reps = reps doc.exercises[idx].weight = Double(newWeight) doc.exercises[idx].loadType = loadType.rawValue doc.exercises[idx].durationSeconds = durationSecs // ON → a (possibly empty) array; OFF → nil, discarding any prior settings. doc.exercises[idx].machineSettings = machineEnabled ? machineSettings : nil } doc.updatedAt = Date() Task { await sync.save(routine: doc) } } }