Files
haptika/ios/HaptikaWatch/Views/WatchRootView.swift
T
2026-09-07 16:02:36 +02:00

207 lines
8.6 KiB
Swift
Raw 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 WatchRootView: View {
@EnvironmentObject private var model: WatchAppModel
var body: some View {
Group {
if WatchDesignMode.enabled || model.hasCompletedOnboarding {
WatchHomeView()
} else {
WatchOnboardingView()
}
}
.preferredColorScheme(.dark)
}
}
struct WatchOnboardingView: View {
@EnvironmentObject private var model: WatchAppModel
@State private var page = 0
@State private var authorizationError: String?
var body: some View {
VStack(spacing: 8) {
TabView(selection: $page) {
onboardingPage(icon: "hand.raised.fill", title: "Vos données", text: "Les mesures restent sur vos appareils et se synchronisent avec votre iPhone appairé.").tag(0)
onboardingPage(icon: "heart.text.square.fill", title: "Bien-être, pas diagnostic", text: "Haptika n’est pas un dispositif médical. Consultez un professionnel en cas de symptôme.").tag(1)
onboardingPage(icon: "waveform.path.ecg", title: "Mesurer sans vibrer", text: "La Watch mesure le pouls optique. Les vibrations sont toujours arrêtées pendant cette phase.").tag(2)
}
.tabViewStyle(.verticalPage)
if let authorizationError {
Text(authorizationError).font(.caption2).foregroundStyle(.orange)
}
Button(page < 2 ? "Continuer" : "Autoriser Santé") {
if page < 2 {
withAnimation { page += 1 }
} else {
Task {
do {
try await model.measurement.requestAuthorization()
model.hasCompletedOnboarding = true
} catch {
authorizationError = error.localizedDescription
}
}
}
}
.buttonStyle(WatchPillStyle(primary: true))
Button("Plus tard") { model.hasCompletedOnboarding = true }
.font(.system(size: 11)).buttonStyle(.plain).foregroundStyle(WatchTheme.secondary)
}
.padding(.horizontal, 6)
.background(WatchTheme.background.ignoresSafeArea())
}
private func onboardingPage(icon: String, title: String, text: String) -> some View {
ScrollView { VStack(spacing: 10) {
Image(systemName: icon).font(.system(size: 34)).foregroundStyle(WatchTheme.accent)
Text(title).font(.headline).multilineTextAlignment(.center)
Text(text).font(.system(size: 12)).foregroundStyle(WatchTheme.secondary).multilineTextAlignment(.center).fixedSize(horizontal: false, vertical: true)
}.padding(.horizontal, 6) }
}
}
enum WatchFigmaScreen: String, Hashable, CaseIterable {
case home, measure, result, breathe, prepare, breathing, anchor, anchoring, meditate, meditation, sleep, wake, face
}
struct WatchHomeView: View {
@State private var page = 0
@State private var path: [WatchFigmaScreen] = WatchDesignMode.initialScreen.flatMap { $0 == .home ? nil : [$0] } ?? []
var body: some View {
NavigationStack(path: $path) {
TabView(selection: $page) {
dashboard.tag(0)
WatchRoutineList(category: .breathe).tag(1)
WatchRoutineList(category: .anchor).tag(2)
WatchRoutineList(category: .meditate).tag(3)
WatchWakeView(onStop: { page = 0 }).tag(4)
WatchFacePreview().tag(5)
}
.tabViewStyle(.verticalPage)
.navigationDestination(for: WatchFigmaScreen.self) { WatchFigmaDestination(screen: $0) }
.haptikaScreen()
.toolbar(.hidden, for: .navigationBar)
.persistentSystemOverlays(.hidden)
}
}
private var dashboard: some View {
WatchFigmaCanvas {
VStack(spacing: 9) {
WatchEyebrow(title: "mon état actuel")
ZStack {
Circle().stroke(.white.opacity(0.05), lineWidth: 4)
VStack(spacing: 8) {
HStack(alignment: .firstTextBaseline, spacing: 0) {
Text(WatchDesignMode.enabled ? "72" : "—")
.font(WatchTheme.font(40, weight: .bold)).tracking(-2.4)
Text("%").font(WatchTheme.font(10, weight: .bold))
}
WatchBalanceAxis(compact: true, position: WatchDesignMode.enabled ? 0.72 : nil)
}
}.frame(width: 99, height: 99).padding(.vertical, 2)
NavigationLink(value: WatchFigmaScreen.measure) {
Text("Mesurer la VCF".uppercased())
}.buttonStyle(WatchPillStyle(primary: true, height: 29, fontSize: 10))
NavigationLink(value: WatchFigmaScreen.measure) {
Text("Actualiser mon état".uppercased())
}.buttonStyle(WatchPillStyle(primary: true, height: 29, fontSize: 10))
}.padding(.horizontal, 24).padding(.top, 14).padding(.bottom, 2)
}
}
}
struct WatchFigmaDestination: View {
let screen: WatchFigmaScreen
var body: some View {
switch screen {
case .home: WatchHomeView()
case .measure: WatchMeasurementView()
case .result: WatchFigmaCanvas { WatchMeasurementResultView().padding(.horizontal, 20) }
case .breathe: WatchRoutineList(category: .breathe)
case .prepare: WatchRoutineView(routine: WatchRoutineCategory.breathe.routines[0])
case .breathing: WatchRoutineView(routine: WatchRoutineCategory.breathe.routines[0], initiallyRunning: true)
case .anchor: WatchRoutineList(category: .anchor)
case .anchoring: WatchRoutineView(routine: WatchRoutineCategory.anchor.routines[0])
case .meditate: WatchRoutineList(category: .meditate)
case .meditation: WatchRoutineView(routine: WatchRoutineCategory.meditate.routines[0])
case .sleep: WatchRoutineView(routine: WatchRoutineCategory.anchor.routines[4])
case .wake: WatchWakeView()
case .face: WatchFacePreview()
}
}
}
struct WatchWakeView: View {
@Environment(\.dismiss) private var dismiss
var onStop: (() -> Void)?
var body: some View {
WatchFigmaCanvas(background: WatchTheme.night) {
VStack(spacing: 12) {
Text("RÉVEIL").font(.system(size: 12, weight: .semibold)).tracking(1.2).foregroundStyle(WatchTheme.secondary.opacity(0.8))
Spacer(minLength: 0)
Group {
if WatchDesignMode.enabled {
Text("7:12")
} else {
TimelineView(.periodic(from: .now, by: 60)) { context in Text(context.date, style: .time) }
}
}.font(.system(size: 44, weight: .bold)).monospacedDigit()
.background { WatchGlow() }
Spacer(minLength: 0)
Button { if let onStop { onStop() } else { dismiss() } } label: {
HStack(spacing: 8) {
Image("WatchStop").resizable().scaledToFit().frame(width: 20, height: 20)
Text("Arrêter")
}
}.buttonStyle(WatchPillStyle(destructive: true, height: 48))
}.padding(.horizontal, 16).padding(.vertical, 10)
}.ignoresSafeArea(.container, edges: .bottom)
}
}
struct WatchFacePreview: View {
var body: some View {
WatchFigmaCanvas {
VStack(spacing: 0) {
Group {
if WatchDesignMode.enabled { Text("10:09") }
else { TimelineView(.periodic(from: .now, by: 60)) { context in Text(context.date, style: .time) } }
}.font(.system(size: 32, weight: .bold))
Text("HAPTIKA").font(.system(size: 10, weight: .light)).tracking(1).foregroundStyle(WatchTheme.accent)
Spacer(minLength: 36)
HStack {
NavigationLink(value: WatchFigmaScreen.measure) {
VStack(spacing: 4) {
Text(WatchDesignMode.enabled ? "68" : "—").font(WatchTheme.font(18, weight: .bold))
.frame(width: 52, height: 52)
.background(WatchTheme.background, in: Circle())
.overlay(Circle().stroke(WatchTheme.accent.opacity(0.12), lineWidth: 2).padding(4))
.shadow(color: WatchTheme.accent.opacity(0.2), radius: 4)
Text("VFC").font(WatchTheme.font(9)).tracking(0.9).foregroundStyle(WatchTheme.secondary)
}
}
Spacer()
NavigationLink {
WatchRoutineView(routine: .init(title: "Sos Stress", protocolID: "sosStress", category: .anchor))
} label: {
VStack(spacing: 4) {
Image("WatchSOS").resizable().scaledToFit().frame(width: 21, height: 21)
.frame(width: 52, height: 52)
.background(Color(red: 0.957, green: 0.247, blue: 0.369).opacity(0.1), in: Circle())
.overlay(Circle().stroke(Color(red: 0.957, green: 0.247, blue: 0.369), lineWidth: 2))
.shadow(color: Color(red: 0.957, green: 0.247, blue: 0.369).opacity(0.5), radius: 6)
Text("SOS").font(WatchTheme.font(9)).tracking(0.9).foregroundStyle(Color(red: 0.957, green: 0.247, blue: 0.369))
}
}
}.buttonStyle(.plain)
}.padding(.horizontal, 23).padding(.vertical, 16).background { WatchGlow() }
}
}
}