import SwiftUI struct WatchMeasurementView: View { @EnvironmentObject private var model: WatchAppModel var body: some View { ScrollView { VStack(spacing: 10) { switch model.measurement.state { case .idle, .authorizing: preparation case let .waitingForFirstSample(secondsRemaining): liveHeader(bpm: nil, caption: "Recherche du signal · \(secondsRemaining) s") ProgressView() cancelButton case let .collecting(progress): liveHeader(bpm: model.measurement.currentBPM, caption: "Mesure en cours") ProgressView(value: progress) Text("Restez immobile, bras détendu").font(.caption2).foregroundStyle(WatchTheme.secondary) cancelButton case .finishing: ProgressView("Analyse…") case let .completed(summary): WatchMeasurementResultView(summary: summary) case let .failed(message): Image(systemName: "exclamationmark.triangle.fill").foregroundStyle(.orange) Text("Mesure interrompue").font(.headline) Text(message).font(.caption2).foregroundStyle(WatchTheme.secondary) Button("Recommencer") { model.measurement.resetAfterResult() } } } .padding(.horizontal, 4) } .navigationTitle("Mesurer") .haptikaScreen() } private var preparation: some View { VStack(spacing: 10) { Image(systemName: "applewatch.radiowaves.left.and.right").font(.system(size: 38)).foregroundStyle(WatchTheme.mint) Text("60 secondes au repos").font(.headline) Text("Serrez le bracelet sans gêne et posez l’avant-bras. Haptika attendra jusqu’à 30 s le premier BPM.") .font(.caption2) .foregroundStyle(WatchTheme.secondary) .multilineTextAlignment(.center) Button("Commencer") { Task { do { try await model.measurement.requestAuthorization() try await model.measurement.start() } catch {} } } .buttonStyle(.borderedProminent) } } private func liveHeader(bpm: Double?, caption: String) -> some View { VStack(spacing: 3) { Image(systemName: "heart.fill").foregroundStyle(.pink).symbolEffect(.pulse, options: .repeating) Text(bpm.map { "\(Int($0.rounded()))" } ?? "—") .font(.system(size: 42, weight: .bold, design: .rounded)) Text(bpm == nil ? caption : "BPM · \(caption)").font(.caption2).foregroundStyle(WatchTheme.secondary) } } private var cancelButton: some View { Button("Arrêter", role: .cancel) { model.measurement.cancel() } .buttonStyle(.bordered) } } struct WatchMeasurementResultView: View { @EnvironmentObject private var model: WatchAppModel let summary: WatchMeasurementSummaryV1 private var comparison: WatchBaselineComparison { model.store.baseline.compare(bpm: summary.averageBPM) } var body: some View { VStack(spacing: 9) { Image(systemName: summary.isValid ? "checkmark.circle.fill" : "exclamationmark.triangle.fill") .font(.title) .foregroundStyle(summary.isValid ? WatchTheme.mint : .orange) Text(summary.averageBPM.map { "\(Int($0.rounded())) BPM" } ?? "Signal incomplet") .font(.title3.bold()) WatchCard { VStack(alignment: .leading, spacing: 5) { Label("Qualité \(Int((summary.quality * 100).rounded())) %", systemImage: "checkmark.seal") Text("\(Int(summary.minimumBPM ?? 0))–\(Int(summary.maximumBPM ?? 0)) BPM") Text("Couverture \(Int(summary.coverageSeconds)) s") Divider() Text(comparison.frenchLabel).foregroundStyle(WatchTheme.accent) } .font(.caption) } Text("Mesure optique de bien-être, non médicale. Aucun indice PRV n’est calculé sur la Watch.") .font(.caption2) .foregroundStyle(WatchTheme.secondary) if summary.isValid { Text("Ressenti").font(.caption2).foregroundStyle(WatchTheme.secondary) HStack(spacing: 4) { ForEach(1...5, id: \.self) { value in Button("\(value)") { model.saveFeeling(value, for: summary) } .buttonStyle(.borderless) .foregroundStyle(summary.feeling == value ? WatchTheme.mint : .white) } } } Button("Terminer") { model.measurement.resetAfterResult() } .buttonStyle(.borderedProminent) } } }