// // RoutineAddEditView.swift // Workouts // // Created by rzen on 7/18/25 at 9:42 AM. // // Copyright 2025 Rouslan Zenetl. All Rights Reserved. // import IndieSync import SwiftUI import SwiftData struct RoutineAddEditView: View { @Environment(SyncEngine.self) private var sync @Environment(\.modelContext) private var modelContext @Environment(\.dismiss) private var dismiss // Resolve the routine by id, not a captured entity: a clone-on-edit swaps a seed's // identity mid-screen (the seed entity is deleted and a clone inserted), which // would dangle a stored `Routine`. `currentRoutineID` follows that swap. `routineID` is // nil in create mode. @State private var routineID: String? @Query private var routines: [Routine] var onDelete: (() -> Void)? @State private var name: String = "" @State private var color: String = "indigo" @State private var systemImage: String = "dumbbell.fill" @State private var activityType: WorkoutActivityType = .traditionalStrength @State private var autoAdvance: Bool = false @State private var restOverrideEnabled: Bool = false @State private var restSecondsValue: Int = 45 @State private var targetHREnabled: Bool = false @State private var targetHRValue: Int = 135 @State private var showingIconPicker: Bool = false @State private var showingDeleteConfirmation: Bool = false var isEditing: Bool { routineID != nil } init(routine: Routine?, onDelete: (() -> Void)? = nil) { _routineID = State(initialValue: routine?.id) self.onDelete = onDelete if let routine = routine { _name = State(initialValue: routine.name) _color = State(initialValue: routine.color) _systemImage = State(initialValue: routine.systemImage) _activityType = State(initialValue: routine.activityTypeEnum) _autoAdvance = State(initialValue: routine.autoAdvance ?? false) _restOverrideEnabled = State(initialValue: routine.restSeconds != nil) _restSecondsValue = State(initialValue: routine.restSeconds ?? 45) _targetHREnabled = State(initialValue: routine.targetHeartRate != nil) _targetHRValue = State(initialValue: routine.targetHeartRate ?? 135) } } private var routine: Routine? { guard let routineID else { return nil } let id = sync.currentRoutineID(for: routineID) return routines.first { $0.id == id } } var body: some View { NavigationStack { if isEditing && routine == nil { // The id we held no longer maps to a live routine (deleted on another // device, or a transient mid-clone frame). Show nothing and leave. ContentUnavailableView("Routine Unavailable", systemImage: "dumbbell") .task { dismiss() } } else { form(for: routine) } } } @ViewBuilder private func form(for routine: Routine?) -> some View { Form { Section { TextField("Name", text: $name) .bold() } header: { Text("Name") } footer: { if let routineID, SeedLibrary.isSeed(id: routineID) { Text("Saving changes to a starter routine creates your own copy; the starter stays available in the gallery.") } } Section(header: Text("Appearance")) { Picker("Color", selection: $color) { ForEach(availableColors, id: \.self) { colorName in HStack { Circle() .fill(Color.color(from: colorName)) .frame(width: 20, height: 20) Text(colorName.capitalized) } .tag(colorName) } } Button { showingIconPicker = true } label: { HStack { Text("Icon") .foregroundColor(.primary) Spacer() Image(systemName: systemImage) .font(.title2) .foregroundColor(.accentColor) } } } Section { Picker("Activity Type", selection: $activityType) { ForEach(WorkoutActivityType.allCases, id: \.self) { type in Label(type.displayName, systemImage: type.systemImage).tag(type) } } } header: { Text("Activity Type") } footer: { Text("Determines how this workout is categorized in Apple Health and how it credits your Activity rings.") } Section { Toggle("Auto-Advance Exercises", isOn: $autoAdvance) Toggle("Custom Rest Time", isOn: $restOverrideEnabled) if restOverrideEnabled { SecondsStepperRow(title: "Rest Time", range: 10...180, seconds: $restSecondsValue) } } header: { Text("Rest & Pacing") } footer: { Text("Auto-Advance flows from one exercise to the next with a rest between, so you can run the whole routine hands-free. Custom Rest Time sets this routine's rest — used between sets, and between exercises when auto-advancing; otherwise the Settings default applies.") } // Only endurance types get a target — a strength set's HR swings by design. if activityType.supportsHeartRateTarget { Section { Toggle("Target Heart Rate", isOn: $targetHREnabled) if targetHREnabled { Stepper(value: $targetHRValue, in: 80...200, step: 5) { HStack { Text("Target") Spacer() Text("\(targetHRValue) bpm").foregroundColor(.secondary) } } } } header: { Text("Heart Rate") } footer: { Text("During a run, the exercise screen shows your live heart rate from the watch with a cue to ease off or push harder — useful for holding a steady effort, like dialing in a treadmill incline.") } } if routine != nil { Section { Button("Delete Routine", role: .destructive) { showingDeleteConfirmation = true } } } } .navigationTitle(isEditing ? "Edit Routine" : "New Routine") .toolbar { ToolbarItem(placement: .navigationBarLeading) { Button("Cancel") { dismiss() } } ToolbarItem(placement: .navigationBarTrailing) { Button("Save") { save() dismiss() } .disabled(name.isEmpty) } } .sheet(isPresented: $showingIconPicker) { SFSymbolPicker(selection: $systemImage) } .confirmationDialog( "Delete This Routine?", isPresented: $showingDeleteConfirmation, titleVisibility: .visible ) { Button("Delete", role: .destructive) { if let routine = routine { Task { await sync.delete(routine: routine) } dismiss() onDelete?() } } } message: { Text("This will permanently delete the routine and all its exercises.") } } private func save() { if isEditing { // Update existing routine. If the id no longer resolves (deleted remotely // mid-edit), there's nothing to save. guard let routine = routine else { return } var doc = RoutineDocument(from: routine) doc.name = name doc.color = color doc.systemImage = systemImage doc.activityType = activityType.rawValue doc.restSeconds = restOverrideEnabled ? restSecondsValue : nil doc.autoAdvance = autoAdvance ? true : nil // Cleared when the type no longer supports a target (the section hides). doc.targetHeartRate = activityType.supportsHeartRateTarget && targetHREnabled ? targetHRValue : nil doc.updatedAt = Date() Task { await sync.save(routine: doc) } } else { // Create new routine let existing = (try? modelContext.fetch(FetchDescriptor())) ?? [] let doc = RoutineDocument( schemaVersion: RoutineDocument.currentSchemaVersion, id: ULID.make(), name: name, color: color, systemImage: systemImage, order: existing.count, createdAt: Date(), updatedAt: Date(), exercises: [], activityType: activityType.rawValue, restSeconds: restOverrideEnabled ? restSecondsValue : nil, autoAdvance: autoAdvance ? true : nil, targetHeartRate: activityType.supportsHeartRateTarget && targetHREnabled ? targetHRValue : nil ) Task { await sync.save(routine: doc) } } } }