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>
This commit is contained in:
@@ -1,12 +1,11 @@
|
||||
import SwiftUI
|
||||
import WatchKit
|
||||
|
||||
/// Routine quotidienne sur la montre : liste des blocs, cochables au doigt.
|
||||
/// 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.
|
||||
/// 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
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
@@ -34,20 +33,21 @@ struct RoutineView: View {
|
||||
|
||||
private var list: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 6) {
|
||||
LazyVStack(spacing: 6, pinnedViews: []) {
|
||||
progressHeader
|
||||
|
||||
if !store.morningBlocks.isEmpty {
|
||||
sectionTitle("Le matin")
|
||||
ForEach(store.morningBlocks) { block in
|
||||
row(block)
|
||||
}
|
||||
ForEach(store.morningBlocks) { block in
|
||||
blockSection(block)
|
||||
}
|
||||
|
||||
if !store.otherBlocks.isEmpty {
|
||||
sectionTitle("Après la course")
|
||||
Text("APRÈS LA COURSE")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.orange)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.top, 8)
|
||||
ForEach(store.otherBlocks) { block in
|
||||
row(block)
|
||||
blockSection(block)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,10 +67,10 @@ struct RoutineView: View {
|
||||
}
|
||||
|
||||
private var progressHeader: some View {
|
||||
let total = store.blocks.count
|
||||
let total = store.totalExercises
|
||||
let count = store.done.count
|
||||
return VStack(spacing: 4) {
|
||||
Text("\(count) / \(total) blocs")
|
||||
Text("\(count) / \(total) exercices")
|
||||
.font(.headline)
|
||||
.foregroundStyle(count == total && total > 0 ? .green : .primary)
|
||||
ProgressView(value: total > 0 ? Double(count) / Double(total) : 0)
|
||||
@@ -79,31 +79,55 @@ struct RoutineView: View {
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
|
||||
private func sectionTitle(_ text: String) -> some View {
|
||||
Text(text.uppercased())
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
/// 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 row(_ block: RoutineBlock) -> some View {
|
||||
let isDone = store.done.contains(block.id)
|
||||
private func exerciseRow(_ item: RoutineExercise) -> some View {
|
||||
let isDone = store.done.contains(item.id)
|
||||
return Button {
|
||||
store.toggle(block.id)
|
||||
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(block.title)
|
||||
Text(item.name)
|
||||
.font(.footnote)
|
||||
.multilineTextAlignment(.leading)
|
||||
.strikethrough(isDone, color: .secondary)
|
||||
Text("\(block.durationMin) min")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
if !item.dose.isEmpty {
|
||||
Text(item.dose)
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user