import SwiftUI import WatchKit /// Séance guidée au poignet : un exercice à la fois, décompte, passage /// automatique. Pendant une routine au sol, regarder sa montre est bien plus /// pratique que d'aller chercher son téléphone — c'est tout l'intérêt d'avoir /// la version watchOS plutôt que le seul lecteur web. /// /// Chaque exercice écoulé est coché via `RoutineStore.toggle`, donc renvoyé à /// l'iPhone puis au serveur : à la fin, le bloc est déjà à jour partout. struct RoutineGuidedView: View { let block: RoutineBlock @StateObject private var store = RoutineStore.shared @Environment(\.dismiss) private var dismiss @State private var index = 0 @State private var remaining = 0.0 @State private var paused = false @State private var finished = false // 0,2 s : assez fluide pour l'anneau, assez lent pour ne pas vider la // batterie de la montre sur une séance de 14 minutes. private let tick = Timer.publish(every: 0.2, on: .main, in: .common).autoconnect() private var current: RoutineExercise? { index < block.items.count ? block.items[index] : nil } var body: some View { Group { if finished || current == nil { completion } else { running } } .navigationTitle(finished ? "Terminé" : "\(index + 1)/\(block.items.count)") .onAppear { startCurrent() } .onReceive(tick) { _ in step() } } // MARK: - Écrans private var running: some View { VStack(spacing: 6) { Text(current?.name ?? "") .font(.headline) .multilineTextAlignment(.center) .minimumScaleFactor(0.7) .lineLimit(3) if let dose = current?.dose, !dose.isEmpty { Text(dose) .font(.caption2) .foregroundStyle(.cyan) } ZStack { Circle() .stroke(Color.gray.opacity(0.25), lineWidth: 6) Circle() .trim(from: 0, to: progress) .stroke(Color.cyan, style: StrokeStyle(lineWidth: 6, lineCap: .round)) .rotationEffect(.degrees(-90)) Text(timeLabel) .font(.system(size: 30, weight: .semibold, design: .rounded)) .monospacedDigit() } .frame(height: 92) .padding(.vertical, 2) HStack(spacing: 10) { Button { skipBack() } label: { Image(systemName: "backward.fill") } .buttonStyle(.plain) Button { paused.toggle() } label: { Image(systemName: paused ? "play.fill" : "pause.fill") .font(.title3) } .buttonStyle(.plain) .foregroundStyle(.cyan) Button { advance(markDone: true) } label: { Image(systemName: "forward.fill") } .buttonStyle(.plain) } if let next = nextName { Text("Ensuite · \(next)") .font(.caption2) .foregroundStyle(.secondary) .lineLimit(1) } } .padding(.horizontal, 4) } private var completion: some View { VStack(spacing: 10) { Image(systemName: "checkmark.circle.fill") .font(.largeTitle) .foregroundStyle(.green) Text("\(block.title) terminé") .font(.footnote) .multilineTextAlignment(.center) Button("Fermer") { dismiss() } .tint(.cyan) } .padding() } // MARK: - Déroulé private var progress: Double { guard let total = current.map({ Double(secondsFor($0)) }), total > 0 else { return 0 } return max(0, min(1, remaining / total)) } private var timeLabel: String { // Arrondir avant de décomposer, sinon 59,8 s affiche "0:60". let t = max(0, Int(remaining.rounded(.up))) return "\(t / 60):" + String(format: "%02d", t % 60) } private var nextName: String? { index + 1 < block.items.count ? block.items[index + 1].name : nil } /// Durée réelle de l'exercice quand l'iPhone l'a transmise ; sinon on /// répartit celle du bloc (snapshots persistés avant le 2026-08-03). private func secondsFor(_ item: RoutineExercise) -> Int { if let d = item.durationSec, d > 0 { return d } guard !block.items.isEmpty else { return 45 } return max(15, block.durationMin * 60 / block.items.count) } private func startCurrent() { guard let cur = current else { return } remaining = Double(secondsFor(cur)) } private func step() { guard !paused, !finished, current != nil else { return } remaining -= 0.2 if remaining <= 0 { advance(markDone: true) } } private func advance(markDone: Bool) { if markDone, let cur = current, !store.done.contains(cur.id) { store.toggle(cur.id) } WKInterfaceDevice.current().play(.success) if index + 1 >= block.items.count { finished = true WKInterfaceDevice.current().play(.notification) return } index += 1 startCurrent() } private func skipBack() { guard index > 0 else { startCurrent() return } index -= 1 startCurrent() WKInterfaceDevice.current().play(.click) } }