// // RoutinePickerSheet.swift // Workouts // // Copyright 2026 Rouslan Zenetl. All Rights Reserved. // import IndieSync import SwiftUI import SwiftData // MARK: - Routine Picker Sheet struct RoutinePickerSheet: View { @Environment(SyncEngine.self) private var sync @Environment(AppServices.self) private var services @Environment(\.dismiss) private var dismiss /// Called with the new workout's id right after it's saved, so the presenting /// screen can navigate into it once the cache catches up. var onStarted: (String) -> Void = { _ in } @Query(sort: [SortDescriptor(\Routine.order), SortDescriptor(\Routine.name)]) private var routines: [Routine] @Query(sort: \Workout.start, order: .reverse) private var workouts: [Workout] /// Set when the user picks a routine while other workouts are still going — drives the /// "end the current one(s) or run in parallel?" prompt. @State private var routineAwaitingConfirmation: Routine? private var activeWorkouts: [Workout] { workouts.filter { $0.status == .inProgress || $0.status == .notStarted } } /// The routines most recently trained, newest first, each with the start date /// of its latest workout — derived from the workout log (`workouts` is /// already sorted by start descending). Each workout's `routineID` follows the /// seed clone-on-edit redirect so a workout started from a since-forked seed /// still credits the live clone. Deduped, capped, and limited to routines that /// still exist. private var recentRoutines: [(routine: Routine, lastStart: Date)] { var seen = Set() var recents: [(routine: Routine, lastStart: Date)] = [] for workout in workouts { guard let routineID = workout.routineID else { continue } let liveID = sync.currentRoutineID(for: routineID) guard !seen.contains(liveID) else { continue } seen.insert(liveID) if let routine = routines.first(where: { $0.id == liveID }) { recents.append((routine, workout.start)) if recents.count == 3 { break } } } return recents } /// One selectable routine row, shared by the Recent and All Routines sections. /// Recent rows pass `lastTrained` to show how long ago the routine was run. private func routineRow(_ routine: Routine, lastTrained: Date? = nil) -> some View { Button { confirmAndStart(with: routine) } label: { HStack { Image(systemName: routine.systemImage) .foregroundColor(Color.color(from: routine.color)) .frame(width: 28) VStack(alignment: .leading, spacing: 2) { Text(routine.name) if let lastTrained { Text(lastTrained.daysAgoLabel()) .font(.caption) .foregroundColor(.secondary) } } Spacer() Text("\(routine.exercisesArray.count) exercises") .font(.caption) .foregroundColor(.secondary) } } } var body: some View { NavigationStack { List { if !recentRoutines.isEmpty { Section("Recent") { ForEach(recentRoutines, id: \.routine.id) { recent in routineRow(recent.routine, lastTrained: recent.lastStart) } } } Section(recentRoutines.isEmpty ? "" : "All Routines") { ForEach(routines) { routine in routineRow(routine) } } } .navigationTitle("Select a Routine") .toolbar { ToolbarItem(placement: .navigationBarLeading) { Button("Cancel") { dismiss() } } } .confirmationDialog( activePromptTitle, isPresented: Binding( get: { routineAwaitingConfirmation != nil }, set: { if !$0 { routineAwaitingConfirmation = nil } } ), titleVisibility: .visible, presenting: routineAwaitingConfirmation ) { routine in Button("End Current & Start New") { endActiveThenStart(with: routine) } Button("Start in Parallel") { start(with: routine) } Button("Cancel", role: .cancel) { routineAwaitingConfirmation = nil } } message: { _ in Text(activePromptMessage) } } } private var activePromptTitle: String { activeWorkouts.count == 1 ? "Workout in Progress" : "\(activeWorkouts.count) Workouts in Progress" } private var activePromptMessage: String { let n = activeWorkouts.count let those = n == 1 ? "it" : "them" return "You already have \(n == 1 ? "a workout" : "\(n) workouts") going. End \(those) first, or run this one alongside." } /// Prompt before starting if other workouts are still going; otherwise start straight away. private func confirmAndStart(with routine: Routine) { if activeWorkouts.isEmpty { start(with: routine) } else { routineAwaitingConfirmation = routine } } /// End every in-flight workout (keeping its progress), then start the picked routine. private func endActiveThenStart(with routine: Routine) { // A pristine draft (no exercise ever started) has nothing worth keeping — // "ending" it would mint a junk all-skipped completed workout, so it's // discarded instead, same rule as backing out of one. let toDiscard = activeWorkouts.filter(\.isPristineDraft) let toEnd = activeWorkouts.filter { !$0.isPristineDraft }.map { WorkoutDocument(from: $0) } routineAwaitingConfirmation = nil Task { for workout in toDiscard { await sync.delete(workout: workout) } for var doc in toEnd { doc.endKeepingProgress() await sync.save(workout: doc) } } start(with: routine) } private func start(with routine: Routine) { // Hand the id back after the save so the presenter can poll the cache and // navigate into the run — mirroring the exercise-list start path. Task { let id = await WorkoutStarter.start(routine: routine, sync: sync) onStarted(id) } dismiss() } }