merge(main): configuration du score chronométré (ticket #26)
Fusionne feature/#26-score-chrono-config — analyze propre, 41/41 tests verts, build APK debug validé. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -9,3 +9,4 @@
|
|||||||
- [gametime-architecture-set-editing](gametime-architecture-set-editing.md) — memory note gametime-architecture-set-editing
|
- [gametime-architecture-set-editing](gametime-architecture-set-editing.md) — memory note gametime-architecture-set-editing
|
||||||
- [gametime-ux-score-chrono](gametime-ux-score-chrono.md) — memory note gametime-ux-score-chrono
|
- [gametime-ux-score-chrono](gametime-ux-score-chrono.md) — memory note gametime-ux-score-chrono
|
||||||
- [gametime-architecture-score-chrono](gametime-architecture-score-chrono.md) — memory note gametime-architecture-score-chrono
|
- [gametime-architecture-score-chrono](gametime-architecture-score-chrono.md) — memory note gametime-architecture-score-chrono
|
||||||
|
- [gametime-resume-plan-2026-07-18](gametime-resume-plan-2026-07-18.md) — memory note gametime-resume-plan-2026-07-18
|
||||||
|
|||||||
48
.ideai/memory/gametime-resume-plan-2026-07-18.md
Normal file
48
.ideai/memory/gametime-resume-plan-2026-07-18.md
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
---
|
||||||
|
name: gametime-resume-plan-2026-07-18
|
||||||
|
description: memory note gametime-resume-plan-2026-07-18
|
||||||
|
metadata:
|
||||||
|
type: project
|
||||||
|
---
|
||||||
|
# GameTime — Plan de reprise (consigné suite à interruption pour limite de tokens, 2026-07-18)
|
||||||
|
|
||||||
|
## Instruction utilisateur explicite
|
||||||
|
1. Attendre le reset de tokens (~3h du matin) avant de reprendre.
|
||||||
|
2. Terminer ce qui était en cours (voir "État exact au moment de l'interruption" ci-dessous).
|
||||||
|
3. Enchaîner ensuite sur TOUS les tickets restants, en commençant par ceux du sprint **"Bug resolution"**.
|
||||||
|
4. Puis les autres tickets par ordre de priorité, en tenant compte des dépendances — l'utilisateur laisse Main juger de l'ordre exact, il ne sera pas devant l'écran.
|
||||||
|
5. **Règle importante pour les bugs** : ne PAS passer les tickets de bug en `closed` une fois corrigés — les passer en statut `QA`. L'utilisateur testera lui-même avec l'APK et les clôturera à la main.
|
||||||
|
6. Une fois absolument tout terminé, rebuild l'APK final (`flutter build apk --debug`), pour que l'utilisateur ait un seul APK à jour avec tous les tickets faits.
|
||||||
|
|
||||||
|
## État exact au moment de l'interruption
|
||||||
|
Ticket **#26** ([DevFrontend] Configuration du Score chrono — exercice + programme), branche `feature/#26-score-chrono-config`, en cours de débogage d'un dernier test qui échoue de façon persistante après plusieurs itérations :
|
||||||
|
|
||||||
|
Test : `test/presentation/exercise_library_screen_test.dart` — "basculer en chrono intégré masque l'unité et affiche le badge" (ligne ~129).
|
||||||
|
Échec : `expect(find.bySemanticsLabel('Score chrono'), findsOneWidget)` ne trouve rien, alors que :
|
||||||
|
- Le widget `MeasureBadge` (lib/presentation/exercise_library_screen.dart ~ligne 655-684) a bien la logique correcte : si `measure == WorkoutMeasure.score && scoreInputMode == ScoreInputMode.stopwatch`, `label = 'Score chrono'`, rendu via `Semantics(label: label, child: Chip(...))`.
|
||||||
|
- Le site d'appel dans `ExerciseListTile` (~ligne 246) passe bien `scoreInputMode: exercise.scoreInputMode` à `MeasureBadge`.
|
||||||
|
- Le test vérifie bien `saved.scoreInputMode == ScoreInputMode.stopwatch` à la ligne 120 (ça passe), puis reconstruit `ExerciseListTile(exercise: saved)` dans un nouveau `pumpWidget` (lignes 122-127), `pumpAndSettle()`, puis cherche le badge (ligne 129) — et ne le trouve pas.
|
||||||
|
|
||||||
|
**Hypothèse non encore vérifiée à l'interruption** : `exercise.availableMeasures` (domain/entities.dart ligne 166) dépend de `hasScoreMeasure` — vérifier si `saved.hasScoreMeasure` est bien `true` après la sauvegarde du formulaire en mode "Chrono intégré" (il est possible que le formulaire ne coche/persiste pas `hasScoreMeasure=true` correctement quand on passe directement en mode chrono sans que le switch "Score" ait été committé avant le changement de mode radio — à vérifier dans `exercise_library_screen.dart`, la logique de sauvegarde du formulaire ExerciseFormScreen, autour de la construction de l'objet Exercise final avant `save()`). C'est la piste la plus probable à vérifier en premier à la reprise, avant de relancer un nouvel aller-retour avec DevFrontend.
|
||||||
|
|
||||||
|
## Chaîne de tickets score chrono (ticket parent #18)
|
||||||
|
- #25 [DevBackend] Domain et migration — **terminé et mergé** (tag v1.5.0-debug + ce qui suit).
|
||||||
|
- #26 [DevFrontend] Configuration Score chrono — **en cours**, cf. ci-dessus. Branche `feature/#26-score-chrono-config` non mergée.
|
||||||
|
- #27 [DevFrontend] Chrono score intégré dans l'exécution — dépend de #25, #26. Pas commencé.
|
||||||
|
- #28 [DevFrontend] Affichage Score chrono dans plan/historique — dépend de #27. Pas commencé.
|
||||||
|
|
||||||
|
## État des branches/tags au moment de l'interruption
|
||||||
|
- `main`/`develop` à jour au tag `v1.5.0-debug` (inclut tickets #1-23, #12, #31).
|
||||||
|
- Branche `feature/#26-score-chrono-config` en cours, non mergée, contient le travail du ticket #26 (avec le test encore rouge).
|
||||||
|
|
||||||
|
## Tickets connus restants après #26/#27/#28
|
||||||
|
- Tout ticket dans le sprint "Bug resolution" (sprintId `abc4f969-b169-45f7-988c-daeeab762201`) — vérifier `idea_ticket_list` pour la liste à jour, le sprint pourrait contenir de nouveaux tickets créés pendant l'interruption/l'absence de l'utilisateur.
|
||||||
|
- Vérifier aussi `idea_ticket_list` pour tout ticket créé par l'utilisateur pendant l'absence (comme le ticket #31 et #18 créés en cours de route précédemment) — ne pas supposer que la liste est figée.
|
||||||
|
|
||||||
|
## Environnement de build (rappel)
|
||||||
|
Voir mémoire "gametime-dev-environment" pour les commandes exactes (ANDROID_HOME, PATH, flutter analyze/test/build). Toujours vérifier soi-même (Main) via Bash après chaque implémentation d'agent, les sandbox des agents n'ont pas accès réseau.
|
||||||
|
|
||||||
|
## Process à respecter pour chaque ticket restant
|
||||||
|
Même cycle que jusqu'ici : Git crée la branche → agent (DevBackend/DevFrontend selon le domaine) implémente → Main vérifie réellement (pub get si besoin, build_runner si schéma Drift touché, analyze, test, build apk) → relais des échecs réels bruts à l'agent si besoin → Git merge dans main + develop + nettoyage branche + tag incrémental (vX.Y.0-debug). Consulter UX/Architect en amont si un ticket touche à la conception produit ou au modèle de données de façon non triviale (comme cela a été fait pour #18 → #25/#26/#27/#28).
|
||||||
|
|
||||||
|
Pour les tickets de type [Bug] : à la fin de l'implémentation et de la vérification technique (analyze/test/build OK), passer le statut à `QA` (pas `closed`) et laisser un message clair dans le carnet du ticket résumant ce qui a été corrigé, pour que l'utilisateur puisse tester sur l'APK final.
|
||||||
@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
issueRef: "#26"
|
issueRef: "#26"
|
||||||
version: 2
|
version: 4
|
||||||
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
updatedAt: 1784328255774
|
updatedAt: 1784358479742
|
||||||
---
|
---
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
id: "37a022d8-b152-48c9-a4d1-72528e2d63af"
|
id: "37a022d8-b152-48c9-a4d1-72528e2d63af"
|
||||||
number: 26
|
number: 26
|
||||||
title: "[DevFrontend] Configuration du Score chrono (exercice + programme)"
|
title: "[DevFrontend] Configuration du Score chrono (exercice + programme)"
|
||||||
status: "open"
|
status: "closed"
|
||||||
priority: "high"
|
priority: "high"
|
||||||
sprint: null
|
sprint: null
|
||||||
links: [{"target":"#18","kind":"relatesTo"},{"target":"#25","kind":"dependsOn"}]
|
links: [{"target":"#18","kind":"relatesTo"},{"target":"#25","kind":"dependsOn"}]
|
||||||
@ -10,7 +10,7 @@ agentRefs: [{"agentId":"9933c93a-b8a1-4164-a3bb-7063fdad747d","role":"assigned"}
|
|||||||
createdBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
createdBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
createdAt: 1784328244539
|
createdAt: 1784328244539
|
||||||
updatedAt: 1784328255774
|
updatedAt: 1784358479742
|
||||||
version: 2
|
version: 4
|
||||||
---
|
---
|
||||||
Dans la création/édition d'exercice (lib/presentation/exercise_library_screen.dart), si Score est activé, ajouter le sous-choix "Mode de saisie" : Saisie libre (actuel) vs Chrono intégré (label par défaut "Temps réalisé", pas d'unité libre). Badge résumé "Score chrono" pour ce mode. Dans l'écran de personnalisation d'exercice en programme (lib/presentation/program_screen.dart), pour un exercice en Score chrono : champ "Objectif de chrono" optionnel au lieu de "cible score". Avertissement si Temps ET Score chrono actifs simultanément. Cf. mémoire "gametime-ux-score-chrono" pour le détail complet.
|
Dans la création/édition d'exercice (lib/presentation/exercise_library_screen.dart), si Score est activé, ajouter le sous-choix "Mode de saisie" : Saisie libre (actuel) vs Chrono intégré (label par défaut "Temps réalisé", pas d'unité libre). Badge résumé "Score chrono" pour ce mode. Dans l'écran de personnalisation d'exercice en programme (lib/presentation/program_screen.dart), pour un exercice en Score chrono : champ "Objectif de chrono" optionnel au lieu de "cible score". Avertissement si Temps ET Score chrono actifs simultanément. Cf. mémoire "gametime-ux-score-chrono" pour le détail complet.
|
||||||
@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
issueRef: "#35"
|
issueRef: "#35"
|
||||||
version: 3
|
version: 5
|
||||||
updatedBy: {"kind":"user"}
|
updatedBy: {"kind":"user"}
|
||||||
updatedAt: 1784329588125
|
updatedAt: 1784329838645
|
||||||
---
|
---
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
id: "3f1a583a-320b-43c3-9993-52d249c9c430"
|
id: "3f1a583a-320b-43c3-9993-52d249c9c430"
|
||||||
number: 35
|
number: 35
|
||||||
title: "Proposer de emttre plsuieurs images pour un exercice"
|
title: "Proposer de mettre plsuieurs images pour un exercice"
|
||||||
status: "open"
|
status: "open"
|
||||||
priority: "low"
|
priority: "low"
|
||||||
sprint: null
|
sprint: null
|
||||||
@ -10,7 +10,7 @@ agentRefs: [{"agentId":"57695b92-24d0-4876-837c-76116e70a6ae","role":"assigned"}
|
|||||||
createdBy: {"kind":"user"}
|
createdBy: {"kind":"user"}
|
||||||
updatedBy: {"kind":"user"}
|
updatedBy: {"kind":"user"}
|
||||||
createdAt: 1784329532159
|
createdAt: 1784329532159
|
||||||
updatedAt: 1784329588125
|
updatedAt: 1784329838645
|
||||||
version: 3
|
version: 5
|
||||||
---
|
---
|
||||||
J'iamerais que lors de la création d'un exercice, ils oit possible d'associer jusqu'a 5 images
|
J'aimerais que lors de la création d'un exercice, ils oit possible d'associer jusqu'a 5 images
|
||||||
6
.ideai/tickets/37/carnet.md
Normal file
6
.ideai/tickets/37/carnet.md
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
issueRef: "#37"
|
||||||
|
version: 3
|
||||||
|
updatedBy: {"kind":"user"}
|
||||||
|
updatedAt: 1784330063749
|
||||||
|
---
|
||||||
16
.ideai/tickets/37/issue.md
Normal file
16
.ideai/tickets/37/issue.md
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
id: "d472ee06-f4f1-4796-9797-b5d8a65000f3"
|
||||||
|
number: 37
|
||||||
|
title: "Pouvoir supprimer un programme, une séance ou un exercice"
|
||||||
|
status: "open"
|
||||||
|
priority: "high"
|
||||||
|
sprint: null
|
||||||
|
links: []
|
||||||
|
agentRefs: [{"agentId":"57695b92-24d0-4876-837c-76116e70a6ae","role":"assigned"}]
|
||||||
|
createdBy: {"kind":"user"}
|
||||||
|
updatedBy: {"kind":"user"}
|
||||||
|
createdAt: 1784329898629
|
||||||
|
updatedAt: 1784330063749
|
||||||
|
version: 3
|
||||||
|
---
|
||||||
|
Il faut que l'utilisateur puisse supprimer un exercice qu'il a créé, un programme qu'il a créé ou une séance qu'il a créé. Si on supprime une exerccie, alors il est retiré des programmes dans lequel il se trouve. Si on supprime un programme alors il est supprimé des séances dans lequel il se trouve. Pour l'historique, il faut cependant que les datas soient persistantes, même si un exercice a été supprimé par exemple, il faut quand meme quie l'on puisse garder les données d'affichées. La séance ne pourra par contre pas être relancée, il faudra donc afficher un message qui l'explique.
|
||||||
6
.ideai/tickets/38/carnet.md
Normal file
6
.ideai/tickets/38/carnet.md
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
issueRef: "#38"
|
||||||
|
version: 3
|
||||||
|
updatedBy: {"kind":"user"}
|
||||||
|
updatedAt: 1784330204091
|
||||||
|
---
|
||||||
16
.ideai/tickets/38/issue.md
Normal file
16
.ideai/tickets/38/issue.md
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
id: "51e8769d-7b2b-4b4d-8de1-dc784881e575"
|
||||||
|
number: 38
|
||||||
|
title: "Proposer de mettre une image pour l'icone de l'exercice"
|
||||||
|
status: "open"
|
||||||
|
priority: "medium"
|
||||||
|
sprint: null
|
||||||
|
links: []
|
||||||
|
agentRefs: [{"agentId":"57695b92-24d0-4876-837c-76116e70a6ae","role":"assigned"}]
|
||||||
|
createdBy: {"kind":"user"}
|
||||||
|
updatedBy: {"kind":"user"}
|
||||||
|
createdAt: 1784330169746
|
||||||
|
updatedAt: 1784330204091
|
||||||
|
version: 3
|
||||||
|
---
|
||||||
|
J'aimerais qu'en plus des images que l'on peut ajouter, qu'il soit possible de mettre une image comme icone de l'exercice
|
||||||
@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"nextNumber": 37
|
"nextNumber": 39
|
||||||
}
|
}
|
||||||
@ -291,13 +291,13 @@
|
|||||||
"issueRef": "#26",
|
"issueRef": "#26",
|
||||||
"path": "26",
|
"path": "26",
|
||||||
"title": "[DevFrontend] Configuration du Score chrono (exercice + programme)",
|
"title": "[DevFrontend] Configuration du Score chrono (exercice + programme)",
|
||||||
"status": "open",
|
"status": "closed",
|
||||||
"priority": "high",
|
"priority": "high",
|
||||||
"sprint": null,
|
"sprint": null,
|
||||||
"assignedAgentIds": [
|
"assignedAgentIds": [
|
||||||
"9933c93a-b8a1-4164-a3bb-7063fdad747d"
|
"9933c93a-b8a1-4164-a3bb-7063fdad747d"
|
||||||
],
|
],
|
||||||
"updatedAt": 1784328255774
|
"updatedAt": 1784358479742
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"issueRef": "#27",
|
"issueRef": "#27",
|
||||||
@ -392,14 +392,14 @@
|
|||||||
{
|
{
|
||||||
"issueRef": "#35",
|
"issueRef": "#35",
|
||||||
"path": "35",
|
"path": "35",
|
||||||
"title": "Proposer de emttre plsuieurs images pour un exercice",
|
"title": "Proposer de mettre plsuieurs images pour un exercice",
|
||||||
"status": "open",
|
"status": "open",
|
||||||
"priority": "low",
|
"priority": "low",
|
||||||
"sprint": null,
|
"sprint": null,
|
||||||
"assignedAgentIds": [
|
"assignedAgentIds": [
|
||||||
"57695b92-24d0-4876-837c-76116e70a6ae"
|
"57695b92-24d0-4876-837c-76116e70a6ae"
|
||||||
],
|
],
|
||||||
"updatedAt": 1784329588125
|
"updatedAt": 1784329838645
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"issueRef": "#36",
|
"issueRef": "#36",
|
||||||
@ -412,6 +412,30 @@
|
|||||||
"57695b92-24d0-4876-837c-76116e70a6ae"
|
"57695b92-24d0-4876-837c-76116e70a6ae"
|
||||||
],
|
],
|
||||||
"updatedAt": 1784329810425
|
"updatedAt": 1784329810425
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"issueRef": "#37",
|
||||||
|
"path": "37",
|
||||||
|
"title": "Pouvoir supprimer un programme, une séance ou un exercice",
|
||||||
|
"status": "open",
|
||||||
|
"priority": "high",
|
||||||
|
"sprint": null,
|
||||||
|
"assignedAgentIds": [
|
||||||
|
"57695b92-24d0-4876-837c-76116e70a6ae"
|
||||||
|
],
|
||||||
|
"updatedAt": 1784330063749
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"issueRef": "#38",
|
||||||
|
"path": "38",
|
||||||
|
"title": "Proposer de mettre une image pour l'icone de l'exercice",
|
||||||
|
"status": "open",
|
||||||
|
"priority": "medium",
|
||||||
|
"sprint": null,
|
||||||
|
"assignedAgentIds": [
|
||||||
|
"57695b92-24d0-4876-837c-76116e70a6ae"
|
||||||
|
],
|
||||||
|
"updatedAt": 1784330204091
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -246,6 +246,7 @@ final class ExerciseListTile extends StatelessWidget {
|
|||||||
(measure) => MeasureBadge(
|
(measure) => MeasureBadge(
|
||||||
measure: measure,
|
measure: measure,
|
||||||
scoreUnit: exercise.scoreUnit,
|
scoreUnit: exercise.scoreUnit,
|
||||||
|
scoreInputMode: exercise.scoreInputMode,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (hasMedia)
|
if (hasMedia)
|
||||||
@ -293,6 +294,7 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
|||||||
var _hasTime = true;
|
var _hasTime = true;
|
||||||
var _hasReps = false;
|
var _hasReps = false;
|
||||||
var _hasScore = false;
|
var _hasScore = false;
|
||||||
|
var _scoreInputMode = ScoreInputMode.manual;
|
||||||
var _saving = false;
|
var _saving = false;
|
||||||
var _importingImage = false;
|
var _importingImage = false;
|
||||||
var _importingVideo = false;
|
var _importingVideo = false;
|
||||||
@ -314,6 +316,7 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
|||||||
_hasTime = exercise?.hasTimeMeasure ?? true;
|
_hasTime = exercise?.hasTimeMeasure ?? true;
|
||||||
_hasReps = exercise?.hasRepsMeasure ?? false;
|
_hasReps = exercise?.hasRepsMeasure ?? false;
|
||||||
_hasScore = exercise?.hasScoreMeasure ?? false;
|
_hasScore = exercise?.hasScoreMeasure ?? false;
|
||||||
|
_scoreInputMode = exercise?.scoreInputMode ?? ScoreInputMode.manual;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -408,27 +411,63 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
|||||||
),
|
),
|
||||||
if (_hasScore) ...[
|
if (_hasScore) ...[
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
TextFormField(
|
Text(
|
||||||
controller: _scoreLabelController,
|
'Mode de saisie',
|
||||||
decoration: const InputDecoration(labelText: 'Score à saisir'),
|
style: Theme.of(context).textTheme.titleSmall,
|
||||||
validator: (value) {
|
),
|
||||||
if (!_hasScore) return null;
|
RadioListTile<ScoreInputMode>(
|
||||||
return value == null || value.trim().isEmpty
|
contentPadding: EdgeInsets.zero,
|
||||||
? 'Le libellé du score est obligatoire.'
|
title: const Text('Saisie libre'),
|
||||||
: null;
|
value: ScoreInputMode.manual,
|
||||||
|
groupValue: _scoreInputMode,
|
||||||
|
onChanged: (value) {
|
||||||
|
if (value == null) return;
|
||||||
|
setState(() => _scoreInputMode = value);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
RadioListTile<ScoreInputMode>(
|
||||||
TextFormField(
|
contentPadding: EdgeInsets.zero,
|
||||||
controller: _scoreUnitController,
|
title: const Text('Chrono intégré'),
|
||||||
decoration: const InputDecoration(labelText: 'Unité'),
|
subtitle: const Text('Temps réalisé'),
|
||||||
validator: (value) {
|
value: ScoreInputMode.stopwatch,
|
||||||
if (!_hasScore) return null;
|
groupValue: _scoreInputMode,
|
||||||
return value == null || value.trim().isEmpty
|
onChanged: (value) {
|
||||||
? 'L’unité du score est obligatoire.'
|
if (value == null) return;
|
||||||
: null;
|
setState(() => _scoreInputMode = value);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
if (_scoreInputMode == ScoreInputMode.manual) ...[
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
TextFormField(
|
||||||
|
controller: _scoreLabelController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Score à saisir',
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
if (!_hasScore ||
|
||||||
|
_scoreInputMode != ScoreInputMode.manual) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return value == null || value.trim().isEmpty
|
||||||
|
? 'Le libellé du score est obligatoire.'
|
||||||
|
: null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
TextFormField(
|
||||||
|
controller: _scoreUnitController,
|
||||||
|
decoration: const InputDecoration(labelText: 'Unité'),
|
||||||
|
validator: (value) {
|
||||||
|
if (!_hasScore ||
|
||||||
|
_scoreInputMode != ScoreInputMode.manual) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return value == null || value.trim().isEmpty
|
||||||
|
? 'L’unité du score est obligatoire.'
|
||||||
|
: null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
if (_warning != null) ...[
|
if (_warning != null) ...[
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
@ -537,6 +576,16 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setState(() => _saving = true);
|
setState(() => _saving = true);
|
||||||
|
final scoreInputMode = _hasScore ? _scoreInputMode : ScoreInputMode.manual;
|
||||||
|
final scoreLabel = _hasScore
|
||||||
|
? switch (scoreInputMode) {
|
||||||
|
ScoreInputMode.manual => _scoreLabelController.text.trim(),
|
||||||
|
ScoreInputMode.stopwatch => 'Temps réalisé',
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
final scoreUnit = _hasScore && scoreInputMode == ScoreInputMode.manual
|
||||||
|
? _scoreUnitController.text.trim()
|
||||||
|
: null;
|
||||||
try {
|
try {
|
||||||
if (exercise == null) {
|
if (exercise == null) {
|
||||||
await widget.exerciseUseCases.create(
|
await widget.exerciseUseCases.create(
|
||||||
@ -547,8 +596,9 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
|||||||
hasTimeMeasure: _hasTime,
|
hasTimeMeasure: _hasTime,
|
||||||
hasRepsMeasure: _hasReps,
|
hasRepsMeasure: _hasReps,
|
||||||
hasScoreMeasure: _hasScore,
|
hasScoreMeasure: _hasScore,
|
||||||
scoreLabel: _hasScore ? _scoreLabelController.text.trim() : null,
|
scoreInputMode: scoreInputMode,
|
||||||
scoreUnit: _hasScore ? _scoreUnitController.text.trim() : null,
|
scoreLabel: scoreLabel,
|
||||||
|
scoreUnit: scoreUnit,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
await widget.exerciseUseCases.update(
|
await widget.exerciseUseCases.update(
|
||||||
@ -560,12 +610,16 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
|||||||
hasTimeMeasure: _hasTime,
|
hasTimeMeasure: _hasTime,
|
||||||
hasRepsMeasure: _hasReps,
|
hasRepsMeasure: _hasReps,
|
||||||
hasScoreMeasure: _hasScore,
|
hasScoreMeasure: _hasScore,
|
||||||
scoreLabel: _hasScore ? _scoreLabelController.text.trim() : null,
|
scoreInputMode: scoreInputMode,
|
||||||
scoreUnit: _hasScore ? _scoreUnitController.text.trim() : null,
|
scoreLabel: scoreLabel,
|
||||||
|
scoreUnit: scoreUnit,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
Navigator.of(context).pop(true);
|
final navigator = Navigator.of(context);
|
||||||
|
if (navigator.canPop()) {
|
||||||
|
navigator.pop(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} on Exception catch (error) {
|
} on Exception catch (error) {
|
||||||
_showSnackBar(error.toString());
|
_showSnackBar(error.toString());
|
||||||
@ -579,7 +633,8 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
|||||||
bool _measuresChanged(Exercise exercise) {
|
bool _measuresChanged(Exercise exercise) {
|
||||||
return exercise.hasTimeMeasure != _hasTime ||
|
return exercise.hasTimeMeasure != _hasTime ||
|
||||||
exercise.hasRepsMeasure != _hasReps ||
|
exercise.hasRepsMeasure != _hasReps ||
|
||||||
exercise.hasScoreMeasure != _hasScore;
|
exercise.hasScoreMeasure != _hasScore ||
|
||||||
|
exercise.scoreInputMode != _scoreInputMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
String? _optionalText(TextEditingController controller) {
|
String? _optionalText(TextEditingController controller) {
|
||||||
@ -601,20 +656,32 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final class MeasureBadge extends StatelessWidget {
|
final class MeasureBadge extends StatelessWidget {
|
||||||
const MeasureBadge({required this.measure, this.scoreUnit, super.key});
|
const MeasureBadge({
|
||||||
|
required this.measure,
|
||||||
|
this.scoreInputMode = ScoreInputMode.manual,
|
||||||
|
this.scoreUnit,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
final WorkoutMeasure measure;
|
final WorkoutMeasure measure;
|
||||||
|
final ScoreInputMode scoreInputMode;
|
||||||
final String? scoreUnit;
|
final String? scoreUnit;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final unit = scoreUnit?.trim();
|
final unit = scoreUnit?.trim();
|
||||||
final label =
|
var label = measure.label;
|
||||||
measure == WorkoutMeasure.score && unit != null && unit.isNotEmpty
|
if (measure == WorkoutMeasure.score &&
|
||||||
? '${measure.label} ($unit)'
|
scoreInputMode == ScoreInputMode.stopwatch) {
|
||||||
: measure.label;
|
label = 'Score chrono';
|
||||||
|
} else if (measure == WorkoutMeasure.score &&
|
||||||
|
unit != null &&
|
||||||
|
unit.isNotEmpty) {
|
||||||
|
label = '${measure.label} ($unit)';
|
||||||
|
}
|
||||||
return Semantics(
|
return Semantics(
|
||||||
label: label,
|
label: label,
|
||||||
|
excludeSemantics: true,
|
||||||
child: Chip(visualDensity: VisualDensity.compact, label: Text(label)),
|
child: Chip(visualDensity: VisualDensity.compact, label: Text(label)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -539,6 +539,13 @@ final class _ProgramExerciseCustomizationScreenState
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final scoreUnit = _draft.scoreUnitSnapshot;
|
final scoreUnit = _draft.scoreUnitSnapshot;
|
||||||
|
final hasStopwatchScore =
|
||||||
|
_draft.scoreInputModeSnapshot == ScoreInputMode.stopwatch;
|
||||||
|
final scoreTitle = hasStopwatchScore
|
||||||
|
? 'Score chrono'
|
||||||
|
: scoreUnit == null
|
||||||
|
? 'Score'
|
||||||
|
: 'Score ($scoreUnit)';
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: const Text("Personnaliser l'exercice")),
|
appBar: AppBar(title: const Text("Personnaliser l'exercice")),
|
||||||
bottomNavigationBar: SafeArea(
|
bottomNavigationBar: SafeArea(
|
||||||
@ -603,10 +610,19 @@ final class _ProgramExerciseCustomizationScreenState
|
|||||||
if (_draft.availableScoreSnapshot)
|
if (_draft.availableScoreSnapshot)
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
title: Text(scoreUnit == null ? 'Score' : 'Score ($scoreUnit)'),
|
title: Text(scoreTitle),
|
||||||
value: _draft.enabledMeasures.contains(WorkoutMeasure.score),
|
value: _draft.enabledMeasures.contains(WorkoutMeasure.score),
|
||||||
onChanged: (value) => _toggle(WorkoutMeasure.score, value),
|
onChanged: (value) => _toggle(WorkoutMeasure.score, value),
|
||||||
),
|
),
|
||||||
|
if (_draft.enabledMeasures.contains(WorkoutMeasure.time) &&
|
||||||
|
_draft.enabledMeasures.contains(WorkoutMeasure.score) &&
|
||||||
|
hasStopwatchScore) ...[
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
const Text(
|
||||||
|
"Temps sert d'objectif de durée ; Score chrono enregistre "
|
||||||
|
'le temps réalisé.',
|
||||||
|
),
|
||||||
|
],
|
||||||
if (_measureError != null)
|
if (_measureError != null)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 8),
|
padding: const EdgeInsets.only(top: 8),
|
||||||
@ -630,13 +646,31 @@ final class _ProgramExerciseCustomizationScreenState
|
|||||||
onChanged: (value) => _draft.targetReps = value?.round(),
|
onChanged: (value) => _draft.targetReps = value?.round(),
|
||||||
),
|
),
|
||||||
if (_draft.enabledMeasures.contains(WorkoutMeasure.score))
|
if (_draft.enabledMeasures.contains(WorkoutMeasure.score))
|
||||||
_NumberField(
|
if (hasStopwatchScore)
|
||||||
label: scoreUnit == null
|
_NumberField(
|
||||||
? 'Cible score'
|
label: 'Objectif de chrono',
|
||||||
: 'Cible score ($scoreUnit)',
|
helperText: 'Le résultat réel sera mesuré pendant la série.',
|
||||||
initialValue: _draft.targetScore,
|
initialValue: _millisecondsToSeconds(
|
||||||
onChanged: (value) => _draft.targetScore = value,
|
_draft.targetScoreTimeMs,
|
||||||
),
|
),
|
||||||
|
onChanged: (value) {
|
||||||
|
_draft.targetScore = null;
|
||||||
|
_draft.targetScoreTimeMs = value == null
|
||||||
|
? null
|
||||||
|
: (value * 1000).round();
|
||||||
|
},
|
||||||
|
)
|
||||||
|
else
|
||||||
|
_NumberField(
|
||||||
|
label: scoreUnit == null
|
||||||
|
? 'Cible score'
|
||||||
|
: 'Cible score ($scoreUnit)',
|
||||||
|
initialValue: _draft.targetScore,
|
||||||
|
onChanged: (value) {
|
||||||
|
_draft.targetScore = value;
|
||||||
|
_draft.targetScoreTimeMs = null;
|
||||||
|
},
|
||||||
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
Text('Repos', style: Theme.of(context).textTheme.titleMedium),
|
Text('Repos', style: Theme.of(context).textTheme.titleMedium),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
@ -662,6 +696,10 @@ final class _ProgramExerciseCustomizationScreenState
|
|||||||
_draft.enabledMeasures = {..._draft.enabledMeasures, measure};
|
_draft.enabledMeasures = {..._draft.enabledMeasures, measure};
|
||||||
} else {
|
} else {
|
||||||
_draft.enabledMeasures = {..._draft.enabledMeasures}..remove(measure);
|
_draft.enabledMeasures = {..._draft.enabledMeasures}..remove(measure);
|
||||||
|
if (measure == WorkoutMeasure.score) {
|
||||||
|
_draft.targetScore = null;
|
||||||
|
_draft.targetScoreTimeMs = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -731,11 +769,13 @@ final class _NumberField extends StatelessWidget {
|
|||||||
required this.label,
|
required this.label,
|
||||||
required this.initialValue,
|
required this.initialValue,
|
||||||
required this.onChanged,
|
required this.onChanged,
|
||||||
|
this.helperText,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String label;
|
final String label;
|
||||||
final num? initialValue;
|
final num? initialValue;
|
||||||
final ValueChanged<double?> onChanged;
|
final ValueChanged<double?> onChanged;
|
||||||
|
final String? helperText;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -743,7 +783,7 @@ final class _NumberField extends StatelessWidget {
|
|||||||
padding: const EdgeInsets.only(top: 8),
|
padding: const EdgeInsets.only(top: 8),
|
||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
initialValue: initialValue?.toString(),
|
initialValue: initialValue?.toString(),
|
||||||
decoration: InputDecoration(labelText: label),
|
decoration: InputDecoration(labelText: label, helperText: helperText),
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
onChanged: (value) => onChanged(double.tryParse(value)),
|
onChanged: (value) => onChanged(double.tryParse(value)),
|
||||||
),
|
),
|
||||||
@ -764,6 +804,7 @@ final class _ProgramExerciseDraft {
|
|||||||
required this.availableTimeSnapshot,
|
required this.availableTimeSnapshot,
|
||||||
required this.availableRepsSnapshot,
|
required this.availableRepsSnapshot,
|
||||||
required this.availableScoreSnapshot,
|
required this.availableScoreSnapshot,
|
||||||
|
this.scoreInputModeSnapshot = ScoreInputMode.manual,
|
||||||
this.scoreLabelSnapshot,
|
this.scoreLabelSnapshot,
|
||||||
this.scoreUnitSnapshot,
|
this.scoreUnitSnapshot,
|
||||||
required this.setsCount,
|
required this.setsCount,
|
||||||
@ -771,6 +812,7 @@ final class _ProgramExerciseDraft {
|
|||||||
this.targetTimeSeconds,
|
this.targetTimeSeconds,
|
||||||
this.targetReps,
|
this.targetReps,
|
||||||
this.targetScore,
|
this.targetScore,
|
||||||
|
this.targetScoreTimeMs,
|
||||||
this.restSecondsOverride,
|
this.restSecondsOverride,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -789,6 +831,7 @@ final class _ProgramExerciseDraft {
|
|||||||
availableTimeSnapshot: exercise.hasTimeMeasure,
|
availableTimeSnapshot: exercise.hasTimeMeasure,
|
||||||
availableRepsSnapshot: exercise.hasRepsMeasure,
|
availableRepsSnapshot: exercise.hasRepsMeasure,
|
||||||
availableScoreSnapshot: exercise.hasScoreMeasure,
|
availableScoreSnapshot: exercise.hasScoreMeasure,
|
||||||
|
scoreInputModeSnapshot: exercise.scoreInputMode,
|
||||||
scoreLabelSnapshot: exercise.scoreLabel,
|
scoreLabelSnapshot: exercise.scoreLabel,
|
||||||
scoreUnitSnapshot: exercise.scoreUnit,
|
scoreUnitSnapshot: exercise.scoreUnit,
|
||||||
setsCount: 3,
|
setsCount: 3,
|
||||||
@ -810,6 +853,7 @@ final class _ProgramExerciseDraft {
|
|||||||
availableTimeSnapshot: exercise.availableTimeSnapshot,
|
availableTimeSnapshot: exercise.availableTimeSnapshot,
|
||||||
availableRepsSnapshot: exercise.availableRepsSnapshot,
|
availableRepsSnapshot: exercise.availableRepsSnapshot,
|
||||||
availableScoreSnapshot: exercise.availableScoreSnapshot,
|
availableScoreSnapshot: exercise.availableScoreSnapshot,
|
||||||
|
scoreInputModeSnapshot: exercise.scoreInputModeSnapshot,
|
||||||
scoreLabelSnapshot: exercise.scoreLabelSnapshot,
|
scoreLabelSnapshot: exercise.scoreLabelSnapshot,
|
||||||
scoreUnitSnapshot: exercise.scoreUnitSnapshot,
|
scoreUnitSnapshot: exercise.scoreUnitSnapshot,
|
||||||
setsCount: exercise.setsCount,
|
setsCount: exercise.setsCount,
|
||||||
@ -821,6 +865,7 @@ final class _ProgramExerciseDraft {
|
|||||||
targetTimeSeconds: exercise.targetTimeSeconds,
|
targetTimeSeconds: exercise.targetTimeSeconds,
|
||||||
targetReps: exercise.targetReps,
|
targetReps: exercise.targetReps,
|
||||||
targetScore: exercise.targetScore,
|
targetScore: exercise.targetScore,
|
||||||
|
targetScoreTimeMs: exercise.targetScoreTimeMs,
|
||||||
restSecondsOverride: exercise.restSecondsOverride,
|
restSecondsOverride: exercise.restSecondsOverride,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -836,6 +881,7 @@ final class _ProgramExerciseDraft {
|
|||||||
final bool availableTimeSnapshot;
|
final bool availableTimeSnapshot;
|
||||||
final bool availableRepsSnapshot;
|
final bool availableRepsSnapshot;
|
||||||
final bool availableScoreSnapshot;
|
final bool availableScoreSnapshot;
|
||||||
|
final ScoreInputMode scoreInputModeSnapshot;
|
||||||
final String? scoreLabelSnapshot;
|
final String? scoreLabelSnapshot;
|
||||||
final String? scoreUnitSnapshot;
|
final String? scoreUnitSnapshot;
|
||||||
int setsCount;
|
int setsCount;
|
||||||
@ -843,6 +889,7 @@ final class _ProgramExerciseDraft {
|
|||||||
int? targetTimeSeconds;
|
int? targetTimeSeconds;
|
||||||
int? targetReps;
|
int? targetReps;
|
||||||
double? targetScore;
|
double? targetScore;
|
||||||
|
int? targetScoreTimeMs;
|
||||||
int? restSecondsOverride;
|
int? restSecondsOverride;
|
||||||
|
|
||||||
_ProgramExerciseDraft copy() {
|
_ProgramExerciseDraft copy() {
|
||||||
@ -858,6 +905,7 @@ final class _ProgramExerciseDraft {
|
|||||||
availableTimeSnapshot: availableTimeSnapshot,
|
availableTimeSnapshot: availableTimeSnapshot,
|
||||||
availableRepsSnapshot: availableRepsSnapshot,
|
availableRepsSnapshot: availableRepsSnapshot,
|
||||||
availableScoreSnapshot: availableScoreSnapshot,
|
availableScoreSnapshot: availableScoreSnapshot,
|
||||||
|
scoreInputModeSnapshot: scoreInputModeSnapshot,
|
||||||
scoreLabelSnapshot: scoreLabelSnapshot,
|
scoreLabelSnapshot: scoreLabelSnapshot,
|
||||||
scoreUnitSnapshot: scoreUnitSnapshot,
|
scoreUnitSnapshot: scoreUnitSnapshot,
|
||||||
setsCount: setsCount,
|
setsCount: setsCount,
|
||||||
@ -865,11 +913,14 @@ final class _ProgramExerciseDraft {
|
|||||||
targetTimeSeconds: targetTimeSeconds,
|
targetTimeSeconds: targetTimeSeconds,
|
||||||
targetReps: targetReps,
|
targetReps: targetReps,
|
||||||
targetScore: targetScore,
|
targetScore: targetScore,
|
||||||
|
targetScoreTimeMs: targetScoreTimeMs,
|
||||||
restSecondsOverride: restSecondsOverride,
|
restSecondsOverride: restSecondsOverride,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ProgramExerciseConfig toConfig() {
|
ProgramExerciseConfig toConfig() {
|
||||||
|
final scoreEnabled = enabledMeasures.contains(WorkoutMeasure.score);
|
||||||
|
final stopwatchScore = scoreInputModeSnapshot == ScoreInputMode.stopwatch;
|
||||||
return ProgramExerciseConfig(
|
return ProgramExerciseConfig(
|
||||||
existingMetadata: existingMetadata,
|
existingMetadata: existingMetadata,
|
||||||
sourceExerciseId: sourceExerciseId,
|
sourceExerciseId: sourceExerciseId,
|
||||||
@ -881,18 +932,26 @@ final class _ProgramExerciseDraft {
|
|||||||
availableTimeSnapshot: availableTimeSnapshot,
|
availableTimeSnapshot: availableTimeSnapshot,
|
||||||
availableRepsSnapshot: availableRepsSnapshot,
|
availableRepsSnapshot: availableRepsSnapshot,
|
||||||
availableScoreSnapshot: availableScoreSnapshot,
|
availableScoreSnapshot: availableScoreSnapshot,
|
||||||
|
scoreInputModeSnapshot: scoreInputModeSnapshot,
|
||||||
scoreLabelSnapshot: scoreLabelSnapshot,
|
scoreLabelSnapshot: scoreLabelSnapshot,
|
||||||
scoreUnitSnapshot: scoreUnitSnapshot,
|
scoreUnitSnapshot: scoreUnitSnapshot,
|
||||||
setsCount: setsCount,
|
setsCount: setsCount,
|
||||||
enabledMeasures: enabledMeasures,
|
enabledMeasures: enabledMeasures,
|
||||||
targetTimeSeconds: targetTimeSeconds,
|
targetTimeSeconds: targetTimeSeconds,
|
||||||
targetReps: targetReps,
|
targetReps: targetReps,
|
||||||
targetScore: targetScore,
|
targetScore: scoreEnabled && !stopwatchScore ? targetScore : null,
|
||||||
|
targetScoreTimeMs: scoreEnabled && stopwatchScore
|
||||||
|
? targetScoreTimeMs
|
||||||
|
: null,
|
||||||
restSecondsOverride: restSecondsOverride,
|
restSecondsOverride: restSecondsOverride,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
double? _millisecondsToSeconds(int? milliseconds) {
|
||||||
|
return milliseconds == null ? null : milliseconds / 1000;
|
||||||
|
}
|
||||||
|
|
||||||
String _programSummary(Program program) {
|
String _programSummary(Program program) {
|
||||||
final exerciseCount = program.exercises.length;
|
final exerciseCount = program.exercises.length;
|
||||||
final setsCount = program.exercises.fold<int>(
|
final setsCount = program.exercises.fold<int>(
|
||||||
@ -907,6 +966,10 @@ String _exerciseSummary(_ProgramExerciseDraft draft, int defaultRestSeconds) {
|
|||||||
final measures = draft.enabledMeasures
|
final measures = draft.enabledMeasures
|
||||||
.map((measure) {
|
.map((measure) {
|
||||||
final unit = draft.scoreUnitSnapshot?.trim();
|
final unit = draft.scoreUnitSnapshot?.trim();
|
||||||
|
if (measure == WorkoutMeasure.score &&
|
||||||
|
draft.scoreInputModeSnapshot == ScoreInputMode.stopwatch) {
|
||||||
|
return 'Score chrono';
|
||||||
|
}
|
||||||
if (measure == WorkoutMeasure.score &&
|
if (measure == WorkoutMeasure.score &&
|
||||||
unit != null &&
|
unit != null &&
|
||||||
unit.isNotEmpty) {
|
unit.isNotEmpty) {
|
||||||
|
|||||||
@ -66,6 +66,70 @@ void main() {
|
|||||||
expect(find.text('Score (kg)'), findsOneWidget);
|
expect(find.text('Score (kg)'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('basculer en chrono intégré masque l’unité et affiche le badge', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
final exerciseRepository = _FakeExerciseRepository();
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: ExerciseFormScreen(
|
||||||
|
exerciseUseCases: _exerciseUseCases(exerciseRepository),
|
||||||
|
mediaUseCases: _mediaUseCases(exerciseRepository),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.enterText(find.widgetWithText(TextFormField, 'Nom'), 'Run');
|
||||||
|
await tester.dragUntilVisible(
|
||||||
|
find.text('Score'),
|
||||||
|
find.byType(ListView),
|
||||||
|
const Offset(0, -200),
|
||||||
|
);
|
||||||
|
await tester.tap(find.widgetWithText(SwitchListTile, 'Score'));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
await tester.dragUntilVisible(
|
||||||
|
find.text('Unité'),
|
||||||
|
find.byType(ListView),
|
||||||
|
const Offset(0, -200),
|
||||||
|
);
|
||||||
|
expect(find.widgetWithText(TextFormField, 'Unité'), findsOneWidget);
|
||||||
|
|
||||||
|
await tester.dragUntilVisible(
|
||||||
|
find.text('Chrono intégré'),
|
||||||
|
find.byType(ListView),
|
||||||
|
const Offset(0, 200),
|
||||||
|
);
|
||||||
|
await tester.tap(find.text('Chrono intégré'));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
await tester.dragUntilVisible(
|
||||||
|
find.text('Temps réalisé'),
|
||||||
|
find.byType(ListView),
|
||||||
|
const Offset(0, -200),
|
||||||
|
);
|
||||||
|
expect(find.text('Temps réalisé'), findsOneWidget);
|
||||||
|
expect(find.widgetWithText(TextFormField, 'Unité'), findsNothing);
|
||||||
|
|
||||||
|
await tester.ensureVisible(find.text('Enregistrer'));
|
||||||
|
await tester.tap(find.text('Enregistrer'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
final saved = exerciseRepository.saved.single;
|
||||||
|
expect(saved.scoreInputMode, ScoreInputMode.stopwatch);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: Scaffold(body: ExerciseListTile(exercise: saved)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Score chrono'), findsOneWidget);
|
||||||
|
expect(find.text('Score'), findsNothing);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets(
|
testWidgets(
|
||||||
'sélectionner une image déclenche l’import et confirme le fichier',
|
'sélectionner une image déclenche l’import et confirme le fichier',
|
||||||
(tester) async {
|
(tester) async {
|
||||||
|
|||||||
@ -102,6 +102,73 @@ void main() {
|
|||||||
expect(find.widgetWithText(SwitchListTile, 'Score'), findsNothing);
|
expect(find.widgetWithText(SwitchListTile, 'Score'), findsNothing);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
testWidgets('affiche l’objectif de chrono pour un score en mode chrono', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
final exerciseRepository = _FakeExerciseRepository()
|
||||||
|
..exercises.add(
|
||||||
|
Exercise(
|
||||||
|
metadata: _metadata('exercise-1'),
|
||||||
|
name: 'Suicides',
|
||||||
|
hasTimeMeasure: true,
|
||||||
|
hasRepsMeasure: true,
|
||||||
|
hasScoreMeasure: true,
|
||||||
|
scoreInputMode: ScoreInputMode.stopwatch,
|
||||||
|
scoreLabel: 'Temps réalisé',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: ProgramFormScreen(
|
||||||
|
programUseCases: _programUseCases(
|
||||||
|
_FakeProgramRepository(),
|
||||||
|
exerciseRepository,
|
||||||
|
),
|
||||||
|
exerciseUseCases: _exerciseUseCases(exerciseRepository),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.tap(find.text('Ajouter un exercice'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.text('Suicides'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.byTooltip("Personnaliser l'exercice"));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Score chrono'), findsOneWidget);
|
||||||
|
await tester.dragUntilVisible(
|
||||||
|
find.text(
|
||||||
|
"Temps sert d'objectif de durée ; Score chrono enregistre "
|
||||||
|
'le temps réalisé.',
|
||||||
|
),
|
||||||
|
find.byType(ListView),
|
||||||
|
const Offset(0, -200),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
find.text(
|
||||||
|
"Temps sert d'objectif de durée ; Score chrono enregistre "
|
||||||
|
'le temps réalisé.',
|
||||||
|
),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
await tester.dragUntilVisible(
|
||||||
|
find.text('Objectif de chrono'),
|
||||||
|
find.byType(ListView),
|
||||||
|
const Offset(0, -200),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
find.widgetWithText(TextFormField, 'Objectif de chrono'),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
expect(find.widgetWithText(TextFormField, 'Cible score'), findsNothing);
|
||||||
|
expect(
|
||||||
|
find.text('Le résultat réel sera mesuré pendant la série.'),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ExerciseUseCases _exerciseUseCases(_FakeExerciseRepository repository) {
|
ExerciseUseCases _exerciseUseCases(_FakeExerciseRepository repository) {
|
||||||
|
|||||||
Reference in New Issue
Block a user