feat(exécution): chrono du score chronométré pendant la séance (ticket #27)

Ajoute le chrono du score chronométré (fondation domain/config des
tickets #25/#26) sur workout_execution_screen.dart, avec les use
cases associés. flutter analyze propre, 75/75 tests verts, build APK
debug validé.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-18 14:34:36 +02:00
parent b8ac8658f3
commit cdcb0beead
3 changed files with 609 additions and 4 deletions

View File

@ -1188,6 +1188,24 @@ final class ActiveWorkoutSessionUseCases {
);
}
Future<ActiveScoreStopwatchState?> findScoreStopwatch({
required String sessionId,
required int programIndex,
required int exerciseIndex,
required int setIndex,
}) {
return sessionRepository.findScoreStopwatchState(
sessionId: sessionId,
programIndex: programIndex,
exerciseIndex: exerciseIndex,
setIndex: setIndex,
);
}
int scoreStopwatchElapsedMilliseconds(ActiveScoreStopwatchState state) {
return state.elapsedMillisecondsAt(clock.now());
}
Future<List<SetResultPositionState>> listSetResults(String sessionId) async {
final session = await _requiredSession(sessionId);
final snapshots = _listSetSnapshots(session.resolvedTemplateSnapshotJson);

View File

@ -38,6 +38,8 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
Timer? _restTicker;
WorkoutHistory? _completedHistory;
String? _activeRestStateId;
ActiveScoreStopwatchState? _scoreStopwatch;
int? _manualScoreTimeMs;
late DateTime _seriesStartedAt;
var _reps = 0;
final _scoreController = TextEditingController();
@ -63,6 +65,7 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
_ticker = Timer.periodic(const Duration(seconds: 1), (_) {
if (mounted) setState(() {});
});
unawaited(_loadScoreStopwatch());
unawaited(_restoreActiveRest());
}
@ -151,6 +154,20 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
scoreController: _scoreController,
onRepsChanged: (value) => setState(() => _reps = value),
),
if (_exercise.stopwatchScoreEnabled) ...[
const SizedBox(height: 12),
_ScoreStopwatchInput(
elapsedLabel: _formatScoreStopwatch(
Duration(milliseconds: _scoreStopwatchElapsedMs),
),
status: _scoreStopwatch?.status,
onStart: _startScoreStopwatch,
onStop: _stopScoreStopwatch,
onResume: _resumeScoreStopwatch,
onReset: _resetScoreStopwatch,
onEdit: _editScoreStopwatchTime,
),
],
const SizedBox(height: 24),
FilledButton(
onPressed: () => _finishSet(skipped: false),
@ -326,6 +343,120 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
);
}
int get _scoreStopwatchElapsedMs {
final manual = _manualScoreTimeMs;
if (manual != null) {
return manual;
}
final state = _scoreStopwatch;
if (state == null) {
return 0;
}
return widget.activeUseCases.scoreStopwatchElapsedMilliseconds(state);
}
Future<void> _loadScoreStopwatch() async {
if (!_exercise.stopwatchScoreEnabled) {
_scoreStopwatch = null;
_manualScoreTimeMs = null;
return;
}
final state = await widget.activeUseCases.findScoreStopwatch(
sessionId: _session.metadata.id,
programIndex: _position.programIndex,
exerciseIndex: _position.exerciseIndex,
setIndex: _position.setIndex,
);
if (!mounted) return;
setState(() => _scoreStopwatch = state);
}
Future<void> _startScoreStopwatch() async {
final state = await widget.activeUseCases.startScoreStopwatch(
sessionId: _session.metadata.id,
programIndex: _position.programIndex,
exerciseIndex: _position.exerciseIndex,
setIndex: _position.setIndex,
);
if (!mounted) return;
setState(() {
_scoreStopwatch = state;
_manualScoreTimeMs = null;
});
}
Future<ActiveScoreStopwatchState?> _stopScoreStopwatch() async {
final state = _scoreStopwatch;
if (state == null) return null;
final stopped = await widget.activeUseCases.stopScoreStopwatch(
sessionId: _session.metadata.id,
programIndex: _position.programIndex,
exerciseIndex: _position.exerciseIndex,
setIndex: _position.setIndex,
);
if (!mounted) return stopped;
setState(() => _scoreStopwatch = stopped);
return stopped;
}
Future<void> _resumeScoreStopwatch() async {
final state = await widget.activeUseCases.resumeScoreStopwatch(
sessionId: _session.metadata.id,
programIndex: _position.programIndex,
exerciseIndex: _position.exerciseIndex,
setIndex: _position.setIndex,
);
if (!mounted) return;
setState(() {
_scoreStopwatch = state;
_manualScoreTimeMs = null;
});
}
Future<void> _resetScoreStopwatch() async {
if (_scoreStopwatchElapsedMs > 0) {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Réinitialiser le chrono ?'),
content: const Text('Le temps mesuré sera supprimé.'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Annuler'),
),
FilledButton(
onPressed: () => Navigator.of(context).pop(true),
child: const Text('Réinitialiser'),
),
],
),
);
if (confirmed != true) return;
}
await widget.activeUseCases.resetScoreStopwatch(
sessionId: _session.metadata.id,
programIndex: _position.programIndex,
exerciseIndex: _position.exerciseIndex,
setIndex: _position.setIndex,
);
if (!mounted) return;
setState(() {
_scoreStopwatch = null;
_manualScoreTimeMs = null;
});
}
Future<void> _editScoreStopwatchTime() async {
final initial = Duration(milliseconds: _scoreStopwatchElapsedMs);
final edited = await showDialog<int>(
context: context,
builder: (context) => _ScoreStopwatchEditDialog(initial: initial),
);
if (edited == null || !mounted) return;
setState(() => _manualScoreTimeMs = edited);
}
bool get _canPauseFromNavigation {
return _mode == WorkoutExecutionMode.active ||
_mode == WorkoutExecutionMode.rest;
@ -338,6 +469,9 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
Future<void> _pause() async {
_restTicker?.cancel();
if (_scoreStopwatch?.status == ActiveScoreStopwatchStatus.running) {
await _stopScoreStopwatch();
}
_session = await widget.activeUseCases.pause(_session.metadata.id);
if (!mounted) return;
setState(() => _mode = WorkoutExecutionMode.paused);
@ -348,6 +482,8 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
if (!mounted) return;
final restoredRest = await _restoreActiveRest();
if (!mounted || restoredRest) return;
await _loadScoreStopwatch();
if (!mounted) return;
setState(() => _mode = WorkoutExecutionMode.active);
}
@ -408,8 +544,88 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
}
Future<void> _finishSet({required bool skipped}) async {
if (skipped &&
_scoreStopwatch?.status == ActiveScoreStopwatchStatus.running) {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Passer cette série ?'),
content: const Text('Le chrono en cours sera ignoré.'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Continuer la série'),
),
FilledButton(
onPressed: () => Navigator.of(context).pop(true),
child: const Text('Passer'),
),
],
),
);
if (confirmed != true) return;
await widget.activeUseCases.resetScoreStopwatch(
sessionId: _session.metadata.id,
programIndex: _position.programIndex,
exerciseIndex: _position.exerciseIndex,
setIndex: _position.setIndex,
);
if (!mounted) return;
setState(() {
_scoreStopwatch = null;
_manualScoreTimeMs = null;
});
}
if (!skipped &&
_exercise.stopwatchScoreEnabled &&
_scoreStopwatch == null &&
_manualScoreTimeMs == null) {
final action = await showDialog<_MissingStopwatchAction>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Aucun temps chronométré'),
content: const Text("Tu n'as pas démarré le chrono score."),
actions: [
TextButton(
onPressed: () =>
Navigator.of(context).pop(_MissingStopwatchAction.start),
child: const Text('Démarrer le chrono'),
),
FilledButton(
onPressed: () => Navigator.of(
context,
).pop(_MissingStopwatchAction.finishWithout),
child: const Text('Terminer sans chrono'),
),
],
),
);
if (action == _MissingStopwatchAction.start) {
await _startScoreStopwatch();
return;
}
if (action != _MissingStopwatchAction.finishWithout) {
return;
}
}
await _recordAndAdvance(skipped: skipped);
}
Future<void> _recordAndAdvance({required bool skipped}) async {
try {
final score = double.tryParse(_scoreController.text.trim());
var actualScoreTimeMs = skipped ? null : _manualScoreTimeMs;
if (!skipped &&
_exercise.stopwatchScoreEnabled &&
actualScoreTimeMs == null) {
final state = _scoreStopwatch;
if (state?.status == ActiveScoreStopwatchStatus.running) {
final stopped = await _stopScoreStopwatch();
actualScoreTimeMs = stopped?.accumulatedMs;
} else {
actualScoreTimeMs = state?.accumulatedMs;
}
}
final actualTimeMs = DateTime.now()
.toUtc()
.difference(_seriesStartedAt)
@ -425,6 +641,7 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
actualScore: skipped
? null
: (_exercise.manualScoreEnabled ? score : null),
actualScoreTimeMs: actualScoreTimeMs,
scoreInputModeSnapshot: _exercise.scoreInputMode,
scoreLabelSnapshot: _exercise.scoreLabel,
scoreUnitSnapshot: _exercise.scoreUnit,
@ -548,6 +765,8 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
_scoreController.clear();
_reps = 0;
_activeRestStateId = null;
_scoreStopwatch = null;
_manualScoreTimeMs = null;
_seriesStartedAt = DateTime.now().toUtc();
_session = await widget.activeUseCases.updateProgress(
sessionId: _session.metadata.id,
@ -556,6 +775,8 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
setIndex: position.setIndex,
);
if (!mounted) return;
await _loadScoreStopwatch();
if (!mounted) return;
setState(() => _mode = WorkoutExecutionMode.active);
}
@ -676,7 +897,7 @@ final class SetMeasureInput extends StatelessWidget {
],
),
],
if (exercise.scoreEnabled) ...[
if (exercise.manualScoreEnabled) ...[
const SizedBox(height: 12),
TextField(
controller: scoreController,
@ -694,6 +915,176 @@ final class SetMeasureInput extends StatelessWidget {
}
}
enum _MissingStopwatchAction { start, finishWithout }
final class _ScoreStopwatchInput extends StatelessWidget {
const _ScoreStopwatchInput({
required this.elapsedLabel,
required this.status,
required this.onStart,
required this.onStop,
required this.onResume,
required this.onReset,
required this.onEdit,
});
final String elapsedLabel;
final ActiveScoreStopwatchStatus? status;
final VoidCallback onStart;
final VoidCallback onStop;
final VoidCallback onResume;
final VoidCallback onReset;
final VoidCallback onEdit;
@override
Widget build(BuildContext context) {
final running = status == ActiveScoreStopwatchStatus.running;
final stopped = status == ActiveScoreStopwatchStatus.stopped;
return CourtBlazerAccentPanel(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text('Chrono score', style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 12),
Center(
child: Text(
elapsedLabel,
style: AppTextStyles.scoreNumber(context),
),
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
alignment: WrapAlignment.center,
children: [
if (status == null)
FilledButton.icon(
onPressed: onStart,
icon: const Icon(Icons.play_arrow),
label: const Text('Démarrer'),
)
else if (running)
FilledButton.icon(
onPressed: onStop,
icon: const Icon(Icons.stop),
label: const Text('Arrêter'),
)
else ...[
FilledButton.icon(
onPressed: onResume,
icon: const Icon(Icons.play_arrow),
label: const Text('Reprendre'),
),
OutlinedButton.icon(
onPressed: onReset,
icon: const Icon(Icons.restart_alt),
label: const Text('Réinitialiser'),
),
],
if (stopped)
TextButton.icon(
onPressed: onEdit,
icon: const Icon(Icons.edit),
label: const Text('Modifier le temps'),
),
],
),
],
),
);
}
}
final class _ScoreStopwatchEditDialog extends StatefulWidget {
const _ScoreStopwatchEditDialog({required this.initial});
final Duration initial;
@override
State<_ScoreStopwatchEditDialog> createState() =>
_ScoreStopwatchEditDialogState();
}
final class _ScoreStopwatchEditDialogState
extends State<_ScoreStopwatchEditDialog> {
late final TextEditingController _minutesController;
late final TextEditingController _secondsController;
late final TextEditingController _tenthsController;
@override
void initState() {
super.initState();
final totalTenths = (widget.initial.inMilliseconds / 100).round();
final minutes = totalTenths ~/ 600;
final seconds = (totalTenths ~/ 10) % 60;
final tenths = totalTenths % 10;
_minutesController = TextEditingController(text: '$minutes');
_secondsController = TextEditingController(text: '$seconds');
_tenthsController = TextEditingController(text: '$tenths');
}
@override
void dispose() {
_minutesController.dispose();
_secondsController.dispose();
_tenthsController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('Modifier le temps'),
content: Row(
children: [
Expanded(
child: TextField(
controller: _minutesController,
decoration: const InputDecoration(labelText: 'Min'),
keyboardType: TextInputType.number,
),
),
const SizedBox(width: 8),
Expanded(
child: TextField(
controller: _secondsController,
decoration: const InputDecoration(labelText: 'S'),
keyboardType: TextInputType.number,
),
),
const SizedBox(width: 8),
Expanded(
child: TextField(
controller: _tenthsController,
decoration: const InputDecoration(labelText: 'Dixièmes'),
keyboardType: TextInputType.number,
),
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Annuler'),
),
FilledButton(
onPressed: () {
final minutes = int.tryParse(_minutesController.text.trim()) ?? 0;
final seconds = int.tryParse(_secondsController.text.trim()) ?? 0;
final tenths = int.tryParse(_tenthsController.text.trim()) ?? 0;
final milliseconds =
(minutes * 60 * 1000) + (seconds * 1000) + (tenths * 100);
Navigator.of(context).pop(milliseconds);
},
child: const Text('Enregistrer'),
),
],
);
}
}
final class _WorkoutPlanSheet extends StatefulWidget {
const _WorkoutPlanSheet({
required this.sessionId,
@ -1202,6 +1593,10 @@ final class _EditSetResultSheetState extends State<_EditSetResultSheet> {
if (actualScore != null) {
_scoreController.text = _formatScore(actualScore);
}
final actualScoreTimeMs = result?.actualScoreTimeMs;
if (actualScoreTimeMs != null) {
_scoreController.text = (actualScoreTimeMs / 1000).toStringAsFixed(1);
}
_remainingRestSeconds = _remainingSecondsForRest(widget.activeRest);
if (widget.activeRest != null && _remainingRestSeconds > 0) {
_timer = Timer.periodic(const Duration(seconds: 1), (_) {
@ -1287,7 +1682,9 @@ final class _EditSetResultSheetState extends State<_EditSetResultSheet> {
TextField(
controller: _scoreController,
decoration: InputDecoration(
labelText: widget.exercise.scoreUnit == null
labelText: widget.exercise.stopwatchScoreEnabled
? 'Temps réalisé (chrono score, s)'
: widget.exercise.scoreUnit == null
? 'Score'
: 'Score (${widget.exercise.scoreUnit})',
),
@ -1339,9 +1736,14 @@ final class _EditSetResultSheetState extends State<_EditSetResultSheet> {
actualReps: widget.exercise.repsEnabled
? int.tryParse(_repsController.text.trim())
: null,
actualScore: widget.exercise.scoreEnabled
actualScore: widget.exercise.manualScoreEnabled
? double.tryParse(_scoreController.text.trim())
: null,
actualScoreTimeMs: widget.exercise.stopwatchScoreEnabled
? _decimalSecondsToMilliseconds(_scoreController.text)
: null,
scoreInputModeSnapshot: widget.exercise.scoreInputMode,
scoreLabelSnapshot: widget.exercise.scoreLabel,
scoreUnitSnapshot: widget.exercise.scoreUnit,
);
await _finishSave();
@ -1472,6 +1874,12 @@ String _setResultSummary(ActiveSetResult result) {
final unit = result.scoreUnitSnapshot?.trim();
parts.add(unit == null || unit.isEmpty ? 'Score $value' : '$value $unit');
}
if (result.actualScoreTimeMs != null) {
final value = _formatScoreStopwatch(
Duration(milliseconds: result.actualScoreTimeMs!),
);
parts.add('Temps réalisé $value');
}
return parts.isEmpty ? 'Aucun résultat' : parts.join(' · ');
}
@ -1486,6 +1894,11 @@ int? _secondsToMilliseconds(String rawValue) {
return seconds == null ? null : seconds * 1000;
}
int? _decimalSecondsToMilliseconds(String rawValue) {
final seconds = double.tryParse(rawValue.trim());
return seconds == null ? null : (seconds * 1000).round();
}
int _remainingSecondsForRest(ActiveRestState? rest) {
if (rest == null) return 0;
final elapsedSeconds = DateTime.now()
@ -1695,6 +2108,10 @@ final class ExecutionExercise {
return scoreEnabled && scoreInputMode == ScoreInputMode.manual;
}
bool get stopwatchScoreEnabled {
return scoreEnabled && scoreInputMode == ScoreInputMode.stopwatch;
}
bool get hasMedia {
return imageMediaIds.isNotEmpty || videoMediaId != null;
}
@ -1810,3 +2227,11 @@ String _formatDuration(Duration duration) {
}
return '$minutes:$seconds';
}
String _formatScoreStopwatch(Duration duration) {
final totalTenths = (duration.inMilliseconds / 100).floor();
final minutes = (totalTenths ~/ 600).toString().padLeft(2, '0');
final seconds = ((totalTenths ~/ 10) % 60).toString().padLeft(2, '0');
final tenths = totalTenths % 10;
return '$minutes:$seconds.$tenths';
}

View File

@ -241,6 +241,160 @@ void main() {
expect(find.text('Image 2'), findsOneWidget);
});
testWidgets('le chrono score démarre et sarrête avec la bonne durée', (
tester,
) async {
await tester.binding.setSurfaceSize(const Size(400, 1400));
addTearDown(() => tester.binding.setSurfaceSize(null));
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
final repository = _FakeActiveSessionRepository();
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(
scoreInputMode: ScoreInputMode.stopwatch,
),
);
repository.session = session;
await tester.pumpWidget(
MaterialApp(
home: WorkoutExecutionScreen(
initialSession: session,
activeUseCases: _activeUseCases(repository, clock),
closeUseCase: _closeUseCase(repository, clock),
historyUseCases: _historyUseCases(clock),
workoutTemplateUseCases: _workoutTemplateUseCases(),
),
),
);
expect(find.text('Chrono score'), findsOneWidget);
expect(find.text('00:00.0'), findsOneWidget);
await tester.tap(find.text('Démarrer'));
await tester.pump();
clock.value = DateTime.utc(2026, 7, 17, 12, 0, 1, 200);
await tester.pump(const Duration(seconds: 1));
expect(find.text('00:01.2'), findsOneWidget);
await tester.tap(find.text('Arrêter'));
await tester.pump();
expect(find.text('00:01.2'), findsOneWidget);
expect(
repository.scoreStopwatchStates.single.status,
ActiveScoreStopwatchStatus.stopped,
);
});
testWidgets('terminer sans chrono démarré affiche une confirmation', (
tester,
) async {
await tester.binding.setSurfaceSize(const Size(400, 1400));
addTearDown(() => tester.binding.setSurfaceSize(null));
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
final repository = _FakeActiveSessionRepository();
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(
scoreInputMode: ScoreInputMode.stopwatch,
),
);
repository.session = session;
await tester.pumpWidget(
MaterialApp(
home: WorkoutExecutionScreen(
initialSession: session,
activeUseCases: _activeUseCases(repository, clock),
closeUseCase: _closeUseCase(repository, clock),
historyUseCases: _historyUseCases(clock),
workoutTemplateUseCases: _workoutTemplateUseCases(),
),
),
);
await tester.tap(find.text('Terminer la série'));
await tester.pumpAndSettle();
expect(find.text('Aucun temps chronométré'), findsOneWidget);
expect(find.text("Tu n'as pas démarré le chrono score."), findsOneWidget);
expect(find.text('Démarrer le chrono'), findsOneWidget);
expect(find.text('Terminer sans chrono'), findsOneWidget);
expect(repository.results, isEmpty);
});
testWidgets('terminer avec chrono en cours larrête et enregistre', (
tester,
) async {
await tester.binding.setSurfaceSize(const Size(400, 1400));
addTearDown(() => tester.binding.setSurfaceSize(null));
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
final repository = _FakeActiveSessionRepository();
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(
setsCount: 2,
scoreInputMode: ScoreInputMode.stopwatch,
),
);
repository.session = session;
await tester.pumpWidget(
MaterialApp(
home: WorkoutExecutionScreen(
initialSession: session,
activeUseCases: _activeUseCases(repository, clock),
closeUseCase: _closeUseCase(repository, clock),
historyUseCases: _historyUseCases(clock),
workoutTemplateUseCases: _workoutTemplateUseCases(),
),
),
);
await tester.tap(find.text('Démarrer'));
await tester.pump();
clock.value = DateTime.utc(2026, 7, 17, 12, 0, 2, 500);
await tester.pump(const Duration(seconds: 1));
await tester.tap(find.text('Terminer la série'));
await tester.pumpAndSettle();
expect(repository.results.single.actualScoreTimeMs, 2500);
expect(
repository.scoreStopwatchStates.single.status,
ActiveScoreStopwatchStatus.stopped,
);
expect(repository.session?.currentSetIndex, 1);
});
testWidgets('un repos actif est repris avec le temps restant recalculé', (
tester,
) async {
@ -850,7 +1004,15 @@ final class _FakeActiveSessionRepository implements ActiveSessionRepository {
required int exerciseIndex,
required int setIndex,
required DateTime deletedAt,
}) async {}
}) async {
scoreStopwatchStates.removeWhere(
(state) =>
state.activeWorkoutSessionId == sessionId &&
state.programIndex == programIndex &&
state.exerciseIndex == exerciseIndex &&
state.setIndex == setIndex,
);
}
@override
Future<void> saveSetResult(ActiveSetResult result) async {