126 lines
5.1 KiB
Swift
126 lines
5.1 KiB
Swift
import SwiftUI
|
|
|
|
struct WatchMeasurementView: View {
|
|
@EnvironmentObject private var model: WatchAppModel
|
|
@Environment(\.dismiss) private var dismiss
|
|
@State private var errorMessage: String?
|
|
@State private var previewStartedAt: Date?
|
|
@State private var previewCompleted = false
|
|
|
|
var body: some View {
|
|
WatchFigmaCanvas {
|
|
Group {
|
|
if previewCompleted {
|
|
WatchMeasurementResultView()
|
|
} else if case let .completed(summary) = model.measurement.state, !WatchDesignMode.enabled {
|
|
WatchMeasurementResultView(summary: summary)
|
|
} else {
|
|
TimelineView(.periodic(from: .now, by: 1)) { context in
|
|
let seconds = remainingSeconds(at: context.date)
|
|
VStack(spacing: 0) {
|
|
WatchEyebrow(title: "Analyse")
|
|
Spacer(minLength: 10)
|
|
WatchSignalRing(progress: 1 - Double(seconds) / 60)
|
|
Text(watchTime(TimeInterval(seconds)))
|
|
.font(WatchTheme.font(15, weight: .bold)).monospacedDigit()
|
|
.padding(.top, 12)
|
|
Text("Restez immobile")
|
|
.font(WatchTheme.font(14)).foregroundStyle(WatchTheme.secondary.opacity(0.6))
|
|
.padding(.top, 6)
|
|
Spacer(minLength: 12)
|
|
Button("Fermer") { close() }.buttonStyle(WatchPillStyle())
|
|
}
|
|
.onChange(of: seconds) { _, value in
|
|
if WatchDesignMode.enabled && !WatchDesignMode.frozen && value == 0 { previewCompleted = true }
|
|
}
|
|
}
|
|
}
|
|
}.padding(.horizontal, 20).padding(.vertical, 12)
|
|
}
|
|
.task { await startMeasurement() }
|
|
.alert(errorMessage ?? "", isPresented: Binding(get: { errorMessage != nil }, set: { if !$0 { errorMessage = nil } })) {
|
|
Button("Fermer") { close() }
|
|
}
|
|
.onChange(of: model.measurement.state) { _, state in
|
|
if !WatchDesignMode.enabled, case let .failed(message) = state { errorMessage = message }
|
|
}
|
|
.onDisappear {
|
|
guard !WatchDesignMode.enabled else { return }
|
|
switch model.measurement.state {
|
|
case .waitingForFirstSample, .collecting: model.measurement.cancel()
|
|
default: break
|
|
}
|
|
}
|
|
}
|
|
|
|
private func remainingSeconds(at date: Date) -> Int {
|
|
if WatchDesignMode.enabled {
|
|
guard !WatchDesignMode.frozen, let previewStartedAt else { return 60 }
|
|
return max(0, 60 - Int(date.timeIntervalSince(previewStartedAt)))
|
|
}
|
|
switch model.measurement.state {
|
|
case let .collecting(progress): return max(0, Int((60 * (1 - progress)).rounded(.up)))
|
|
case .finishing: return 0
|
|
default: return 60
|
|
}
|
|
}
|
|
|
|
private func startMeasurement() async {
|
|
if WatchDesignMode.enabled { previewStartedAt = Date(); return }
|
|
do {
|
|
model.measurement.resetAfterResult()
|
|
try await model.measurement.requestAuthorization()
|
|
guard !Task.isCancelled else { return }
|
|
try await model.measurement.start()
|
|
} catch { errorMessage = error.localizedDescription }
|
|
}
|
|
|
|
private func close() {
|
|
if !WatchDesignMode.enabled { model.measurement.cancel(); model.measurement.resetAfterResult() }
|
|
dismiss()
|
|
}
|
|
}
|
|
|
|
struct WatchSignalRing: View {
|
|
let progress: Double
|
|
var body: some View {
|
|
ZStack {
|
|
Circle().stroke(Color(red: 25 / 255, green: 32 / 255, blue: 44 / 255).opacity(0.4), lineWidth: 6)
|
|
Circle().stroke(WatchTheme.accent.opacity(0.8), lineWidth: 6)
|
|
Circle().stroke(WatchTheme.accent.opacity(0.6), lineWidth: 2)
|
|
Circle().fill(WatchTheme.accent.opacity(0.2)).frame(width: 40, height: 40)
|
|
.overlay(Circle().stroke(WatchTheme.accent.opacity(0.3)))
|
|
.shadow(color: WatchTheme.accent.opacity(0.3), radius: 8)
|
|
Image("WatchHeart").resizable().scaledToFit().frame(width: 16.667, height: 15.292)
|
|
}.frame(width: 72, height: 72).padding(3)
|
|
.accessibilityLabel("Analyse").accessibilityValue("\(Int(progress * 100)) pour cent")
|
|
}
|
|
}
|
|
|
|
struct WatchMeasurementResultView: View {
|
|
@EnvironmentObject private var model: WatchAppModel
|
|
@Environment(\.dismiss) private var dismiss
|
|
var summary: WatchMeasurementSummaryV1? = nil
|
|
|
|
var body: some View {
|
|
VStack(spacing: 0) {
|
|
WatchEyebrow(title: "RÉSULTAT")
|
|
Spacer(minLength: 12)
|
|
HStack(alignment: .firstTextBaseline, spacing: 0) {
|
|
Text(WatchDesignMode.enabled ? "72" : "—")
|
|
.font(WatchTheme.font(64, weight: .bold)).tracking(-3.2)
|
|
.foregroundStyle(LinearGradient(colors: [WatchTheme.mint, WatchTheme.accent], startPoint: .topLeading, endPoint: .bottomTrailing))
|
|
Text("%").font(WatchTheme.font(14, weight: .medium)).foregroundStyle(WatchTheme.mint.opacity(0.8))
|
|
}.foregroundStyle(WatchTheme.accent).shadow(color: WatchTheme.accent.opacity(0.5), radius: 5)
|
|
WatchBalanceAxis(position: WatchDesignMode.enabled ? 0.77 : nil).padding(.top, 14)
|
|
Text(WatchDesignMode.enabled ? "Équilibre favorable" : "")
|
|
.font(WatchTheme.font(10)).foregroundStyle(WatchTheme.secondary).padding(.top, 14)
|
|
Spacer(minLength: 12)
|
|
Button("Fermer") {
|
|
if !WatchDesignMode.enabled { model.measurement.resetAfterResult() }
|
|
dismiss()
|
|
}.buttonStyle(WatchPillStyle())
|
|
}
|
|
}
|
|
}
|