Files
haptika/ios/HaptikaWatch/Views/WatchBeforeAfterFlowView.swift
2026-07-31 16:20:10 +02:00

179 lines
6.6 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import SwiftUI
struct WatchBeforeAfterFlowView: View {
enum Stage { case intro, before, beforeFeeling, exercise, after, afterFeeling, result }
@EnvironmentObject private var model: WatchAppModel
let regulation: WatchRegulationProtocol
@State private var stage: Stage = .intro
@State private var before: WatchMeasurementSummaryV1?
@State private var after: WatchMeasurementSummaryV1?
@State private var exerciseStartedAt: Date?
@State private var flowError: String?
@State private var selectedFeeling = 3
@State private var feelingBefore: Int?
@State private var feelingAfter: Int?
var body: some View {
ScrollView {
VStack(spacing: 10) {
switch stage {
case .intro:
Image(systemName: "arrow.left.arrow.right.heart.fill").font(.title).foregroundStyle(WatchTheme.accent)
Text("Avant · Exercice · Après").font(.headline).multilineTextAlignment(.center)
Text("Deux mesures identiques encadrent l’exercice pour comparer le BPM et la qualité, sans score de stress.")
.font(.caption2).foregroundStyle(WatchTheme.secondary).multilineTextAlignment(.center)
Button("Mesurer avant") { startMeasurement(stage: .before) }.buttonStyle(.borderedProminent)
case .before, .after:
measurementContent
case .beforeFeeling:
feelingContent(title: "Comment vous sentez-vous avant ?") {
feelingBefore = selectedFeeling
if var measurement = before {
measurement.feeling = selectedFeeling
measurement.feelingRecordedAt = Date()
measurement.updatedAt = Date()
before = measurement
model.enqueueMeasurement(measurement)
}
stage = .exercise
exerciseStartedAt = Date()
Task {
do { try await model.haptics.start(protocol: regulation) }
catch { flowError = error.localizedDescription }
}
}
case .exercise:
if model.haptics.isRunning, let exerciseStartedAt {
BreathOrb(regulation: regulation, startedAt: exerciseStartedAt)
ProgressView(value: model.haptics.elapsedSeconds, total: regulation.durationSeconds)
Button("Arrêter", role: .cancel) { model.haptics.cancel() }
} else {
Text("Exercice terminé").font(.headline)
Button("Mesurer après") { startMeasurement(stage: .after) }.buttonStyle(.borderedProminent)
}
case .afterFeeling:
feelingContent(title: "Comment vous sentez-vous après ?") {
feelingAfter = selectedFeeling
if var measurement = after {
measurement.feeling = selectedFeeling
measurement.feelingRecordedAt = Date()
measurement.updatedAt = Date()
after = measurement
model.enqueueMeasurement(measurement)
}
saveCompletedSession()
stage = .result
}
case .result:
resultContent
}
if let flowError { Text(flowError).font(.caption2).foregroundStyle(.orange) }
}
.padding(.horizontal, 4)
}
.navigationTitle("Comparaison")
.haptikaScreen()
.onChange(of: model.measurement.state) { _, newState in handleMeasurementState(newState) }
}
@ViewBuilder
private var measurementContent: some View {
switch model.measurement.state {
case let .waitingForFirstSample(seconds):
ProgressView()
Text("Recherche du signal · \(seconds) s").font(.caption2)
case let .collecting(progress):
Text(model.measurement.currentBPM.map { "\(Int($0.rounded())) BPM" } ?? "— BPM").font(.title2.bold())
ProgressView(value: progress)
Text(stage == .before ? "Mesure avant" : "Mesure après").font(.caption2)
case .finishing:
ProgressView("Analyse…")
default:
ProgressView()
}
Button("Annuler", role: .cancel) { model.measurement.cancel(); stage = .intro }
}
private var resultContent: some View {
VStack(spacing: 9) {
Text("Votre comparaison").font(.headline)
WatchCard {
VStack(alignment: .leading, spacing: 5) {
metricRow("Avant", before?.averageBPM)
metricRow("Après", after?.averageBPM)
if let beforeBPM = before?.averageBPM, let afterBPM = after?.averageBPM {
Divider()
Text(String(format: "Écart %+.0f BPM", afterBPM - beforeBPM)).foregroundStyle(WatchTheme.accent)
}
Text("Qualité : \(Int(((before?.quality ?? 0) * 100).rounded())) % → \(Int(((after?.quality ?? 0) * 100).rounded())) %")
if let feelingBefore, let feelingAfter {
Text("Ressenti : \(feelingBefore)/5 → \(feelingAfter)/5")
}
}
.font(.caption)
}
Text("Une variation ponctuelle n’est pas un diagnostic.").font(.caption2).foregroundStyle(WatchTheme.secondary)
}
}
private func metricRow(_ label: String, _ bpm: Double?) -> some View {
HStack {
Text(label)
Spacer()
Text(bpm.map { "\(Int($0.rounded())) BPM" } ?? "—")
}
}
private func startMeasurement(stage: Stage) {
self.stage = stage
model.measurement.resetAfterResult()
Task {
do {
try await model.measurement.requestAuthorization()
try await model.measurement.start()
} catch { flowError = error.localizedDescription }
}
}
private func feelingContent(title: String, onConfirm: @escaping () -> Void) -> some View {
VStack(spacing: 10) {
Text(title).font(.headline).multilineTextAlignment(.center)
Picker("Ressenti", selection: $selectedFeeling) {
ForEach(1...5, id: \.self) { score in Text("\(score) / 5").tag(score) }
}
Button("Continuer", action: onConfirm).buttonStyle(.borderedProminent)
}
}
private func saveCompletedSession() {
let endedAt = Date()
model.enqueueSession(.init(
protocolID: regulation.id,
startedAt: exerciseStartedAt ?? endedAt,
endedAt: endedAt,
durationSeconds: regulation.durationSeconds,
state: .completed,
beforeMeasurementID: before?.id,
afterMeasurementID: after?.id,
feelingBefore: feelingBefore,
feelingAfter: feelingAfter
))
}
private func handleMeasurementState(_ state: WatchMeasurementState) {
guard case let .completed(summary) = state else { return }
if stage == .before {
before = summary
model.measurement.resetAfterResult()
selectedFeeling = 3
stage = .beforeFeeling
} else if stage == .after {
after = summary
model.measurement.resetAfterResult()
selectedFeeling = 3
stage = .afterFeeling
}
}
}