feat: implement Haptika PPG and Watch V1
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
import SwiftUI
|
||||
|
||||
struct WatchExerciseListView: View {
|
||||
private var hapticProtocols: [WatchRegulationProtocol] {
|
||||
WatchProtocolCatalog.protocols.filter { $0.mode == .haptic }
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
List(hapticProtocols) { regulation in
|
||||
NavigationLink {
|
||||
WatchExercisePlayerView(regulation: regulation)
|
||||
} label: {
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
Text(regulation.title).font(.subheadline.bold())
|
||||
Text(formatDuration(regulation.durationSeconds)).font(.caption2).foregroundStyle(WatchTheme.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Exercices")
|
||||
.haptikaScreen()
|
||||
}
|
||||
}
|
||||
|
||||
struct WatchExercisePlayerView: View {
|
||||
@EnvironmentObject private var model: WatchAppModel
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
let regulation: WatchRegulationProtocol
|
||||
@State private var startedAt: Date?
|
||||
@State private var errorMessage: String?
|
||||
@State private var sessionSaved = false
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 10) {
|
||||
if model.haptics.isRunning, let startedAt {
|
||||
BreathOrb(regulation: regulation, startedAt: startedAt)
|
||||
ProgressView(value: model.haptics.elapsedSeconds, total: regulation.durationSeconds)
|
||||
Text("\(Int(max(0, regulation.durationSeconds - model.haptics.elapsedSeconds))) s")
|
||||
.font(.caption.monospacedDigit()).foregroundStyle(WatchTheme.secondary)
|
||||
Button("Arrêter", role: .cancel) {
|
||||
saveSession(state: .cancelled)
|
||||
model.haptics.cancel()
|
||||
}
|
||||
} else if startedAt != nil {
|
||||
Image(systemName: "checkmark.circle.fill").font(.title).foregroundStyle(WatchTheme.mint)
|
||||
Text("Exercice terminé").font(.headline)
|
||||
Button("Terminer") { dismiss() }.buttonStyle(.borderedProminent)
|
||||
.onAppear { saveSession(state: .completed) }
|
||||
} else {
|
||||
Text(regulation.eyebrow).font(.caption2.bold()).foregroundStyle(WatchTheme.accent)
|
||||
Text(regulation.title).font(.headline).multilineTextAlignment(.center)
|
||||
Text(regulation.summary).font(.caption2).foregroundStyle(WatchTheme.secondary).multilineTextAlignment(.center)
|
||||
Text("Aucune mesure cardiaque pendant les vibrations.").font(.caption2.bold()).foregroundStyle(.orange)
|
||||
Button("Commencer") { start() }.buttonStyle(.borderedProminent)
|
||||
NavigationLink("Parcours avant / après") {
|
||||
WatchBeforeAfterFlowView(regulation: regulation)
|
||||
}
|
||||
.font(.caption)
|
||||
}
|
||||
if let errorMessage { Text(errorMessage).font(.caption2).foregroundStyle(.orange) }
|
||||
}
|
||||
.padding(.horizontal, 4)
|
||||
}
|
||||
.navigationTitle(regulation.title)
|
||||
.haptikaScreen()
|
||||
.onDisappear {
|
||||
if model.haptics.isRunning {
|
||||
saveSession(state: .cancelled)
|
||||
model.haptics.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func start() {
|
||||
let now = Date()
|
||||
Task {
|
||||
do {
|
||||
try await model.haptics.start(protocol: regulation)
|
||||
startedAt = now
|
||||
} catch {
|
||||
errorMessage = error.localizedDescription
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func saveSession(state: WatchRegulationSessionState) {
|
||||
guard !sessionSaved, let startedAt else { return }
|
||||
sessionSaved = true
|
||||
model.enqueueSession(.init(
|
||||
protocolID: regulation.id,
|
||||
startedAt: startedAt,
|
||||
endedAt: Date(),
|
||||
durationSeconds: model.haptics.elapsedSeconds,
|
||||
state: state
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
private func formatDuration(_ duration: TimeInterval) -> String {
|
||||
let seconds = Int(duration)
|
||||
return seconds < 60 ? "\(seconds) s" : "\(seconds / 60) min \(seconds % 60 == 0 ? "" : "\(seconds % 60) s")"
|
||||
}
|
||||
Reference in New Issue
Block a user