merge(main): score par défaut à 0 et affichage des répétitions en séance (tickets #44, #45)

Fusionne feature/bugsprint-score-zero-reps-display — analyze propre
(mêmes 8 infos préexistantes), 79/79 tests verts, build APK debug
validé.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-18 22:36:03 +02:00
13 changed files with 106 additions and 37 deletions

View File

@ -1,6 +1,6 @@
--- ---
issueRef: "#41" issueRef: "#41"
version: 5 version: 6
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"} updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
updatedAt: 1784379589805 updatedAt: 1784406566969
--- ---

View File

@ -2,7 +2,7 @@
id: "d92c2af3-1981-4782-9aba-460a45b37d56" id: "d92c2af3-1981-4782-9aba-460a45b37d56"
number: 41 number: 41
title: "[bug] images non affichée dans le caroussel" title: "[bug] images non affichée dans le caroussel"
status: "inProgress" status: "qa"
priority: "medium" priority: "medium"
sprint: "abc4f969-b169-45f7-988c-daeeab762201" sprint: "abc4f969-b169-45f7-988c-daeeab762201"
links: [] links: []
@ -10,7 +10,7 @@ agentRefs: [{"agentId":"57695b92-24d0-4876-837c-76116e70a6ae","role":"assigned"}
createdBy: {"kind":"user"} createdBy: {"kind":"user"}
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"} updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
createdAt: 1784378925702 createdAt: 1784378925702
updatedAt: 1784379589805 updatedAt: 1784406566969
version: 5 version: 6
--- ---
Dans le carroussel des exercices dans les séances, les images ne s'affichent pas vraiment, je n'ai qu'une icone avec le titre image 1, image 2 etc... Dans le carroussel des exercices dans les séances, les images ne s'affichent pas vraiment, je n'ai qu'une icone avec le titre image 1, image 2 etc...

View File

@ -1,6 +1,6 @@
--- ---
issueRef: "#42" issueRef: "#42"
version: 5 version: 6
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"} updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
updatedAt: 1784379590525 updatedAt: 1784406567477
--- ---

View File

@ -2,7 +2,7 @@
id: "a788671c-fbc4-4c3d-855e-bb44c120ab54" id: "a788671c-fbc4-4c3d-855e-bb44c120ab54"
number: 42 number: 42
title: "[bug] vidéo non affichée dans le caroussel" title: "[bug] vidéo non affichée dans le caroussel"
status: "inProgress" status: "qa"
priority: "medium" priority: "medium"
sprint: "abc4f969-b169-45f7-988c-daeeab762201" sprint: "abc4f969-b169-45f7-988c-daeeab762201"
links: [] links: []
@ -10,7 +10,7 @@ agentRefs: [{"agentId":"57695b92-24d0-4876-837c-76116e70a6ae","role":"assigned"}
createdBy: {"kind":"user"} createdBy: {"kind":"user"}
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"} updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
createdAt: 1784378998844 createdAt: 1784378998844
updatedAt: 1784379590525 updatedAt: 1784406567477
version: 5 version: 6
--- ---
Dans les exerccies des séances, je ne peux pas lancer la vidéo. Je n'ai qu'un icone Play avec marqué Vidéo mais rien ne se passe si je selectionne Dans les exerccies des séances, je ne peux pas lancer la vidéo. Je n'ai qu'un icone Play avec marqué Vidéo mais rien ne se passe si je selectionne

View File

@ -1,6 +1,6 @@
--- ---
issueRef: "#43" issueRef: "#43"
version: 5 version: 6
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"} updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
updatedAt: 1784379591252 updatedAt: 1784406567925
--- ---

View File

@ -2,7 +2,7 @@
id: "33546028-43ab-467c-90af-f2ff3aeb920f" id: "33546028-43ab-467c-90af-f2ff3aeb920f"
number: 43 number: 43
title: "[Bug] chrono qui n'affiche que les secondes" title: "[Bug] chrono qui n'affiche que les secondes"
status: "inProgress" status: "qa"
priority: "medium" priority: "medium"
sprint: "abc4f969-b169-45f7-988c-daeeab762201" sprint: "abc4f969-b169-45f7-988c-daeeab762201"
links: [] links: []
@ -10,7 +10,7 @@ agentRefs: [{"agentId":"57695b92-24d0-4876-837c-76116e70a6ae","role":"assigned"}
createdBy: {"kind":"user"} createdBy: {"kind":"user"}
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"} updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
createdAt: 1784379078126 createdAt: 1784379078126
updatedAt: 1784379591252 updatedAt: 1784406567925
version: 5 version: 6
--- ---
Pour le chrono score, quand je le lance j'ai d'abord 0.9 sec qui s'affiche, puis uniquement les secondes qui s'incrémentent Pour le chrono score, quand je le lance j'ai d'abord 0.9 sec qui s'affiche, puis uniquement les secondes qui s'incrémentent

View File

@ -465,37 +465,37 @@
"issueRef": "#41", "issueRef": "#41",
"path": "41", "path": "41",
"title": "[bug] images non affichée dans le caroussel", "title": "[bug] images non affichée dans le caroussel",
"status": "inProgress", "status": "qa",
"priority": "medium", "priority": "medium",
"sprint": "abc4f969-b169-45f7-988c-daeeab762201", "sprint": "abc4f969-b169-45f7-988c-daeeab762201",
"assignedAgentIds": [ "assignedAgentIds": [
"57695b92-24d0-4876-837c-76116e70a6ae" "57695b92-24d0-4876-837c-76116e70a6ae"
], ],
"updatedAt": 1784379589805 "updatedAt": 1784406566969
}, },
{ {
"issueRef": "#42", "issueRef": "#42",
"path": "42", "path": "42",
"title": "[bug] vidéo non affichée dans le caroussel", "title": "[bug] vidéo non affichée dans le caroussel",
"status": "inProgress", "status": "qa",
"priority": "medium", "priority": "medium",
"sprint": "abc4f969-b169-45f7-988c-daeeab762201", "sprint": "abc4f969-b169-45f7-988c-daeeab762201",
"assignedAgentIds": [ "assignedAgentIds": [
"57695b92-24d0-4876-837c-76116e70a6ae" "57695b92-24d0-4876-837c-76116e70a6ae"
], ],
"updatedAt": 1784379590525 "updatedAt": 1784406567477
}, },
{ {
"issueRef": "#43", "issueRef": "#43",
"path": "43", "path": "43",
"title": "[Bug] chrono qui n'affiche que les secondes", "title": "[Bug] chrono qui n'affiche que les secondes",
"status": "inProgress", "status": "qa",
"priority": "medium", "priority": "medium",
"sprint": "abc4f969-b169-45f7-988c-daeeab762201", "sprint": "abc4f969-b169-45f7-988c-daeeab762201",
"assignedAgentIds": [ "assignedAgentIds": [
"57695b92-24d0-4876-837c-76116e70a6ae" "57695b92-24d0-4876-837c-76116e70a6ae"
], ],
"updatedAt": 1784379591252 "updatedAt": 1784406567925
}, },
{ {
"issueRef": "#44", "issueRef": "#44",

View File

@ -1646,8 +1646,8 @@ void _validateExerciseDefaultTargets({
} }
if (hasScoreMeasure && if (hasScoreMeasure &&
scoreInputMode == ScoreInputMode.manual && scoreInputMode == ScoreInputMode.manual &&
(defaultTargetScore == null || defaultTargetScore <= 0)) { (defaultTargetScore == null || defaultTargetScore < 0)) {
throw const DomainException('Default target score must be positive.'); throw const DomainException('Default target score must not be negative.');
} }
if (defaultTargetScore != null && if (defaultTargetScore != null &&
(!hasScoreMeasure || scoreInputMode != ScoreInputMode.manual)) { (!hasScoreMeasure || scoreInputMode != ScoreInputMode.manual)) {

View File

@ -167,7 +167,10 @@ final class Exercise {
'Default target time seconds', 'Default target time seconds',
); );
_requireNullablePositive(defaultTargetReps, 'Default target reps'); _requireNullablePositive(defaultTargetReps, 'Default target reps');
_requireNullablePositiveDouble(defaultTargetScore, 'Default target score'); _requireNullableNonNegativeDouble(
defaultTargetScore,
'Default target score',
);
_requireNullablePositive( _requireNullablePositive(
defaultTargetScoreTimeMs, defaultTargetScoreTimeMs,
'Default target score time ms', 'Default target score time ms',
@ -1035,12 +1038,6 @@ void _requireNullableNonNegativeDouble(double? value, String label) {
} }
} }
void _requireNullablePositiveDouble(double? value, String label) {
if (value != null && value <= 0) {
throw DomainException('$label must be positive.');
}
}
void _requireScoreTargetShape({ void _requireScoreTargetShape({
required bool scoreEnabled, required bool scoreEnabled,
required ScoreInputMode scoreInputMode, required ScoreInputMode scoreInputMode,

View File

@ -635,9 +635,9 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
_scoreInputMode != ScoreInputMode.manual) { _scoreInputMode != ScoreInputMode.manual) {
return null; return null;
} }
return _positiveDoubleValidator( return _nonNegativeDoubleValidator(
value, value,
'Saisis un score supérieur à 0.', 'Saisis un score positif ou nul.',
); );
}, },
), ),
@ -914,6 +914,14 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
return null; return null;
} }
String? _nonNegativeDoubleValidator(String? value, String message) {
final number = double.tryParse(value?.trim() ?? '');
if (number == null || number < 0) {
return message;
}
return null;
}
String _optionalIntText(int? value) { String _optionalIntText(int? value) {
return value == null ? '' : '$value'; return value == null ? '' : '$value';
} }

View File

@ -72,6 +72,7 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
_mode = _session.status == ActiveWorkoutStatus.running _mode = _session.status == ActiveWorkoutStatus.running
? WorkoutExecutionMode.active ? WorkoutExecutionMode.active
: WorkoutExecutionMode.paused; : WorkoutExecutionMode.paused;
_reps = _initialRepsFor(_exercise);
_seriesStartedAt = DateTime.now().toUtc(); _seriesStartedAt = DateTime.now().toUtc();
_ticker = Timer.periodic(const Duration(seconds: 1), (_) { _ticker = Timer.periodic(const Duration(seconds: 1), (_) {
if (mounted) setState(() {}); if (mounted) setState(() {});
@ -808,7 +809,7 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
Future<void> _moveTo(ExecutionPosition position) async { Future<void> _moveTo(ExecutionPosition position) async {
_scoreController.clear(); _scoreController.clear();
_reps = 0; _reps = _initialRepsFor(_plan.exerciseAt(position));
_activeRestStateId = null; _activeRestStateId = null;
_scoreStopwatch = null; _scoreStopwatch = null;
_manualScoreTimeMs = null; _manualScoreTimeMs = null;
@ -889,6 +890,11 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
), ),
); );
} }
int _initialRepsFor(ExecutionExercise exercise) {
if (!exercise.repsEnabled) return 0;
return exercise.targetReps ?? 0;
}
} }
final class SetMeasureInput extends StatelessWidget { final class SetMeasureInput extends StatelessWidget {

View File

@ -55,7 +55,7 @@ void main() {
}, },
); );
test('Exercise use case rejects non-positive default targets', () async { test('Exercise use case rejects invalid default targets', () async {
final useCase = _exerciseUseCase(_FakeExerciseRepository()); final useCase = _exerciseUseCase(_FakeExerciseRepository());
await expectLater( await expectLater(
@ -86,12 +86,33 @@ void main() {
hasScoreMeasure: true, hasScoreMeasure: true,
scoreLabel: 'Charge', scoreLabel: 'Charge',
scoreUnit: 'kg', scoreUnit: 'kg',
defaultTargetScore: 0, defaultTargetScore: -1,
), ),
throwsA(isA<DomainException>()), throwsA(isA<DomainException>()),
); );
}); });
test(
'Exercise use case accepts a zero manual score default target',
() async {
final repository = _FakeExerciseRepository();
final useCase = _exerciseUseCase(repository);
final exercise = await useCase.create(
name: 'Charge',
hasTimeMeasure: false,
hasRepsMeasure: false,
hasScoreMeasure: true,
scoreLabel: 'Charge',
scoreUnit: 'kg',
defaultTargetScore: 0,
);
expect(exercise.defaultTargetScore, 0);
expect(repository.saved.single.defaultTargetScore, 0);
},
);
test( test(
'Exercise use case accepts stopwatch score without default target', 'Exercise use case accepts stopwatch score without default target',
() async { () async {

View File

@ -43,6 +43,42 @@ void main() {
expect(find.text('00:45'), findsOneWidget); expect(find.text('00:45'), findsOneWidget);
}); });
testWidgets('les répétitions sont initialisées avec la cible', (
tester,
) async {
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
final session = ActiveWorkoutSession(
metadata: _metadata('session-1'),
sourceWorkoutTemplateId: 'template-1',
status: ActiveWorkoutStatus.running,
startedAt: DateTime.utc(2026, 7, 17, 12),
lastPersistedAt: DateTime.utc(2026, 7, 17, 12),
elapsedActiveMs: 0,
currentProgramIndex: 0,
currentExerciseIndex: 0,
currentSetIndex: 0,
resolvedTemplateSnapshotJson: _sessionSnapshot(targetReps: 8),
);
await tester.pumpWidget(
MaterialApp(
home: WorkoutExecutionScreen(
initialSession: session,
activeUseCases: _activeUseCases(
_FakeActiveSessionRepository(),
clock,
),
closeUseCase: _closeUseCase(_FakeActiveSessionRepository(), clock),
historyUseCases: _historyUseCases(clock),
workoutTemplateUseCases: _workoutTemplateUseCases(),
),
),
);
expect(find.text('Répétitions'), findsOneWidget);
expect(find.text('8'), findsOneWidget);
});
testWidgets('les mesures cumulées respectent la hiérarchie visuelle', ( testWidgets('les mesures cumulées respectent la hiérarchie visuelle', (
tester, tester,
) async { ) async {
@ -618,8 +654,8 @@ void main() {
expect(find.text('2 / 3'), findsOneWidget); expect(find.text('2 / 3'), findsOneWidget);
expect(find.text('30 s'), findsOneWidget); expect(find.text('30 s'), findsOneWidget);
for (var i = 0; i < 3; i++) { for (var i = 0; i < 2; i++) {
await tester.tap(find.byIcon(Icons.add)); await tester.tap(find.byIcon(Icons.remove));
await tester.pump(); await tester.pump();
} }
await tester.enterText( await tester.enterText(
@ -637,6 +673,7 @@ void main() {
expect(repository.session?.currentSetIndex, 2); expect(repository.session?.currentSetIndex, 2);
expect(find.text('Programme 1/1 · Exercice 1/1'), findsOneWidget); expect(find.text('Programme 1/1 · Exercice 1/1'), findsOneWidget);
expect(find.text('3 / 3'), findsOneWidget); expect(find.text('3 / 3'), findsOneWidget);
expect(find.text('5'), findsOneWidget);
}, },
); );