From 12b56a4d393bbdc9fa042b8d6a4f7127d14b49a2 Mon Sep 17 00:00:00 2001 From: Sylvain Bettinelli Date: Tue, 11 Aug 2026 17:56:53 +0000 Subject: [PATCH] =?UTF-8?q?Le=20widget=20disait=20=C2=AB=20JOUR=20OFF=20?= =?UTF-8?q?=C2=BB=20les=20jours=20de=20sortie=20non=20planifiee?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pendant du commit web 25f53b5. Quand la seance prevue est remplacee par une autre activite, le plan du jour se vide : le widget iPhone affichait « JOUR OFF / Pas de seance prevue » et la complication Watch « Jour OFF » — le jour meme d'une sortie de 20 km. Le snapshot ne portait que le PLAN. CoachWidgetSnapshot gagne doneToday (sport, titre, sous-titre), optionnel : un snapshot ecrit par une version anterieure reste decodable. today decrit le plan, doneToday decrit la journee ; les deux peuvent coexister. CoachWidgetBridge le conserve quand l'appel l'omet, comme la nutrition — mais seulement si le snapshot precedent date d'aujourd'hui, sinon la sortie de la veille resterait affichee indefiniment. Les deux vues basculent sur l'activite realisee, avec la pastille verte « Realisee » deja utilisee pour une seance planifiee faite. Le sous-titre (« 20,3 km · 56 min ») passe devant le titre : le titre d'une seance importee est souvent vide. ⚠️ NON COMPILE — a builder au prochain passage sur le Mac. Aucun fichier nouveau, donc rien a cabler dans le pbxproj : les quatre fichiers touches sont deja membres de leurs cibles. Co-Authored-By: Claude Opus 5 (1M context) --- ios/App/App/CoachWidgetBridge.swift | 17 ++++++++ ios/App/App/CoachWidgetSnapshot.swift | 17 ++++++++ ios/App/CoachLiveActivity/CoachWidgets.swift | 40 +++++++++++++++++-- .../CoachWatchComplications.swift | 19 +++++++++ 4 files changed, 89 insertions(+), 4 deletions(-) diff --git a/ios/App/App/CoachWidgetBridge.swift b/ios/App/App/CoachWidgetBridge.swift index 611c193..a44b336 100644 --- a/ios/App/App/CoachWidgetBridge.swift +++ b/ios/App/App/CoachWidgetBridge.swift @@ -58,7 +58,24 @@ public class CoachWidgetBridgePlugin: CAPPlugin, CAPBridgedPlugin { (call.getValue(key) as? NSNumber)?.doubleValue } + var done: CoachWidgetSnapshot.DoneActivity? + if let d = call.getObject("doneToday") { + done = CoachWidgetSnapshot.DoneActivity( + sport: d["sport"] as? String ?? "other", + title: d["title"] as? String, + subtitle: d["subtitle"] as? String + ) + } + var snapshot = CoachWidgetSnapshot(updatedAt: Date(), today: today, forme: forme) + /* `doneToday` suit la même règle que la nutrition : absent de l'appel + → on garde le précédent. Un appel partiel effacerait sinon l'activité + du jour, et le widget rebasculerait sur « JOUR OFF ». + ⚠️ Mais seulement dans la journée : au lendemain, un snapshot qui + n'annonce plus rien doit repartir vide, sans quoi la sortie de la + veille resterait affichée indéfiniment. */ + snapshot.doneToday = done ?? (Calendar.current.isDateInToday(previous?.updatedAt ?? .distantPast) + ? previous?.doneToday : nil) snapshot.waterMlToday = number("waterMlToday") ?? previous?.waterMlToday snapshot.coffeeCountToday = (call.getValue("coffeeCountToday") as? NSNumber)?.intValue ?? previous?.coffeeCountToday diff --git a/ios/App/App/CoachWidgetSnapshot.swift b/ios/App/App/CoachWidgetSnapshot.swift index 2be05cf..900729a 100644 --- a/ios/App/App/CoachWidgetSnapshot.swift +++ b/ios/App/App/CoachWidgetSnapshot.swift @@ -60,6 +60,16 @@ struct CoachWidgetSnapshot: Codable { var kcalGoal: Double? = nil + /// Activité RÉELLEMENT réalisée aujourd'hui, indépendante du plan. + /// + /// Sans elle, un jour où la séance prévue est remplacée par une autre + /// activité (bouton « j'ai fait du VTT », qui décale la séquence et vide + /// le jour) s'affiche « JOUR OFF » — le jour même d'une sortie de 20 km. + /// `today` décrit le plan, `doneToday` décrit la journée : les deux + /// peuvent coexister, l'un être nil sans l'autre. + /// Optionnel : un snapshot écrit par une version antérieure reste lisible. + var doneToday: DoneActivity? = nil + /// Séance planifiée du jour (nil = jour OFF / pas de séance). struct TodaySession: Codable { var sport: String // running, cycling, strength, mobility, hiking, rest… @@ -68,6 +78,13 @@ struct CoachWidgetSnapshot: Codable { var done: Bool // séance déjà réalisée aujourd'hui } + /// Ce qui a été fait dans la journée : sport, et de quoi le qualifier. + struct DoneActivity: Codable { + var sport: String // running, cycling, walking, strength… + var title: String? // nom de la séance, souvent absent + var subtitle: String? // "20,3 km · 56 min" + } + /// Score de forme / récupération (nil = pas encore de score). struct FormeScore: Codable { var score: Int? // 0–100 diff --git a/ios/App/CoachLiveActivity/CoachWidgets.swift b/ios/App/CoachLiveActivity/CoachWidgets.swift index 5634bf8..63ac695 100644 --- a/ios/App/CoachLiveActivity/CoachWidgets.swift +++ b/ios/App/CoachLiveActivity/CoachWidgets.swift @@ -167,15 +167,47 @@ struct CoachTodaySessionEntryView: View { .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) } + /// Rien au plan. Deux situations très différentes, longtemps confondues : + /// une vraie journée de repos, et une journée où le plan a été vidé parce + /// que l'activité a été faite autrement. Annoncer « JOUR OFF » à qui vient + /// de rouler 20 km efface son effort — d'où la bascule sur `doneToday`. + @ViewBuilder private var emptyView: some View { + if let d = entry.snapshot?.doneToday { + doneView(d) + } else { + VStack(alignment: .leading, spacing: 6) { + HStack(spacing: 6) { + Image(systemName: "moon.zzz.fill").foregroundStyle(.secondary) + Text("JOUR OFF").font(.caption2.weight(.bold)).foregroundStyle(.secondary) + } + Spacer(minLength: 0) + Text("Pas de séance prévue") + .font(.subheadline).foregroundStyle(.secondary) + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) + } + } + + private func doneView(_ d: CoachWidgetSnapshot.DoneActivity) -> some View { VStack(alignment: .leading, spacing: 6) { HStack(spacing: 6) { - Image(systemName: "moon.zzz.fill").foregroundStyle(.secondary) - Text("JOUR OFF").font(.caption2.weight(.bold)).foregroundStyle(.secondary) + Image(systemName: sportSymbol(d.sport)) + .foregroundStyle(sportColor(d.sport)) + Text(sportLabel(d.sport).uppercased()) + .font(.caption2.weight(.bold)).foregroundStyle(.secondary) + Spacer() + Image(systemName: "checkmark.seal.fill") + .foregroundStyle(.green).font(.caption) } Spacer(minLength: 0) - Text("Pas de séance prévue") - .font(.subheadline).foregroundStyle(.secondary) + // Le sous-titre porte la substance (« 20,3 km · 56 min ») ; le + // titre d'une séance importée est souvent vide, d'où le repli. + Text(d.subtitle ?? d.title ?? "Séance réalisée") + .font(.headline).lineLimit(2).minimumScaleFactor(0.8) + if d.subtitle != nil, let t = d.title, !t.isEmpty { + Text(t).font(.caption).foregroundStyle(.secondary).lineLimit(1) + } } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) } diff --git a/ios/App/CoachWatchWidgets/CoachWatchComplications.swift b/ios/App/CoachWatchWidgets/CoachWatchComplications.swift index faaacc5..9c6d203 100644 --- a/ios/App/CoachWatchWidgets/CoachWatchComplications.swift +++ b/ios/App/CoachWatchWidgets/CoachWatchComplications.swift @@ -162,6 +162,25 @@ struct CoachWatchSessionView: View { } } } + } else if let d = entry.snapshot?.doneToday { + /* Rien au plan, mais quelque chose a été fait : le plan se vide + quand l'activité est remplacée (« j'ai fait du VTT »). + Afficher « Jour OFF » ce jour-là est faux au poignet de + quelqu'un qui vient de rentrer de sortie. */ + if family == .accessoryInline { + Label(d.subtitle ?? watchSportLabel(d.sport), + systemImage: watchSportSymbol(d.sport)) + } else { + VStack(alignment: .leading, spacing: 2) { + Label(watchSportLabel(d.sport).uppercased(), + systemImage: watchSportSymbol(d.sport)) + .font(.caption2.weight(.bold)) + .foregroundStyle(.secondary) + Text(d.subtitle ?? d.title ?? "Séance réalisée") + .font(.headline).lineLimit(1) + Text("Réalisée").font(.caption2).foregroundStyle(.green) + } + } } else { if family == .accessoryInline { Label("Jour OFF", systemImage: "moon.zzz.fill")