// // ExerciseView.swift // Workouts // // Created by rzen on 7/18/25 at 5:44 PM. // // Copyright 2025 Rouslan Zenetl. All Rights Reserved. // import SwiftUI import SwiftData import Charts struct ExerciseView: View { @Environment(AppServices.self) private var services let workout: Workout let logID: String /// Working copy of the parent workout. Driving the UI from local state (not the /// cache entity) lets `seedDoc` show a just-added log before the file→cache /// round-trip catches up. @State private var doc: WorkoutDocument @State private var showingPlanEdit = false @State private var showingNotesEdit = false /// `seedDoc` lets the caller hand over an in-memory document (e.g. the parent's /// working copy right after adding an exercise) so the screen doesn't wait on /// the file→cache round-trip to find the just-created log. init(workout: Workout, logID: String, seedDoc: WorkoutDocument? = nil) { self.workout = workout self.logID = logID _doc = State(initialValue: seedDoc ?? WorkoutDocument(from: workout)) } /// The log being edited within the working doc. private var log: WorkoutLogDocument? { doc.logs.first { $0.id == logID } } var body: some View { Group { if let log { content(for: log) } else { // The just-added log hasn't reached the cache yet; refresh shortly. ProgressView() } } .navigationTitle(log?.exerciseName ?? "") .sheet(isPresented: $showingPlanEdit) { PlanEditView(workout: workout, logID: logID) } .sheet(isPresented: $showingNotesEdit) { NotesEditView(workout: workout, logID: logID) } .onAppear { refreshDocIfNeeded() // Take over this run: the watch parks and locks it while we're editing here. services.watchBridge.setEditingWorkout(workout.id) } .onDisappear { services.watchBridge.setEditingWorkout(nil) } // Reflect external changes (e.g. a set completed on the watch) live. Each edit // rewrites the whole workout file, so the cache always holds the latest — pulling // it in can't revert a newer local tap. .onChange(of: workout.updatedAt) { _, _ in absorbExternalUpdate() } } /// Pull an externally-changed workout into the working copy so the plan and notes /// update without leaving and re-entering the screen. private func absorbExternalUpdate() { doc = WorkoutDocument(from: workout) } @ViewBuilder private func content(for log: WorkoutLogDocument) -> some View { Form { // MARK: - Plan Section (Read-only with Edit button) Section { PlanTilesView(log: log) } header: { HStack { Text("Plan") Spacer() Button("Edit") { showingPlanEdit = true } .font(.subheadline) .textCase(.none) } } // MARK: - Notes Section (Read-only with Edit button) Section { if let notes = log.notes, !notes.isEmpty { Text(notes) .foregroundColor(.primary) } else { Text("No notes") .foregroundColor(.secondary) .italic() } } header: { HStack { Text("Notes") Spacer() Button("Edit") { showingNotesEdit = true } .font(.subheadline) .textCase(.none) } } // MARK: - Progress Tracking Chart Section(header: Text("Progress Tracking")) { WeightProgressionChartView(exerciseName: log.exerciseName) } } // Pull plan/notes edits made in the sheets back into the live doc. .onChange(of: showingPlanEdit) { _, presenting in if !presenting { refreshDocFromCache() } } .onChange(of: showingNotesEdit) { _, presenting in if !presenting { refreshDocFromCache() } } } /// If the requested log isn't in the working doc yet (just-added race), pull a /// fresh copy from the cache entity once it catches up. private func refreshDocIfNeeded() { guard log == nil else { return } refreshDocFromCache() } /// Re-read the workout from the cache to absorb edits made by child sheets /// (plan/notes). private func refreshDocFromCache() { doc = WorkoutDocument(from: workout) } } // MARK: - Plan Tiles View struct PlanTilesView: View { let log: WorkoutLogDocument @AppStorage("weightUnit") private var weightUnit: WeightUnit = .lb var body: some View { if LoadType(rawValue: log.loadType) == .duration { // Duration layout: Sets | Duration HStack(spacing: 0) { PlanTile(label: "Sets", value: "\(log.sets)") PlanTile(label: "Duration", value: formattedDuration) } } else { // Weight layout: Sets | Reps | Weight HStack(spacing: 0) { PlanTile(label: "Sets", value: "\(log.sets)") PlanTile(label: "Reps", value: "\(log.reps)") PlanTile(label: "Weight", value: weightUnit.format(log.weight)) } } } private var formattedDuration: String { let mins = log.durationSeconds / 60 let secs = log.durationSeconds % 60 if mins > 0 && secs > 0 { return "\(mins)m \(secs)s" } else if mins > 0 { return "\(mins) min" } else if secs > 0 { return "\(secs) sec" } else { return "0 sec" } } } struct PlanTile: View { let label: String let value: String var body: some View { VStack(spacing: 4) { Text(label) .font(.caption) .foregroundColor(.secondary) Text(value) .font(.title2) .fontWeight(.semibold) .foregroundColor(.primary) } .frame(maxWidth: .infinity) .padding(.vertical, 12) .background(Color(.systemGray6)) .cornerRadius(8) } }