Files
coach-ios/ios/App/CoachWatch/RoutineView.swift
Sylvain Bettinelli d5e42bf662 feat(watch): coches au grain exercice sur la montre
Pendant natif du commit coach_sportif 01bad9e. Les 24 exercices sont
cochables un par un pendant la séance, comme sur le web.

- RoutineExercise + items dans RoutineBlock ; les exercices voyagent avec le
  snapshot, rien n'est codé en dur côté watchOS
- l'en-tête de bloc est tappable : coche/décoche tous ses exercices d'un coup,
  pour enchaîner une série sans s'arrêter à chaque ligne
- compteur n/N par bloc + progression globale en exercices
- CoachRoutineBridge.sanitizeBlocks transmet désormais les items

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 16:01:40 +00:00

143 lines
5.0 KiB
Swift

import SwiftUI
import WatchKit
/// Routine quotidienne sur la montre : les exercices, cochables un par un au fil
/// de la séance. Chaque coche part vers l'iPhone, qui la POSTe au serveur
/// l'état est donc partagé avec la page /routine et le widget de la home.
struct RoutineView: View {
@StateObject private var store = RoutineStore.shared
var body: some View {
Group {
if store.isEmpty {
emptyState
} else {
list
}
}
.navigationTitle("Routine")
}
private var emptyState: some View {
VStack(spacing: 8) {
Image(systemName: "figure.cooldown")
.font(.title2)
.foregroundStyle(.secondary)
Text("Ouvre l'app Coach sur l'iPhone une fois pour recevoir la routine.")
.font(.footnote)
.multilineTextAlignment(.center)
.foregroundStyle(.secondary)
}
.padding()
}
private var list: some View {
ScrollView {
LazyVStack(spacing: 6, pinnedViews: []) {
progressHeader
ForEach(store.morningBlocks) { block in
blockSection(block)
}
if !store.otherBlocks.isEmpty {
Text("APRÈS LA COURSE")
.font(.caption2)
.foregroundStyle(.orange)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.top, 8)
ForEach(store.otherBlocks) { block in
blockSection(block)
}
}
if store.lastSyncFailed {
Button {
store.retry()
} label: {
Label("Renvoyer vers l'iPhone", systemImage: "arrow.clockwise")
.font(.footnote)
}
.tint(.orange)
.padding(.top, 4)
}
}
.padding(.horizontal, 4)
}
}
private var progressHeader: some View {
let total = store.totalExercises
let count = store.done.count
return VStack(spacing: 4) {
Text("\(count) / \(total) exercices")
.font(.headline)
.foregroundStyle(count == total && total > 0 ? .green : .primary)
ProgressView(value: total > 0 ? Double(count) / Double(total) : 0)
.tint(.cyan)
}
.padding(.vertical, 4)
}
/// En-tête de bloc (tap = tout cocher d'un coup) puis ses exercices.
private func blockSection(_ block: RoutineBlock) -> some View {
let complete = store.isComplete(block)
return VStack(spacing: 4) {
Button {
store.toggleBlock(block)
WKInterfaceDevice.current().play(complete ? .click : .success)
} label: {
HStack(spacing: 6) {
Text("\(block.num). \(block.title)")
.font(.caption)
.fontWeight(.semibold)
.multilineTextAlignment(.leading)
Spacer(minLength: 0)
Text("\(store.doneCount(in: block))/\(block.items.count)")
.font(.caption2)
.monospacedDigit()
.foregroundStyle(complete ? .green : .secondary)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
.buttonStyle(.plain)
.padding(.top, 6)
ForEach(block.items) { item in
exerciseRow(item)
}
}
}
private func exerciseRow(_ item: RoutineExercise) -> some View {
let isDone = store.done.contains(item.id)
return Button {
store.toggle(item.id)
WKInterfaceDevice.current().play(isDone ? .click : .success)
} label: {
HStack(spacing: 8) {
Image(systemName: isDone ? "checkmark.circle.fill" : "circle")
.foregroundStyle(isDone ? .green : .secondary)
VStack(alignment: .leading, spacing: 1) {
Text(item.name)
.font(.footnote)
.multilineTextAlignment(.leading)
.strikethrough(isDone, color: .secondary)
if !item.dose.isEmpty {
Text(item.dose)
.font(.caption2)
.foregroundStyle(.secondary)
}
}
Spacer(minLength: 0)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
.buttonStyle(.plain)
.padding(.vertical, 6)
.padding(.horizontal, 8)
.background(Color.gray.opacity(isDone ? 0.10 : 0.18), in: RoundedRectangle(cornerRadius: 8))
.opacity(isDone ? 0.65 : 1)
}
}