// // ExerciseProgressView.swift // Workouts Watch App // // Copyright 2025 Rouslan Zenetl. All Rights Reserved. // import SwiftUI import WatchKit struct ExerciseProgressView: View { @Environment(\.managedObjectContext) private var viewContext @Environment(\.dismiss) private var dismiss @ObservedObject var workoutLog: WorkoutLog @State private var currentPage: Int = 0 @State private var showingCancelConfirm = false private var totalSets: Int { max(1, Int(workoutLog.sets)) } private var totalPages: Int { // Set 1, Rest 1, Set 2, Rest 2, ..., Set N, Done // = N sets + (N-1) rests + 1 done = 2N totalSets * 2 } private var firstUnfinishedSetPage: Int { // currentStateIndex is the number of completed sets let completedSets = Int(workoutLog.currentStateIndex) if completedSets >= totalSets { // All done, go to done page return totalPages - 1 } // Each set is at page index * 2 (Set1=0, Set2=2, Set3=4...) return completedSets * 2 } var body: some View { TabView(selection: $currentPage) { ForEach(0.. some View { let lastPageIndex = totalPages - 1 if index == lastPageIndex { // Done page DonePageView { completeExercise() dismiss() } } else if index % 2 == 0 { // Set page (0, 2, 4, ...) let setNumber = (index / 2) + 1 SetPageView( setNumber: setNumber, totalSets: totalSets, reps: Int(workoutLog.reps), isTimeBased: workoutLog.loadTypeEnum == .duration, durationMinutes: workoutLog.durationMinutes, durationSeconds: workoutLog.durationSeconds ) } else { // Rest page (1, 3, 5, ...) let restNumber = (index / 2) + 1 RestPageView(restNumber: restNumber) } } private func updateProgress(for pageIndex: Int) { // Calculate which set we're on based on page index // Pages: Set1(0), Rest1(1), Set2(2), Rest2(3), Set3(4), Done(5) // After completing Set 1 and moving to Rest 1, progress should be 1 let setIndex = (pageIndex + 1) / 2 let clampedProgress = min(setIndex, totalSets) if clampedProgress != Int(workoutLog.currentStateIndex) { workoutLog.currentStateIndex = Int32(clampedProgress) if clampedProgress >= totalSets { workoutLog.status = .completed workoutLog.completed = true } else if clampedProgress > 0 { workoutLog.status = .inProgress workoutLog.completed = false } updateWorkoutStatus() try? viewContext.save() // Sync to iOS WatchConnectivityManager.shared.syncToiOS() } } private func completeExercise() { workoutLog.currentStateIndex = Int32(totalSets) workoutLog.status = .completed workoutLog.completed = true updateWorkoutStatus() try? viewContext.save() // Sync to iOS WatchConnectivityManager.shared.syncToiOS() } private func updateWorkoutStatus() { guard let workout = workoutLog.workout else { return } let logs = workout.logsArray let allCompleted = logs.allSatisfy { $0.status == .completed } let anyInProgress = logs.contains { $0.status == .inProgress } let allNotStarted = logs.allSatisfy { $0.status == .notStarted } if allCompleted { workout.status = .completed workout.end = Date() } else if anyInProgress || !allNotStarted { workout.status = .inProgress } else { workout.status = .notStarted } } } // MARK: - Set Page View struct SetPageView: View { let setNumber: Int let totalSets: Int let reps: Int let isTimeBased: Bool let durationMinutes: Int let durationSeconds: Int var body: some View { VStack(spacing: 8) { Text("Set \(setNumber) of \(totalSets)") .font(.headline) .foregroundColor(.secondary) Text("\(setNumber)") .font(.system(size: 72, weight: .bold, design: .rounded)) .foregroundColor(.green) if isTimeBased { Text(formattedDuration) .font(.title3) .foregroundColor(.secondary) } else { Text("\(reps) reps") .font(.title3) .foregroundColor(.secondary) } } .frame(maxWidth: .infinity, maxHeight: .infinity) .onAppear { WKInterfaceDevice.current().play(.start) } } private var formattedDuration: String { if durationMinutes > 0 && durationSeconds > 0 { return "\(durationMinutes)m \(durationSeconds)s" } else if durationMinutes > 0 { return "\(durationMinutes) min" } else { return "\(durationSeconds) sec" } } } // MARK: - Rest Page View struct RestPageView: View { let restNumber: Int @State private var elapsedSeconds: Int = 0 @State private var timer: Timer? var body: some View { VStack(spacing: 8) { Text("Rest") .font(.headline) .foregroundColor(.secondary) Text(formattedTime) .font(.system(size: 56, weight: .bold, design: .monospaced)) .foregroundColor(.orange) Text("Swipe to continue") .font(.caption2) .foregroundColor(.secondary) } .frame(maxWidth: .infinity, maxHeight: .infinity) .onAppear { startTimer() WKInterfaceDevice.current().play(.start) } .onDisappear { stopTimer() } } private var formattedTime: String { let minutes = elapsedSeconds / 60 let seconds = elapsedSeconds % 60 return String(format: "%d:%02d", minutes, seconds) } private func startTimer() { elapsedSeconds = 0 timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in elapsedSeconds += 1 checkHapticPing() } } private func stopTimer() { timer?.invalidate() timer = nil } private func checkHapticPing() { // Haptic ping every 10 seconds with pattern: // 10s: single, 20s: double, 30s: triple, 40s: single, 50s: double, etc. guard elapsedSeconds > 0 && elapsedSeconds % 10 == 0 else { return } let cyclePosition = (elapsedSeconds / 10) % 3 let pingCount: Int switch cyclePosition { case 1: pingCount = 1 // 10s, 40s, 70s... case 2: pingCount = 2 // 20s, 50s, 80s... case 0: pingCount = 3 // 30s, 60s, 90s... default: pingCount = 1 } playHapticPings(count: pingCount) } private func playHapticPings(count: Int) { for i in 0.. Void var body: some View { VStack(spacing: 16) { Image(systemName: "checkmark.circle.fill") .font(.system(size: 60)) .foregroundColor(.green) Text("Done!") .font(.title2) .fontWeight(.bold) Text("Tap to finish") .font(.caption) .foregroundColor(.secondary) } .frame(maxWidth: .infinity, maxHeight: .infinity) .contentShape(Rectangle()) .onTapGesture { WKInterfaceDevice.current().play(.success) onDone() } .onAppear { WKInterfaceDevice.current().play(.success) } } }