merge(main): résolution effective et réglages auto-enchaînement des chronos d'étapes (tickets #76, #77)
Fusionne feature/step-chaining-76-resolution — pub get OK, dart format appliqué (7 fichiers reformatés), analyze propre (mêmes infos préexistantes), 152/152 tests verts, build APK debug validé. Propagation du champ de _ProgramExerciseDraft vérifiée exhaustivement sur ses 5 points d'impact (pas de répétition du bug #72). Tickets #76 (DevBackend) et #77 (DevFrontend) développés dans le même worktree partagé, commités séparément (fichiers disjoints) ; feature/step-chaining-77-ui-settings restée vide, jamais divergée. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
issueRef: "#75"
|
issueRef: "#75"
|
||||||
version: 1
|
version: 2
|
||||||
updatedBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"}
|
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
updatedAt: 1784547220555
|
updatedAt: 1784547809520
|
||||||
---
|
---
|
||||||
|
|||||||
@ -2,15 +2,15 @@
|
|||||||
id: "0197daa2-674e-4b03-9142-d0180874ffc4"
|
id: "0197daa2-674e-4b03-9142-d0180874ffc4"
|
||||||
number: 75
|
number: 75
|
||||||
title: "[DevBackend] Modèle et migration pour auto-enchaînement des chronos d'étapes"
|
title: "[DevBackend] Modèle et migration pour auto-enchaînement des chronos d'étapes"
|
||||||
status: "open"
|
status: "qa"
|
||||||
priority: "medium"
|
priority: "medium"
|
||||||
sprint: null
|
sprint: null
|
||||||
links: [{"target":"#73","kind":"relatesTo"}]
|
links: [{"target":"#73","kind":"relatesTo"}]
|
||||||
agentRefs: [{"agentId":"10ee045b-1c41-479e-ba03-dceed9edd495","role":"assigned"}]
|
agentRefs: [{"agentId":"10ee045b-1c41-479e-ba03-dceed9edd495","role":"assigned"}]
|
||||||
createdBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"}
|
createdBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"}
|
||||||
updatedBy: {"kind":"agent","agent_id":"f8f40941-ecf7-4830-b9de-8818a099f448"}
|
updatedBy: {"kind":"agent","agent_id":"57695b92-24d0-4876-837c-76116e70a6ae"}
|
||||||
createdAt: 1784547220555
|
createdAt: 1784547220555
|
||||||
updatedAt: 1784547220555
|
updatedAt: 1784547809520
|
||||||
version: 1
|
version: 2
|
||||||
---
|
---
|
||||||
Ajouter le réglage `autoStartNextTimedStep` côté modèle. Sur `Exercise` : bool non-null par défaut true. Sur `ProgramExercise` : `autoStartNextTimedStepSnapshot` non-null + `autoStartNextTimedStepOverride` nullable pour revenir au réglage exercice snapshoté. Sur `WorkoutTemplateExerciseOverride` : `autoStartNextTimedStepOverride` nullable, décision consciente d'élargir l'override séance à un comportement booléen non structurel. Propager dans `ProgramExerciseConfig`, `_ProgramExerciseDraft`, snapshots JSON et mappers Drift. Migration Drift schemaVersion 13 -> 14 avec defaults true et colonnes nullable d'override.
|
Ajouter le réglage `autoStartNextTimedStep` côté modèle. Sur `Exercise` : bool non-null par défaut true. Sur `ProgramExercise` : `autoStartNextTimedStepSnapshot` non-null + `autoStartNextTimedStepOverride` nullable pour revenir au réglage exercice snapshoté. Sur `WorkoutTemplateExerciseOverride` : `autoStartNextTimedStepOverride` nullable, décision consciente d'élargir l'override séance à un comportement booléen non structurel. Propager dans `ProgramExerciseConfig`, `_ProgramExerciseDraft`, snapshots JSON et mappers Drift. Migration Drift schemaVersion 13 -> 14 avec defaults true et colonnes nullable d'override.
|
||||||
@ -847,13 +847,13 @@
|
|||||||
"issueRef": "#75",
|
"issueRef": "#75",
|
||||||
"path": "75",
|
"path": "75",
|
||||||
"title": "[DevBackend] Modèle et migration pour auto-enchaînement des chronos d'étapes",
|
"title": "[DevBackend] Modèle et migration pour auto-enchaînement des chronos d'étapes",
|
||||||
"status": "open",
|
"status": "qa",
|
||||||
"priority": "medium",
|
"priority": "medium",
|
||||||
"sprint": null,
|
"sprint": null,
|
||||||
"assignedAgentIds": [
|
"assignedAgentIds": [
|
||||||
"10ee045b-1c41-479e-ba03-dceed9edd495"
|
"10ee045b-1c41-479e-ba03-dceed9edd495"
|
||||||
],
|
],
|
||||||
"updatedAt": 1784547220555
|
"updatedAt": 1784547809520
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"issueRef": "#76",
|
"issueRef": "#76",
|
||||||
|
|||||||
@ -1214,10 +1214,8 @@ final class ProgramUseCases {
|
|||||||
exerciseImageMediaIdsSnapshot: input.exerciseImageMediaIdsSnapshot,
|
exerciseImageMediaIdsSnapshot: input.exerciseImageMediaIdsSnapshot,
|
||||||
exerciseVideoMediaIdSnapshot: input.exerciseVideoMediaIdSnapshot,
|
exerciseVideoMediaIdSnapshot: input.exerciseVideoMediaIdSnapshot,
|
||||||
exerciseStepsSnapshot: input.exerciseStepsSnapshot,
|
exerciseStepsSnapshot: input.exerciseStepsSnapshot,
|
||||||
autoStartNextTimedStepSnapshot:
|
autoStartNextTimedStepSnapshot: input.autoStartNextTimedStepSnapshot,
|
||||||
input.autoStartNextTimedStepSnapshot,
|
autoStartNextTimedStepOverride: input.autoStartNextTimedStepOverride,
|
||||||
autoStartNextTimedStepOverride:
|
|
||||||
input.autoStartNextTimedStepOverride,
|
|
||||||
exerciseArchivedSnapshot: input.exerciseArchivedSnapshot,
|
exerciseArchivedSnapshot: input.exerciseArchivedSnapshot,
|
||||||
availableTimeSnapshot: input.availableTimeSnapshot,
|
availableTimeSnapshot: input.availableTimeSnapshot,
|
||||||
availableRepsSnapshot: input.availableRepsSnapshot,
|
availableRepsSnapshot: input.availableRepsSnapshot,
|
||||||
@ -1478,8 +1476,7 @@ final class WorkoutTemplateUseCases {
|
|||||||
targetRepsOverride: input.targetRepsOverride,
|
targetRepsOverride: input.targetRepsOverride,
|
||||||
targetScoreOverride: input.targetScoreOverride,
|
targetScoreOverride: input.targetScoreOverride,
|
||||||
targetScoreTimeMsOverride: input.targetScoreTimeMsOverride,
|
targetScoreTimeMsOverride: input.targetScoreTimeMsOverride,
|
||||||
autoStartNextTimedStepOverride:
|
autoStartNextTimedStepOverride: input.autoStartNextTimedStepOverride,
|
||||||
input.autoStartNextTimedStepOverride,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -2387,16 +2384,43 @@ final class ActiveExerciseStepUseCases {
|
|||||||
setIndex: setIndex,
|
setIndex: setIndex,
|
||||||
);
|
);
|
||||||
final now = clock.now();
|
final now = clock.now();
|
||||||
final state = await _autoAdvanceElapsedTimers(
|
final currentState = await _requiredStepProgressState(
|
||||||
context,
|
sessionId: sessionId,
|
||||||
await _requiredStepProgressState(
|
programIndex: programIndex,
|
||||||
sessionId: sessionId,
|
exerciseIndex: exerciseIndex,
|
||||||
programIndex: programIndex,
|
setIndex: setIndex,
|
||||||
exerciseIndex: exerciseIndex,
|
|
||||||
setIndex: setIndex,
|
|
||||||
),
|
|
||||||
now,
|
|
||||||
);
|
);
|
||||||
|
final currentStep = _currentStep(context, currentState);
|
||||||
|
if (currentState.status == ActiveExerciseStepProgressStatus.runningTimer &&
|
||||||
|
currentStep.type == ExerciseStepType.time) {
|
||||||
|
final targetMs = currentStep.defaultTargetValue * 1000;
|
||||||
|
final elapsedMs = currentState.elapsedMillisecondsAt(now);
|
||||||
|
if (elapsedMs >= targetMs) {
|
||||||
|
final completed = _stepResult(
|
||||||
|
context: context,
|
||||||
|
state: currentState,
|
||||||
|
step: currentStep,
|
||||||
|
status: SetResultStatus.completed,
|
||||||
|
now: now,
|
||||||
|
actualTimeMs: targetMs,
|
||||||
|
actualScore: actualScore,
|
||||||
|
actualScoreTimeMs: actualScoreTimeMs,
|
||||||
|
note: note,
|
||||||
|
);
|
||||||
|
await sessionRepository.saveExerciseStepResult(completed);
|
||||||
|
await sessionRepository.saveExerciseStepProgressState(
|
||||||
|
_advanceState(
|
||||||
|
context,
|
||||||
|
currentState,
|
||||||
|
now,
|
||||||
|
runningTimerOverflowMs: elapsedMs - targetMs,
|
||||||
|
completedStep: currentStep,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return completed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final state = await _autoAdvanceElapsedTimers(context, currentState, now);
|
||||||
final step = _currentStep(context, state);
|
final step = _currentStep(context, state);
|
||||||
if (state.status == ActiveExerciseStepProgressStatus.sequenceComplete) {
|
if (state.status == ActiveExerciseStepProgressStatus.sequenceComplete) {
|
||||||
throw const DomainException('Exercise step sequence is complete.');
|
throw const DomainException('Exercise step sequence is complete.');
|
||||||
@ -2419,7 +2443,7 @@ final class ActiveExerciseStepUseCases {
|
|||||||
);
|
);
|
||||||
await sessionRepository.saveExerciseStepResult(completed);
|
await sessionRepository.saveExerciseStepResult(completed);
|
||||||
await sessionRepository.saveExerciseStepProgressState(
|
await sessionRepository.saveExerciseStepProgressState(
|
||||||
_advanceState(context, state, now),
|
_advanceState(context, state, now, completedStep: step),
|
||||||
);
|
);
|
||||||
return completed;
|
return completed;
|
||||||
}
|
}
|
||||||
@ -2453,7 +2477,7 @@ final class ActiveExerciseStepUseCases {
|
|||||||
);
|
);
|
||||||
await sessionRepository.saveExerciseStepResult(skipped);
|
await sessionRepository.saveExerciseStepResult(skipped);
|
||||||
await sessionRepository.saveExerciseStepProgressState(
|
await sessionRepository.saveExerciseStepProgressState(
|
||||||
_advanceState(context, state, now),
|
_advanceState(context, state, now, completedStep: step),
|
||||||
);
|
);
|
||||||
return skipped;
|
return skipped;
|
||||||
}
|
}
|
||||||
@ -2491,7 +2515,7 @@ final class ActiveExerciseStepUseCases {
|
|||||||
);
|
);
|
||||||
skipped.add(result);
|
skipped.add(result);
|
||||||
await sessionRepository.saveExerciseStepResult(result);
|
await sessionRepository.saveExerciseStepResult(result);
|
||||||
state = _advanceState(context, state, now);
|
state = _advanceState(context, state, now, completedStep: step);
|
||||||
}
|
}
|
||||||
await sessionRepository.saveExerciseStepProgressState(state);
|
await sessionRepository.saveExerciseStepProgressState(state);
|
||||||
return skipped;
|
return skipped;
|
||||||
@ -2528,7 +2552,7 @@ final class ActiveExerciseStepUseCases {
|
|||||||
);
|
);
|
||||||
skipped.add(result);
|
skipped.add(result);
|
||||||
await sessionRepository.saveExerciseStepResult(result);
|
await sessionRepository.saveExerciseStepResult(result);
|
||||||
state = _advanceState(context, state, now);
|
state = _advanceState(context, state, now, completedStep: step);
|
||||||
}
|
}
|
||||||
await sessionRepository.saveExerciseStepProgressState(state);
|
await sessionRepository.saveExerciseStepProgressState(state);
|
||||||
return skipped;
|
return skipped;
|
||||||
@ -2581,6 +2605,7 @@ final class ActiveExerciseStepUseCases {
|
|||||||
expectedPassages: snapshot.repsEnabled
|
expectedPassages: snapshot.repsEnabled
|
||||||
? (snapshot.targetReps ?? 1).clamp(1, 1 << 31)
|
? (snapshot.targetReps ?? 1).clamp(1, 1 << 31)
|
||||||
: 1,
|
: 1,
|
||||||
|
autoStartNextTimedStep: snapshot.autoStartNextTimedStepEffective,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2638,6 +2663,7 @@ final class ActiveExerciseStepUseCases {
|
|||||||
current,
|
current,
|
||||||
now,
|
now,
|
||||||
runningTimerOverflowMs: overflowMs,
|
runningTimerOverflowMs: overflowMs,
|
||||||
|
completedStep: step,
|
||||||
);
|
);
|
||||||
await sessionRepository.saveExerciseStepProgressState(current);
|
await sessionRepository.saveExerciseStepProgressState(current);
|
||||||
}
|
}
|
||||||
@ -2649,6 +2675,7 @@ final class ActiveExerciseStepUseCases {
|
|||||||
ActiveExerciseStepProgressState state,
|
ActiveExerciseStepProgressState state,
|
||||||
DateTime now, {
|
DateTime now, {
|
||||||
int runningTimerOverflowMs = 0,
|
int runningTimerOverflowMs = 0,
|
||||||
|
ExerciseStep? completedStep,
|
||||||
}) {
|
}) {
|
||||||
var nextPassage = state.currentPassageIndex;
|
var nextPassage = state.currentPassageIndex;
|
||||||
var nextStep = state.currentStepIndex + 1;
|
var nextStep = state.currentStepIndex + 1;
|
||||||
@ -2666,8 +2693,15 @@ final class ActiveExerciseStepUseCases {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
final step = context.steps[nextStep];
|
final step = context.steps[nextStep];
|
||||||
|
final shouldStopBeforeNextTimedStep =
|
||||||
|
!context.autoStartNextTimedStep &&
|
||||||
|
completedStep?.type == ExerciseStepType.time &&
|
||||||
|
step.type == ExerciseStepType.time;
|
||||||
|
final resolvedOverflowMs = shouldStopBeforeNextTimedStep
|
||||||
|
? 0
|
||||||
|
: runningTimerOverflowMs;
|
||||||
final status = step.type == ExerciseStepType.time
|
final status = step.type == ExerciseStepType.time
|
||||||
? runningTimerOverflowMs > 0
|
? resolvedOverflowMs > 0
|
||||||
? ActiveExerciseStepProgressStatus.runningTimer
|
? ActiveExerciseStepProgressStatus.runningTimer
|
||||||
: ActiveExerciseStepProgressStatus.stoppedTimer
|
: ActiveExerciseStepProgressStatus.stoppedTimer
|
||||||
: ActiveExerciseStepProgressStatus.waitingManual;
|
: ActiveExerciseStepProgressStatus.waitingManual;
|
||||||
@ -2678,7 +2712,7 @@ final class ActiveExerciseStepUseCases {
|
|||||||
currentStepSnapshotId: step.id,
|
currentStepSnapshotId: step.id,
|
||||||
status: status,
|
status: status,
|
||||||
startedAt: status == ActiveExerciseStepProgressStatus.runningTimer
|
startedAt: status == ActiveExerciseStepProgressStatus.runningTimer
|
||||||
? now.subtract(Duration(milliseconds: runningTimerOverflowMs))
|
? now.subtract(Duration(milliseconds: resolvedOverflowMs))
|
||||||
: null,
|
: null,
|
||||||
accumulatedMs: 0,
|
accumulatedMs: 0,
|
||||||
lastTransitionAt: now,
|
lastTransitionAt: now,
|
||||||
@ -3320,6 +3354,15 @@ _ResolvedExerciseSnapshot? _findExerciseSnapshot({
|
|||||||
}
|
}
|
||||||
final exercise = exercises[exerciseIndex] as Map<String, dynamic>;
|
final exercise = exercises[exerciseIndex] as Map<String, dynamic>;
|
||||||
final id = exercise['id'] as String? ?? 'exercise-$exerciseIndex';
|
final id = exercise['id'] as String? ?? 'exercise-$exerciseIndex';
|
||||||
|
final autoStartNextTimedStepEffective =
|
||||||
|
_templateStepChainingOverride(
|
||||||
|
decoded,
|
||||||
|
workoutTemplateProgramId: programSnapshotId,
|
||||||
|
snapshotProgramExerciseId: id,
|
||||||
|
) ??
|
||||||
|
(exercise['autoStartNextTimedStepOverride'] as bool?) ??
|
||||||
|
(exercise['autoStartNextTimedStepSnapshot'] as bool?) ??
|
||||||
|
true;
|
||||||
return _ResolvedExerciseSnapshot(
|
return _ResolvedExerciseSnapshot(
|
||||||
programSnapshotId: programSnapshotId,
|
programSnapshotId: programSnapshotId,
|
||||||
exerciseSnapshotId: id,
|
exerciseSnapshotId: id,
|
||||||
@ -3338,9 +3381,31 @@ _ResolvedExerciseSnapshot? _findExerciseSnapshot({
|
|||||||
scoreLabelSnapshot: exercise['scoreLabelSnapshot'] as String?,
|
scoreLabelSnapshot: exercise['scoreLabelSnapshot'] as String?,
|
||||||
scoreUnitSnapshot: exercise['scoreUnitSnapshot'] as String?,
|
scoreUnitSnapshot: exercise['scoreUnitSnapshot'] as String?,
|
||||||
steps: _exerciseStepsFromSnapshot(exercise['exerciseStepsSnapshot']),
|
steps: _exerciseStepsFromSnapshot(exercise['exerciseStepsSnapshot']),
|
||||||
|
autoStartNextTimedStepEffective: autoStartNextTimedStepEffective,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool? _templateStepChainingOverride(
|
||||||
|
Map<String, dynamic> resolvedTemplateSnapshot, {
|
||||||
|
required String workoutTemplateProgramId,
|
||||||
|
required String snapshotProgramExerciseId,
|
||||||
|
}) {
|
||||||
|
final overrides =
|
||||||
|
(resolvedTemplateSnapshot['overrides'] as List<dynamic>? ?? const []);
|
||||||
|
for (final raw in overrides) {
|
||||||
|
if (raw is! Map) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final override = Map<String, dynamic>.from(raw);
|
||||||
|
if (override['workoutTemplateProgramId'] == workoutTemplateProgramId &&
|
||||||
|
override['snapshotProgramExerciseId'] == snapshotProgramExerciseId) {
|
||||||
|
final value = override['autoStartNextTimedStepOverride'];
|
||||||
|
return value is bool ? value : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
List<_SetPositionSnapshot> _listSetSnapshots(
|
List<_SetPositionSnapshot> _listSetSnapshots(
|
||||||
String resolvedTemplateSnapshotJson,
|
String resolvedTemplateSnapshotJson,
|
||||||
) {
|
) {
|
||||||
@ -3422,6 +3487,7 @@ final class _StepSequenceContext {
|
|||||||
required this.setIndex,
|
required this.setIndex,
|
||||||
required this.steps,
|
required this.steps,
|
||||||
required this.expectedPassages,
|
required this.expectedPassages,
|
||||||
|
required this.autoStartNextTimedStep,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String sessionId;
|
final String sessionId;
|
||||||
@ -3432,6 +3498,7 @@ final class _StepSequenceContext {
|
|||||||
final int setIndex;
|
final int setIndex;
|
||||||
final List<ExerciseStep> steps;
|
final List<ExerciseStep> steps;
|
||||||
final int expectedPassages;
|
final int expectedPassages;
|
||||||
|
final bool autoStartNextTimedStep;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<WorkoutHistorySetResult> _historyResultsFromActiveResults({
|
List<WorkoutHistorySetResult> _historyResultsFromActiveResults({
|
||||||
@ -3523,6 +3590,10 @@ Map<String, _ResolvedExerciseSnapshot> _exerciseSnapshotsById(
|
|||||||
scoreLabelSnapshot: exercise['scoreLabelSnapshot'] as String?,
|
scoreLabelSnapshot: exercise['scoreLabelSnapshot'] as String?,
|
||||||
scoreUnitSnapshot: exercise['scoreUnitSnapshot'] as String?,
|
scoreUnitSnapshot: exercise['scoreUnitSnapshot'] as String?,
|
||||||
steps: _exerciseStepsFromSnapshot(exercise['exerciseStepsSnapshot']),
|
steps: _exerciseStepsFromSnapshot(exercise['exerciseStepsSnapshot']),
|
||||||
|
autoStartNextTimedStepEffective:
|
||||||
|
(exercise['autoStartNextTimedStepOverride'] as bool?) ??
|
||||||
|
(exercise['autoStartNextTimedStepSnapshot'] as bool?) ??
|
||||||
|
true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3546,6 +3617,7 @@ final class _ResolvedExerciseSnapshot {
|
|||||||
this.scoreLabelSnapshot,
|
this.scoreLabelSnapshot,
|
||||||
this.scoreUnitSnapshot,
|
this.scoreUnitSnapshot,
|
||||||
this.steps = const [],
|
this.steps = const [],
|
||||||
|
this.autoStartNextTimedStepEffective = true,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String programSnapshotId;
|
final String programSnapshotId;
|
||||||
@ -3563,6 +3635,7 @@ final class _ResolvedExerciseSnapshot {
|
|||||||
final String? scoreLabelSnapshot;
|
final String? scoreLabelSnapshot;
|
||||||
final String? scoreUnitSnapshot;
|
final String? scoreUnitSnapshot;
|
||||||
final List<ExerciseStep> steps;
|
final List<ExerciseStep> steps;
|
||||||
|
final bool autoStartNextTimedStepEffective;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<ExerciseStep> _exerciseStepsFromSnapshot(Object? value) {
|
List<ExerciseStep> _exerciseStepsFromSnapshot(Object? value) {
|
||||||
|
|||||||
@ -3047,8 +3047,7 @@ domain.Exercise _exerciseFromPayload(RemoteSyncedItem item) {
|
|||||||
defaultTargetReps: payload['defaultTargetReps'] as int?,
|
defaultTargetReps: payload['defaultTargetReps'] as int?,
|
||||||
defaultTargetScore: (payload['defaultTargetScore'] as num?)?.toDouble(),
|
defaultTargetScore: (payload['defaultTargetScore'] as num?)?.toDouble(),
|
||||||
defaultTargetScoreTimeMs: payload['defaultTargetScoreTimeMs'] as int?,
|
defaultTargetScoreTimeMs: payload['defaultTargetScoreTimeMs'] as int?,
|
||||||
autoStartNextTimedStep:
|
autoStartNextTimedStep: payload['autoStartNextTimedStep'] as bool? ?? true,
|
||||||
payload['autoStartNextTimedStep'] as bool? ?? true,
|
|
||||||
steps: _stepsFromPayload(payload['steps']),
|
steps: _stepsFromPayload(payload['steps']),
|
||||||
archivedAt: _dateTimeFromPayload(payload['archivedAt']),
|
archivedAt: _dateTimeFromPayload(payload['archivedAt']),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -404,6 +404,7 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
|||||||
var _importingImage = false;
|
var _importingImage = false;
|
||||||
var _importingVideo = false;
|
var _importingVideo = false;
|
||||||
var _stepsEnabled = false;
|
var _stepsEnabled = false;
|
||||||
|
var _autoStartNextTimedStep = true;
|
||||||
var _nextStepDraftId = 0;
|
var _nextStepDraftId = 0;
|
||||||
final _stepDrafts = <_ExerciseStepDraft>[];
|
final _stepDrafts = <_ExerciseStepDraft>[];
|
||||||
String? _measureError;
|
String? _measureError;
|
||||||
@ -442,6 +443,7 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
|||||||
_hasScore = exercise?.hasScoreMeasure ?? false;
|
_hasScore = exercise?.hasScoreMeasure ?? false;
|
||||||
_scoreInputMode = exercise?.scoreInputMode ?? ScoreInputMode.manual;
|
_scoreInputMode = exercise?.scoreInputMode ?? ScoreInputMode.manual;
|
||||||
_stepsEnabled = exercise?.steps.isNotEmpty ?? false;
|
_stepsEnabled = exercise?.steps.isNotEmpty ?? false;
|
||||||
|
_autoStartNextTimedStep = exercise?.autoStartNextTimedStep ?? true;
|
||||||
for (final step in exercise?.steps ?? const <ExerciseStep>[]) {
|
for (final step in exercise?.steps ?? const <ExerciseStep>[]) {
|
||||||
_stepDrafts.add(_draftFromStep(step));
|
_stepDrafts.add(_draftFromStep(step));
|
||||||
}
|
}
|
||||||
@ -742,6 +744,25 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (_stepsEnabled) ...[
|
if (_stepsEnabled) ...[
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
SwitchListTile(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
title: const Text(
|
||||||
|
'Enchaîner automatiquement les chronos consécutifs',
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
_autoStartNextTimedStep
|
||||||
|
? 'Le chrono suivant démarre dès que le précédent arrive à 0.'
|
||||||
|
: "L'app attendra ton démarrage avant de lancer le chrono "
|
||||||
|
'suivant.',
|
||||||
|
),
|
||||||
|
value: _autoStartNextTimedStep,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_autoStartNextTimedStep = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
ReorderableListView.builder(
|
ReorderableListView.builder(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
@ -1312,6 +1333,7 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
|||||||
defaultTargetReps: defaultTargetReps,
|
defaultTargetReps: defaultTargetReps,
|
||||||
defaultTargetScore: defaultTargetScore,
|
defaultTargetScore: defaultTargetScore,
|
||||||
defaultTargetScoreTimeMs: defaultTargetScoreTimeMs,
|
defaultTargetScoreTimeMs: defaultTargetScoreTimeMs,
|
||||||
|
autoStartNextTimedStep: _autoStartNextTimedStep,
|
||||||
steps: steps,
|
steps: steps,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@ -1332,6 +1354,7 @@ final class _ExerciseFormScreenState extends State<ExerciseFormScreen> {
|
|||||||
defaultTargetReps: defaultTargetReps,
|
defaultTargetReps: defaultTargetReps,
|
||||||
defaultTargetScore: defaultTargetScore,
|
defaultTargetScore: defaultTargetScore,
|
||||||
defaultTargetScoreTimeMs: defaultTargetScoreTimeMs,
|
defaultTargetScoreTimeMs: defaultTargetScoreTimeMs,
|
||||||
|
autoStartNextTimedStep: _autoStartNextTimedStep,
|
||||||
steps: steps,
|
steps: steps,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -660,6 +660,11 @@ final class _ProgramExerciseCustomizationScreenState
|
|||||||
: scoreUnit == null
|
: scoreUnit == null
|
||||||
? 'Score'
|
? 'Score'
|
||||||
: 'Score ($scoreUnit)';
|
: 'Score ($scoreUnit)';
|
||||||
|
final autoStartNextTimedStepCustomized =
|
||||||
|
_draft.autoStartNextTimedStepOverride != null;
|
||||||
|
final autoStartNextTimedStepStateLabel = autoStartNextTimedStepCustomized
|
||||||
|
? 'Personnalisé pour ce programme'
|
||||||
|
: "Réglage de l'exercice";
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: const Text("Personnaliser l'exercice")),
|
appBar: AppBar(title: const Text("Personnaliser l'exercice")),
|
||||||
bottomNavigationBar: SafeArea(
|
bottomNavigationBar: SafeArea(
|
||||||
@ -785,6 +790,36 @@ final class _ProgramExerciseCustomizationScreenState
|
|||||||
_draft.targetScoreTimeMs = null;
|
_draft.targetScoreTimeMs = null;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
if (_draft.exerciseStepsSnapshot.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Text('Séquence', style: Theme.of(context).textTheme.titleMedium),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
SwitchListTile(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
title: const Text(
|
||||||
|
'Enchaîner automatiquement les chronos consécutifs',
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
'$autoStartNextTimedStepStateLabel\n'
|
||||||
|
'${_autoStartHelpText(_draft.autoStartNextTimedStepEffective)}',
|
||||||
|
),
|
||||||
|
value: _draft.autoStartNextTimedStepEffective,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_draft.autoStartNextTimedStepOverride = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (autoStartNextTimedStepCustomized)
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
_draft.autoStartNextTimedStepOverride = null;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: const Text("Revenir au réglage de l'exercice"),
|
||||||
|
),
|
||||||
|
],
|
||||||
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),
|
||||||
@ -916,6 +951,8 @@ final class _ProgramExerciseDraft {
|
|||||||
this.exerciseImageMediaIdsSnapshot = const [],
|
this.exerciseImageMediaIdsSnapshot = const [],
|
||||||
this.exerciseVideoMediaIdSnapshot,
|
this.exerciseVideoMediaIdSnapshot,
|
||||||
this.exerciseStepsSnapshot = const [],
|
this.exerciseStepsSnapshot = const [],
|
||||||
|
this.autoStartNextTimedStepSnapshot = true,
|
||||||
|
this.autoStartNextTimedStepOverride,
|
||||||
this.exerciseArchivedSnapshot = false,
|
this.exerciseArchivedSnapshot = false,
|
||||||
required this.availableTimeSnapshot,
|
required this.availableTimeSnapshot,
|
||||||
required this.availableRepsSnapshot,
|
required this.availableRepsSnapshot,
|
||||||
@ -946,6 +983,7 @@ final class _ProgramExerciseDraft {
|
|||||||
exerciseImageMediaIdsSnapshot: exercise.imageMediaIds,
|
exerciseImageMediaIdsSnapshot: exercise.imageMediaIds,
|
||||||
exerciseVideoMediaIdSnapshot: exercise.videoMediaId,
|
exerciseVideoMediaIdSnapshot: exercise.videoMediaId,
|
||||||
exerciseStepsSnapshot: exercise.steps,
|
exerciseStepsSnapshot: exercise.steps,
|
||||||
|
autoStartNextTimedStepSnapshot: exercise.autoStartNextTimedStep,
|
||||||
exerciseArchivedSnapshot: exercise.archivedAt != null,
|
exerciseArchivedSnapshot: exercise.archivedAt != null,
|
||||||
availableTimeSnapshot: exercise.hasTimeMeasure,
|
availableTimeSnapshot: exercise.hasTimeMeasure,
|
||||||
availableRepsSnapshot: exercise.hasRepsMeasure,
|
availableRepsSnapshot: exercise.hasRepsMeasure,
|
||||||
@ -978,6 +1016,8 @@ final class _ProgramExerciseDraft {
|
|||||||
exerciseImageMediaIdsSnapshot: exercise.exerciseImageMediaIdsSnapshot,
|
exerciseImageMediaIdsSnapshot: exercise.exerciseImageMediaIdsSnapshot,
|
||||||
exerciseVideoMediaIdSnapshot: exercise.exerciseVideoMediaIdSnapshot,
|
exerciseVideoMediaIdSnapshot: exercise.exerciseVideoMediaIdSnapshot,
|
||||||
exerciseStepsSnapshot: exercise.exerciseStepsSnapshot,
|
exerciseStepsSnapshot: exercise.exerciseStepsSnapshot,
|
||||||
|
autoStartNextTimedStepSnapshot: exercise.autoStartNextTimedStepSnapshot,
|
||||||
|
autoStartNextTimedStepOverride: exercise.autoStartNextTimedStepOverride,
|
||||||
exerciseArchivedSnapshot: exercise.exerciseArchivedSnapshot,
|
exerciseArchivedSnapshot: exercise.exerciseArchivedSnapshot,
|
||||||
availableTimeSnapshot: exercise.availableTimeSnapshot,
|
availableTimeSnapshot: exercise.availableTimeSnapshot,
|
||||||
availableRepsSnapshot: exercise.availableRepsSnapshot,
|
availableRepsSnapshot: exercise.availableRepsSnapshot,
|
||||||
@ -1008,6 +1048,7 @@ final class _ProgramExerciseDraft {
|
|||||||
final List<String> exerciseImageMediaIdsSnapshot;
|
final List<String> exerciseImageMediaIdsSnapshot;
|
||||||
final String? exerciseVideoMediaIdSnapshot;
|
final String? exerciseVideoMediaIdSnapshot;
|
||||||
final List<ExerciseStep> exerciseStepsSnapshot;
|
final List<ExerciseStep> exerciseStepsSnapshot;
|
||||||
|
final bool autoStartNextTimedStepSnapshot;
|
||||||
final bool exerciseArchivedSnapshot;
|
final bool exerciseArchivedSnapshot;
|
||||||
final bool availableTimeSnapshot;
|
final bool availableTimeSnapshot;
|
||||||
final bool availableRepsSnapshot;
|
final bool availableRepsSnapshot;
|
||||||
@ -1022,6 +1063,11 @@ final class _ProgramExerciseDraft {
|
|||||||
double? targetScore;
|
double? targetScore;
|
||||||
int? targetScoreTimeMs;
|
int? targetScoreTimeMs;
|
||||||
int? restSecondsOverride;
|
int? restSecondsOverride;
|
||||||
|
bool? autoStartNextTimedStepOverride;
|
||||||
|
|
||||||
|
bool get autoStartNextTimedStepEffective {
|
||||||
|
return autoStartNextTimedStepOverride ?? autoStartNextTimedStepSnapshot;
|
||||||
|
}
|
||||||
|
|
||||||
_ProgramExerciseDraft copy() {
|
_ProgramExerciseDraft copy() {
|
||||||
return _ProgramExerciseDraft(
|
return _ProgramExerciseDraft(
|
||||||
@ -1034,6 +1080,8 @@ final class _ProgramExerciseDraft {
|
|||||||
exerciseImageMediaIdsSnapshot: exerciseImageMediaIdsSnapshot,
|
exerciseImageMediaIdsSnapshot: exerciseImageMediaIdsSnapshot,
|
||||||
exerciseVideoMediaIdSnapshot: exerciseVideoMediaIdSnapshot,
|
exerciseVideoMediaIdSnapshot: exerciseVideoMediaIdSnapshot,
|
||||||
exerciseStepsSnapshot: exerciseStepsSnapshot,
|
exerciseStepsSnapshot: exerciseStepsSnapshot,
|
||||||
|
autoStartNextTimedStepSnapshot: autoStartNextTimedStepSnapshot,
|
||||||
|
autoStartNextTimedStepOverride: autoStartNextTimedStepOverride,
|
||||||
exerciseArchivedSnapshot: exerciseArchivedSnapshot,
|
exerciseArchivedSnapshot: exerciseArchivedSnapshot,
|
||||||
availableTimeSnapshot: availableTimeSnapshot,
|
availableTimeSnapshot: availableTimeSnapshot,
|
||||||
availableRepsSnapshot: availableRepsSnapshot,
|
availableRepsSnapshot: availableRepsSnapshot,
|
||||||
@ -1063,6 +1111,8 @@ final class _ProgramExerciseDraft {
|
|||||||
exerciseImageMediaIdsSnapshot: exerciseImageMediaIdsSnapshot,
|
exerciseImageMediaIdsSnapshot: exerciseImageMediaIdsSnapshot,
|
||||||
exerciseVideoMediaIdSnapshot: exerciseVideoMediaIdSnapshot,
|
exerciseVideoMediaIdSnapshot: exerciseVideoMediaIdSnapshot,
|
||||||
exerciseStepsSnapshot: exerciseStepsSnapshot,
|
exerciseStepsSnapshot: exerciseStepsSnapshot,
|
||||||
|
autoStartNextTimedStepSnapshot: autoStartNextTimedStepSnapshot,
|
||||||
|
autoStartNextTimedStepOverride: autoStartNextTimedStepOverride,
|
||||||
exerciseArchivedSnapshot: exerciseArchivedSnapshot,
|
exerciseArchivedSnapshot: exerciseArchivedSnapshot,
|
||||||
availableTimeSnapshot: availableTimeSnapshot,
|
availableTimeSnapshot: availableTimeSnapshot,
|
||||||
availableRepsSnapshot: availableRepsSnapshot,
|
availableRepsSnapshot: availableRepsSnapshot,
|
||||||
@ -1083,6 +1133,12 @@ final class _ProgramExerciseDraft {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _autoStartHelpText(bool active) {
|
||||||
|
return active
|
||||||
|
? 'Le chrono suivant démarre dès que le précédent arrive à 0.'
|
||||||
|
: "L'app attendra ton démarrage avant de lancer le chrono suivant.";
|
||||||
|
}
|
||||||
|
|
||||||
double? _millisecondsToSeconds(int? milliseconds) {
|
double? _millisecondsToSeconds(int? milliseconds) {
|
||||||
return milliseconds == null ? null : milliseconds / 1000;
|
return milliseconds == null ? null : milliseconds / 1000;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -502,6 +502,12 @@ final class _WorkoutTemplateProgramDetailScreenState
|
|||||||
itemCount: widget.program.exercises.length,
|
itemCount: widget.program.exercises.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final exercise = widget.program.exercises[index];
|
final exercise = widget.program.exercises[index];
|
||||||
|
final autoStartNextTimedStepCustomized =
|
||||||
|
exercise.autoStartNextTimedStepOverride != null;
|
||||||
|
final autoStartNextTimedStepStateLabel =
|
||||||
|
autoStartNextTimedStepCustomized
|
||||||
|
? 'Personnalisé pour cette séance'
|
||||||
|
: 'Réglage du programme';
|
||||||
return Card(
|
return Card(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
@ -564,6 +570,38 @@ final class _WorkoutTemplateProgramDetailScreenState
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
if (exercise.exerciseStepsSnapshot.isNotEmpty) ...[
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'Séquence',
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
SwitchListTile(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
title: const Text(
|
||||||
|
'Enchaîner automatiquement les chronos consécutifs',
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
'$autoStartNextTimedStepStateLabel\n'
|
||||||
|
'${_autoStartHelpText(exercise.autoStartNextTimedStepEffective)}',
|
||||||
|
),
|
||||||
|
value: exercise.autoStartNextTimedStepEffective,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
exercise.autoStartNextTimedStepOverride = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (autoStartNextTimedStepCustomized)
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
exercise.autoStartNextTimedStepOverride = null;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: const Text('Revenir au réglage du programme'),
|
||||||
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -621,6 +659,8 @@ final class WorkoutTemplateProgramDraft {
|
|||||||
override.targetTimeSecondsOverride;
|
override.targetTimeSecondsOverride;
|
||||||
exercise.targetRepsOverride = override.targetRepsOverride;
|
exercise.targetRepsOverride = override.targetRepsOverride;
|
||||||
exercise.targetScoreOverride = override.targetScoreOverride;
|
exercise.targetScoreOverride = override.targetScoreOverride;
|
||||||
|
exercise.autoStartNextTimedStepOverride =
|
||||||
|
override.autoStartNextTimedStepOverride;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -667,6 +707,7 @@ final class WorkoutTemplateProgramDraft {
|
|||||||
targetTimeSecondsOverride: exercise.targetTimeSecondsOverride,
|
targetTimeSecondsOverride: exercise.targetTimeSecondsOverride,
|
||||||
targetRepsOverride: exercise.targetRepsOverride,
|
targetRepsOverride: exercise.targetRepsOverride,
|
||||||
targetScoreOverride: exercise.targetScoreOverride,
|
targetScoreOverride: exercise.targetScoreOverride,
|
||||||
|
autoStartNextTimedStepOverride: exercise.autoStartNextTimedStepOverride,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -680,6 +721,8 @@ final class WorkoutTemplateExerciseDraft {
|
|||||||
required this.timeEnabled,
|
required this.timeEnabled,
|
||||||
required this.repsEnabled,
|
required this.repsEnabled,
|
||||||
required this.scoreEnabled,
|
required this.scoreEnabled,
|
||||||
|
this.exerciseStepsSnapshot = const [],
|
||||||
|
this.autoStartNextTimedStepSnapshot = true,
|
||||||
this.scoreUnitSnapshot,
|
this.scoreUnitSnapshot,
|
||||||
this.targetTimeSeconds,
|
this.targetTimeSeconds,
|
||||||
this.targetReps,
|
this.targetReps,
|
||||||
@ -689,6 +732,7 @@ final class WorkoutTemplateExerciseDraft {
|
|||||||
this.targetTimeSecondsOverride,
|
this.targetTimeSecondsOverride,
|
||||||
this.targetRepsOverride,
|
this.targetRepsOverride,
|
||||||
this.targetScoreOverride,
|
this.targetScoreOverride,
|
||||||
|
this.autoStartNextTimedStepOverride,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String snapshotExerciseId;
|
final String snapshotExerciseId;
|
||||||
@ -697,6 +741,8 @@ final class WorkoutTemplateExerciseDraft {
|
|||||||
final bool timeEnabled;
|
final bool timeEnabled;
|
||||||
final bool repsEnabled;
|
final bool repsEnabled;
|
||||||
final bool scoreEnabled;
|
final bool scoreEnabled;
|
||||||
|
final List<ExerciseStep> exerciseStepsSnapshot;
|
||||||
|
final bool autoStartNextTimedStepSnapshot;
|
||||||
final String? scoreUnitSnapshot;
|
final String? scoreUnitSnapshot;
|
||||||
final int? targetTimeSeconds;
|
final int? targetTimeSeconds;
|
||||||
final int? targetReps;
|
final int? targetReps;
|
||||||
@ -706,12 +752,18 @@ final class WorkoutTemplateExerciseDraft {
|
|||||||
int? targetTimeSecondsOverride;
|
int? targetTimeSecondsOverride;
|
||||||
int? targetRepsOverride;
|
int? targetRepsOverride;
|
||||||
double? targetScoreOverride;
|
double? targetScoreOverride;
|
||||||
|
bool? autoStartNextTimedStepOverride;
|
||||||
|
|
||||||
|
bool get autoStartNextTimedStepEffective {
|
||||||
|
return autoStartNextTimedStepOverride ?? autoStartNextTimedStepSnapshot;
|
||||||
|
}
|
||||||
|
|
||||||
bool get hasOverride {
|
bool get hasOverride {
|
||||||
return setsCountOverride != null ||
|
return setsCountOverride != null ||
|
||||||
targetTimeSecondsOverride != null ||
|
targetTimeSecondsOverride != null ||
|
||||||
targetRepsOverride != null ||
|
targetRepsOverride != null ||
|
||||||
targetScoreOverride != null;
|
targetScoreOverride != null ||
|
||||||
|
autoStartNextTimedStepOverride != null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -746,6 +798,10 @@ List<WorkoutTemplateExerciseDraft> _parseExercises(
|
|||||||
final rawExercises = snapshot['exercises'] as List<dynamic>? ?? const [];
|
final rawExercises = snapshot['exercises'] as List<dynamic>? ?? const [];
|
||||||
return rawExercises.map((raw) {
|
return rawExercises.map((raw) {
|
||||||
final exercise = raw as Map<String, dynamic>;
|
final exercise = raw as Map<String, dynamic>;
|
||||||
|
final autoStartNextTimedStepSnapshot =
|
||||||
|
(exercise['autoStartNextTimedStepOverride'] as bool?) ??
|
||||||
|
(exercise['autoStartNextTimedStepSnapshot'] as bool?) ??
|
||||||
|
true;
|
||||||
return WorkoutTemplateExerciseDraft(
|
return WorkoutTemplateExerciseDraft(
|
||||||
snapshotExerciseId: exercise['id'] as String,
|
snapshotExerciseId: exercise['id'] as String,
|
||||||
exerciseNameSnapshot: exercise['exerciseNameSnapshot'] as String,
|
exerciseNameSnapshot: exercise['exerciseNameSnapshot'] as String,
|
||||||
@ -753,6 +809,10 @@ List<WorkoutTemplateExerciseDraft> _parseExercises(
|
|||||||
timeEnabled: exercise['timeEnabled'] as bool,
|
timeEnabled: exercise['timeEnabled'] as bool,
|
||||||
repsEnabled: exercise['repsEnabled'] as bool,
|
repsEnabled: exercise['repsEnabled'] as bool,
|
||||||
scoreEnabled: exercise['scoreEnabled'] as bool,
|
scoreEnabled: exercise['scoreEnabled'] as bool,
|
||||||
|
exerciseStepsSnapshot: _parseExerciseSteps(
|
||||||
|
exercise['exerciseStepsSnapshot'],
|
||||||
|
),
|
||||||
|
autoStartNextTimedStepSnapshot: autoStartNextTimedStepSnapshot,
|
||||||
scoreUnitSnapshot: exercise['scoreUnitSnapshot'] as String?,
|
scoreUnitSnapshot: exercise['scoreUnitSnapshot'] as String?,
|
||||||
targetTimeSeconds: exercise['targetTimeSeconds'] as int?,
|
targetTimeSeconds: exercise['targetTimeSeconds'] as int?,
|
||||||
targetReps: exercise['targetReps'] as int?,
|
targetReps: exercise['targetReps'] as int?,
|
||||||
@ -761,6 +821,34 @@ List<WorkoutTemplateExerciseDraft> _parseExercises(
|
|||||||
}).toList();
|
}).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<ExerciseStep> _parseExerciseSteps(Object? value) {
|
||||||
|
final rawSteps = value as List<dynamic>? ?? const [];
|
||||||
|
return rawSteps.map((raw) {
|
||||||
|
final step = raw as Map<String, dynamic>;
|
||||||
|
return ExerciseStep(
|
||||||
|
id: step['id'] as String,
|
||||||
|
position: step['position'] as int,
|
||||||
|
name: step['name'] as String,
|
||||||
|
type: ExerciseStepType.values.byName(step['type'] as String),
|
||||||
|
defaultTargetValue: step['defaultTargetValue'] as int,
|
||||||
|
hasScore: step['hasScore'] as bool? ?? false,
|
||||||
|
scoreInputMode: ScoreInputMode.values.byName(
|
||||||
|
step['scoreInputMode'] as String? ?? ScoreInputMode.manual.name,
|
||||||
|
),
|
||||||
|
scoreLabel: step['scoreLabel'] as String?,
|
||||||
|
scoreUnit: step['scoreUnit'] as String?,
|
||||||
|
defaultTargetScore: (step['defaultTargetScore'] as num?)?.toDouble(),
|
||||||
|
defaultTargetScoreTimeMs: step['defaultTargetScoreTimeMs'] as int?,
|
||||||
|
);
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
String _autoStartHelpText(bool active) {
|
||||||
|
return active
|
||||||
|
? 'Le chrono suivant démarre dès que le précédent arrive à 0.'
|
||||||
|
: "L'app attendra ton démarrage avant de lancer le chrono suivant.";
|
||||||
|
}
|
||||||
|
|
||||||
String _templateSummary(WorkoutTemplate template) {
|
String _templateSummary(WorkoutTemplate template) {
|
||||||
final programCount = template.programs.length;
|
final programCount = template.programs.length;
|
||||||
final exerciseCount = template.programs.fold<int>(
|
final exerciseCount = template.programs.fold<int>(
|
||||||
|
|||||||
@ -479,49 +479,52 @@ void main() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('program exercise snapshots step chaining setting from exercise', () async {
|
test(
|
||||||
final exerciseRepository = _FakeExerciseRepository()
|
'program exercise snapshots step chaining setting from exercise',
|
||||||
..exercise = Exercise(
|
() async {
|
||||||
metadata: _metadata('exercise-1'),
|
final exerciseRepository = _FakeExerciseRepository()
|
||||||
name: 'Circuit',
|
..exercise = Exercise(
|
||||||
hasTimeMeasure: false,
|
metadata: _metadata('exercise-1'),
|
||||||
hasRepsMeasure: true,
|
name: 'Circuit',
|
||||||
hasScoreMeasure: false,
|
hasTimeMeasure: false,
|
||||||
defaultTargetReps: 10,
|
hasRepsMeasure: true,
|
||||||
autoStartNextTimedStep: false,
|
hasScoreMeasure: false,
|
||||||
|
defaultTargetReps: 10,
|
||||||
|
autoStartNextTimedStep: false,
|
||||||
|
);
|
||||||
|
final programRepository = _FakeProgramRepository()
|
||||||
|
..programs.add(
|
||||||
|
Program(
|
||||||
|
metadata: _metadata('program-1'),
|
||||||
|
name: 'Programme',
|
||||||
|
defaultRestSeconds: 60,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final useCase = ProgramUseCases(
|
||||||
|
programRepository: programRepository,
|
||||||
|
exerciseRepository: exerciseRepository,
|
||||||
|
templateRepository: _FakeWorkoutTemplateRepository(),
|
||||||
|
clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)),
|
||||||
|
ids: _FakeIds(),
|
||||||
|
originDeviceId: 'device-1',
|
||||||
);
|
);
|
||||||
final programRepository = _FakeProgramRepository()
|
|
||||||
..programs.add(
|
final snapshot = await useCase.addExercise(
|
||||||
Program(
|
programId: 'program-1',
|
||||||
metadata: _metadata('program-1'),
|
exerciseId: 'exercise-1',
|
||||||
name: 'Programme',
|
position: 0,
|
||||||
defaultRestSeconds: 60,
|
setsCount: 3,
|
||||||
),
|
enabledMeasures: const {WorkoutMeasure.reps},
|
||||||
);
|
);
|
||||||
final useCase = ProgramUseCases(
|
|
||||||
programRepository: programRepository,
|
|
||||||
exerciseRepository: exerciseRepository,
|
|
||||||
templateRepository: _FakeWorkoutTemplateRepository(),
|
|
||||||
clock: _FakeClock(DateTime.utc(2026, 7, 17, 12)),
|
|
||||||
ids: _FakeIds(),
|
|
||||||
originDeviceId: 'device-1',
|
|
||||||
);
|
|
||||||
|
|
||||||
final snapshot = await useCase.addExercise(
|
expect(snapshot.autoStartNextTimedStepSnapshot, isFalse);
|
||||||
programId: 'program-1',
|
expect(snapshot.autoStartNextTimedStepOverride, isNull);
|
||||||
exerciseId: 'exercise-1',
|
expect(
|
||||||
position: 0,
|
snapshot.toSnapshotJson()['autoStartNextTimedStepSnapshot'],
|
||||||
setsCount: 3,
|
isFalse,
|
||||||
enabledMeasures: const {WorkoutMeasure.reps},
|
);
|
||||||
);
|
},
|
||||||
|
);
|
||||||
expect(snapshot.autoStartNextTimedStepSnapshot, isFalse);
|
|
||||||
expect(snapshot.autoStartNextTimedStepOverride, isNull);
|
|
||||||
expect(
|
|
||||||
snapshot.toSnapshotJson()['autoStartNextTimedStepSnapshot'],
|
|
||||||
isFalse,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('program saveConfigured preserves step chaining override', () async {
|
test('program saveConfigured preserves step chaining override', () async {
|
||||||
final programRepository = _FakeProgramRepository();
|
final programRepository = _FakeProgramRepository();
|
||||||
@ -596,10 +599,7 @@ void main() {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(
|
expect(template.overrides.single.autoStartNextTimedStepOverride, isFalse);
|
||||||
template.overrides.single.autoStartNextTimedStepOverride,
|
|
||||||
isFalse,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('delete exercise keeps existing program snapshots unchanged', () async {
|
test('delete exercise keeps existing program snapshots unchanged', () async {
|
||||||
@ -1230,6 +1230,318 @@ void main() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'step chaining resolution uses template override before program override',
|
||||||
|
() async {
|
||||||
|
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
|
||||||
|
final session = _sessionWithSnapshot(
|
||||||
|
currentProgramIndex: 0,
|
||||||
|
currentExerciseIndex: 0,
|
||||||
|
currentSetIndex: 0,
|
||||||
|
setsCount: 1,
|
||||||
|
targetReps: 1,
|
||||||
|
autoStartNextTimedStepSnapshot: true,
|
||||||
|
autoStartNextTimedStepOverride: true,
|
||||||
|
templateAutoStartNextTimedStepOverride: false,
|
||||||
|
exerciseSteps: _twoTimedSteps(),
|
||||||
|
);
|
||||||
|
final repository = _FakeActiveSessionRepository()..session = session;
|
||||||
|
final useCase = ActiveExerciseStepUseCases(
|
||||||
|
sessionRepository: repository,
|
||||||
|
clock: clock,
|
||||||
|
ids: _FakeIds(),
|
||||||
|
originDeviceId: 'device-1',
|
||||||
|
);
|
||||||
|
|
||||||
|
await useCase.startOrResumeProgress(
|
||||||
|
sessionId: session.metadata.id,
|
||||||
|
programIndex: 0,
|
||||||
|
exerciseIndex: 0,
|
||||||
|
setIndex: 0,
|
||||||
|
);
|
||||||
|
await useCase.startTimer(
|
||||||
|
sessionId: session.metadata.id,
|
||||||
|
programIndex: 0,
|
||||||
|
exerciseIndex: 0,
|
||||||
|
setIndex: 0,
|
||||||
|
);
|
||||||
|
clock.value = clock.value.add(const Duration(milliseconds: 2500));
|
||||||
|
|
||||||
|
final view = await useCase.startOrResumeProgress(
|
||||||
|
sessionId: session.metadata.id,
|
||||||
|
programIndex: 0,
|
||||||
|
exerciseIndex: 0,
|
||||||
|
setIndex: 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(view.state.status, ActiveExerciseStepProgressStatus.stoppedTimer);
|
||||||
|
expect(view.state.currentStepIndex, 1);
|
||||||
|
expect(view.state.accumulatedMs, 0);
|
||||||
|
expect(repository.stepResults, hasLength(1));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'step chaining resolution uses program override before exercise snapshot',
|
||||||
|
() async {
|
||||||
|
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
|
||||||
|
final session = _sessionWithSnapshot(
|
||||||
|
currentProgramIndex: 0,
|
||||||
|
currentExerciseIndex: 0,
|
||||||
|
currentSetIndex: 0,
|
||||||
|
setsCount: 1,
|
||||||
|
targetReps: 1,
|
||||||
|
autoStartNextTimedStepSnapshot: false,
|
||||||
|
autoStartNextTimedStepOverride: true,
|
||||||
|
exerciseSteps: _twoTimedSteps(),
|
||||||
|
);
|
||||||
|
final repository = _FakeActiveSessionRepository()..session = session;
|
||||||
|
final useCase = ActiveExerciseStepUseCases(
|
||||||
|
sessionRepository: repository,
|
||||||
|
clock: clock,
|
||||||
|
ids: _FakeIds(),
|
||||||
|
originDeviceId: 'device-1',
|
||||||
|
);
|
||||||
|
|
||||||
|
await useCase.startOrResumeProgress(
|
||||||
|
sessionId: session.metadata.id,
|
||||||
|
programIndex: 0,
|
||||||
|
exerciseIndex: 0,
|
||||||
|
setIndex: 0,
|
||||||
|
);
|
||||||
|
await useCase.startTimer(
|
||||||
|
sessionId: session.metadata.id,
|
||||||
|
programIndex: 0,
|
||||||
|
exerciseIndex: 0,
|
||||||
|
setIndex: 0,
|
||||||
|
);
|
||||||
|
clock.value = clock.value.add(const Duration(milliseconds: 2500));
|
||||||
|
|
||||||
|
final view = await useCase.startOrResumeProgress(
|
||||||
|
sessionId: session.metadata.id,
|
||||||
|
programIndex: 0,
|
||||||
|
exerciseIndex: 0,
|
||||||
|
setIndex: 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
view.state.status,
|
||||||
|
ActiveExerciseStepProgressStatus.sequenceComplete,
|
||||||
|
);
|
||||||
|
expect(repository.stepResults, hasLength(2));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'step chaining defaults to true when snapshot fields are absent',
|
||||||
|
() async {
|
||||||
|
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
|
||||||
|
final session = _sessionWithSnapshot(
|
||||||
|
currentProgramIndex: 0,
|
||||||
|
currentExerciseIndex: 0,
|
||||||
|
currentSetIndex: 0,
|
||||||
|
setsCount: 1,
|
||||||
|
targetReps: 1,
|
||||||
|
autoStartNextTimedStepSnapshot: null,
|
||||||
|
exerciseSteps: _twoTimedSteps(),
|
||||||
|
);
|
||||||
|
final repository = _FakeActiveSessionRepository()..session = session;
|
||||||
|
final useCase = ActiveExerciseStepUseCases(
|
||||||
|
sessionRepository: repository,
|
||||||
|
clock: clock,
|
||||||
|
ids: _FakeIds(),
|
||||||
|
originDeviceId: 'device-1',
|
||||||
|
);
|
||||||
|
|
||||||
|
await useCase.startOrResumeProgress(
|
||||||
|
sessionId: session.metadata.id,
|
||||||
|
programIndex: 0,
|
||||||
|
exerciseIndex: 0,
|
||||||
|
setIndex: 0,
|
||||||
|
);
|
||||||
|
await useCase.startTimer(
|
||||||
|
sessionId: session.metadata.id,
|
||||||
|
programIndex: 0,
|
||||||
|
exerciseIndex: 0,
|
||||||
|
setIndex: 0,
|
||||||
|
);
|
||||||
|
clock.value = clock.value.add(const Duration(milliseconds: 2500));
|
||||||
|
|
||||||
|
final view = await useCase.startOrResumeProgress(
|
||||||
|
sessionId: session.metadata.id,
|
||||||
|
programIndex: 0,
|
||||||
|
exerciseIndex: 0,
|
||||||
|
setIndex: 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
view.state.status,
|
||||||
|
ActiveExerciseStepProgressStatus.sequenceComplete,
|
||||||
|
);
|
||||||
|
expect(repository.stepResults, hasLength(2));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'completeCurrentStep leaves next timed step stopped when chaining disabled',
|
||||||
|
() async {
|
||||||
|
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
|
||||||
|
final session = _sessionWithSnapshot(
|
||||||
|
currentProgramIndex: 0,
|
||||||
|
currentExerciseIndex: 0,
|
||||||
|
currentSetIndex: 0,
|
||||||
|
setsCount: 1,
|
||||||
|
targetReps: 1,
|
||||||
|
autoStartNextTimedStepSnapshot: false,
|
||||||
|
exerciseSteps: _twoTimedSteps(),
|
||||||
|
);
|
||||||
|
final repository = _FakeActiveSessionRepository()..session = session;
|
||||||
|
final useCase = ActiveExerciseStepUseCases(
|
||||||
|
sessionRepository: repository,
|
||||||
|
clock: clock,
|
||||||
|
ids: _FakeIds(),
|
||||||
|
originDeviceId: 'device-1',
|
||||||
|
);
|
||||||
|
|
||||||
|
await useCase.startOrResumeProgress(
|
||||||
|
sessionId: session.metadata.id,
|
||||||
|
programIndex: 0,
|
||||||
|
exerciseIndex: 0,
|
||||||
|
setIndex: 0,
|
||||||
|
);
|
||||||
|
await useCase.startTimer(
|
||||||
|
sessionId: session.metadata.id,
|
||||||
|
programIndex: 0,
|
||||||
|
exerciseIndex: 0,
|
||||||
|
setIndex: 0,
|
||||||
|
);
|
||||||
|
clock.value = clock.value.add(const Duration(milliseconds: 500));
|
||||||
|
await useCase.completeCurrentStep(
|
||||||
|
sessionId: session.metadata.id,
|
||||||
|
programIndex: 0,
|
||||||
|
exerciseIndex: 0,
|
||||||
|
setIndex: 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
final view = await useCase.readProgress(
|
||||||
|
sessionId: session.metadata.id,
|
||||||
|
programIndex: 0,
|
||||||
|
exerciseIndex: 0,
|
||||||
|
setIndex: 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(view.state.status, ActiveExerciseStepProgressStatus.stoppedTimer);
|
||||||
|
expect(view.state.currentStepIndex, 1);
|
||||||
|
expect(view.state.startedAt, isNull);
|
||||||
|
expect(view.state.accumulatedMs, 0);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'completeCurrentStep starts next timed step when chaining is enabled',
|
||||||
|
() async {
|
||||||
|
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
|
||||||
|
final session = _sessionWithSnapshot(
|
||||||
|
currentProgramIndex: 0,
|
||||||
|
currentExerciseIndex: 0,
|
||||||
|
currentSetIndex: 0,
|
||||||
|
setsCount: 1,
|
||||||
|
targetReps: 1,
|
||||||
|
autoStartNextTimedStepSnapshot: true,
|
||||||
|
exerciseSteps: _twoTimedSteps(),
|
||||||
|
);
|
||||||
|
final repository = _FakeActiveSessionRepository()..session = session;
|
||||||
|
final useCase = ActiveExerciseStepUseCases(
|
||||||
|
sessionRepository: repository,
|
||||||
|
clock: clock,
|
||||||
|
ids: _FakeIds(),
|
||||||
|
originDeviceId: 'device-1',
|
||||||
|
);
|
||||||
|
|
||||||
|
await useCase.startOrResumeProgress(
|
||||||
|
sessionId: session.metadata.id,
|
||||||
|
programIndex: 0,
|
||||||
|
exerciseIndex: 0,
|
||||||
|
setIndex: 0,
|
||||||
|
);
|
||||||
|
await useCase.startTimer(
|
||||||
|
sessionId: session.metadata.id,
|
||||||
|
programIndex: 0,
|
||||||
|
exerciseIndex: 0,
|
||||||
|
setIndex: 0,
|
||||||
|
);
|
||||||
|
clock.value = clock.value.add(const Duration(milliseconds: 1500));
|
||||||
|
await useCase.completeCurrentStep(
|
||||||
|
sessionId: session.metadata.id,
|
||||||
|
programIndex: 0,
|
||||||
|
exerciseIndex: 0,
|
||||||
|
setIndex: 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
final view = await useCase.readProgress(
|
||||||
|
sessionId: session.metadata.id,
|
||||||
|
programIndex: 0,
|
||||||
|
exerciseIndex: 0,
|
||||||
|
setIndex: 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(view.state.status, ActiveExerciseStepProgressStatus.runningTimer);
|
||||||
|
expect(view.state.currentStepIndex, 1);
|
||||||
|
expect(view.state.elapsedMillisecondsAt(clock.now()), 500);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test('startTimer manually starts a stopped next timed step', () async {
|
||||||
|
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
|
||||||
|
final session = _sessionWithSnapshot(
|
||||||
|
currentProgramIndex: 0,
|
||||||
|
currentExerciseIndex: 0,
|
||||||
|
currentSetIndex: 0,
|
||||||
|
setsCount: 1,
|
||||||
|
targetReps: 1,
|
||||||
|
autoStartNextTimedStepSnapshot: false,
|
||||||
|
exerciseSteps: _twoTimedSteps(),
|
||||||
|
);
|
||||||
|
final repository = _FakeActiveSessionRepository()..session = session;
|
||||||
|
final useCase = ActiveExerciseStepUseCases(
|
||||||
|
sessionRepository: repository,
|
||||||
|
clock: clock,
|
||||||
|
ids: _FakeIds(),
|
||||||
|
originDeviceId: 'device-1',
|
||||||
|
);
|
||||||
|
|
||||||
|
await useCase.startOrResumeProgress(
|
||||||
|
sessionId: session.metadata.id,
|
||||||
|
programIndex: 0,
|
||||||
|
exerciseIndex: 0,
|
||||||
|
setIndex: 0,
|
||||||
|
);
|
||||||
|
await useCase.startTimer(
|
||||||
|
sessionId: session.metadata.id,
|
||||||
|
programIndex: 0,
|
||||||
|
exerciseIndex: 0,
|
||||||
|
setIndex: 0,
|
||||||
|
);
|
||||||
|
clock.value = clock.value.add(const Duration(milliseconds: 1500));
|
||||||
|
await useCase.startOrResumeProgress(
|
||||||
|
sessionId: session.metadata.id,
|
||||||
|
programIndex: 0,
|
||||||
|
exerciseIndex: 0,
|
||||||
|
setIndex: 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
final running = await useCase.startTimer(
|
||||||
|
sessionId: session.metadata.id,
|
||||||
|
programIndex: 0,
|
||||||
|
exerciseIndex: 0,
|
||||||
|
setIndex: 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(running.status, ActiveExerciseStepProgressStatus.runningTimer);
|
||||||
|
expect(running.currentStepIndex, 1);
|
||||||
|
expect(running.startedAt, clock.now());
|
||||||
|
});
|
||||||
|
|
||||||
test('AuthUseCases register stores token and account session', () async {
|
test('AuthUseCases register stores token and account session', () async {
|
||||||
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
|
final clock = _FakeClock(DateTime.utc(2026, 7, 17, 12));
|
||||||
final tokenStore = _FakeAuthTokenStore();
|
final tokenStore = _FakeAuthTokenStore();
|
||||||
@ -2622,7 +2934,26 @@ ActiveWorkoutSession _sessionWithSnapshot({
|
|||||||
int setsCount = 2,
|
int setsCount = 2,
|
||||||
int targetReps = 10,
|
int targetReps = 10,
|
||||||
List<ExerciseStep> exerciseSteps = const [],
|
List<ExerciseStep> exerciseSteps = const [],
|
||||||
|
bool? autoStartNextTimedStepSnapshot = true,
|
||||||
|
bool? autoStartNextTimedStepOverride,
|
||||||
|
bool? templateAutoStartNextTimedStepOverride,
|
||||||
}) {
|
}) {
|
||||||
|
final exerciseSnapshot = {
|
||||||
|
'id': 'exercise-snapshot-1',
|
||||||
|
'exerciseNameSnapshot': 'Squat',
|
||||||
|
'setsCount': setsCount,
|
||||||
|
'timeEnabled': false,
|
||||||
|
'repsEnabled': true,
|
||||||
|
'scoreEnabled': false,
|
||||||
|
'targetReps': targetReps,
|
||||||
|
'exerciseStepsSnapshot': exerciseSteps
|
||||||
|
.map((step) => step.toSnapshotJson())
|
||||||
|
.toList(),
|
||||||
|
if (autoStartNextTimedStepSnapshot != null)
|
||||||
|
'autoStartNextTimedStepSnapshot': autoStartNextTimedStepSnapshot,
|
||||||
|
if (autoStartNextTimedStepOverride != null)
|
||||||
|
'autoStartNextTimedStepOverride': autoStartNextTimedStepOverride,
|
||||||
|
};
|
||||||
return ActiveWorkoutSession(
|
return ActiveWorkoutSession(
|
||||||
metadata: _metadata('session-1'),
|
metadata: _metadata('session-1'),
|
||||||
status: ActiveWorkoutStatus.running,
|
status: ActiveWorkoutStatus.running,
|
||||||
@ -2638,23 +2969,38 @@ ActiveWorkoutSession _sessionWithSnapshot({
|
|||||||
'id': 'program-snapshot-1',
|
'id': 'program-snapshot-1',
|
||||||
'programNameSnapshot': 'Programme',
|
'programNameSnapshot': 'Programme',
|
||||||
'programSnapshotJson': jsonEncode({
|
'programSnapshotJson': jsonEncode({
|
||||||
'exercises': [
|
'exercises': [exerciseSnapshot],
|
||||||
{
|
|
||||||
'id': 'exercise-snapshot-1',
|
|
||||||
'exerciseNameSnapshot': 'Squat',
|
|
||||||
'setsCount': setsCount,
|
|
||||||
'timeEnabled': false,
|
|
||||||
'repsEnabled': true,
|
|
||||||
'scoreEnabled': false,
|
|
||||||
'targetReps': targetReps,
|
|
||||||
'exerciseStepsSnapshot': exerciseSteps
|
|
||||||
.map((step) => step.toSnapshotJson())
|
|
||||||
.toList(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
'overrides': [
|
||||||
|
if (templateAutoStartNextTimedStepOverride != null)
|
||||||
|
{
|
||||||
|
'workoutTemplateProgramId': 'program-snapshot-1',
|
||||||
|
'snapshotProgramExerciseId': 'exercise-snapshot-1',
|
||||||
|
'autoStartNextTimedStepOverride':
|
||||||
|
templateAutoStartNextTimedStepOverride,
|
||||||
|
},
|
||||||
|
],
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<ExerciseStep> _twoTimedSteps() {
|
||||||
|
return [
|
||||||
|
_step(
|
||||||
|
id: 'step-1',
|
||||||
|
position: 0,
|
||||||
|
name: 'Gauche',
|
||||||
|
type: ExerciseStepType.time,
|
||||||
|
defaultTargetValue: 1,
|
||||||
|
),
|
||||||
|
_step(
|
||||||
|
id: 'step-2',
|
||||||
|
position: 1,
|
||||||
|
name: 'Droite',
|
||||||
|
type: ExerciseStepType.time,
|
||||||
|
defaultTargetValue: 1,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|||||||
@ -98,99 +98,108 @@ void main() {
|
|||||||
expect(restored!.defaultTargetScore, 0);
|
expect(restored!.defaultTargetScore, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('step chaining settings round-trip through drift repositories', () async {
|
test(
|
||||||
final now = DateTime.utc(2026, 7, 17, 12);
|
'step chaining settings round-trip through drift repositories',
|
||||||
final exercise = Exercise(
|
() async {
|
||||||
metadata: _metadata('exercise-chain-1', now),
|
final now = DateTime.utc(2026, 7, 17, 12);
|
||||||
name: 'Circuit',
|
final exercise = Exercise(
|
||||||
hasTimeMeasure: false,
|
metadata: _metadata('exercise-chain-1', now),
|
||||||
hasRepsMeasure: true,
|
name: 'Circuit',
|
||||||
hasScoreMeasure: false,
|
hasTimeMeasure: false,
|
||||||
defaultTargetReps: 10,
|
hasRepsMeasure: true,
|
||||||
autoStartNextTimedStep: false,
|
hasScoreMeasure: false,
|
||||||
);
|
defaultTargetReps: 10,
|
||||||
await exerciseRepository.save(exercise);
|
autoStartNextTimedStep: false,
|
||||||
|
);
|
||||||
|
await exerciseRepository.save(exercise);
|
||||||
|
|
||||||
final restoredExercise = await exerciseRepository.findById(
|
final restoredExercise = await exerciseRepository.findById(
|
||||||
exercise.metadata.id,
|
exercise.metadata.id,
|
||||||
);
|
);
|
||||||
expect(restoredExercise!.autoStartNextTimedStep, isFalse);
|
expect(restoredExercise!.autoStartNextTimedStep, isFalse);
|
||||||
|
|
||||||
final programExercise = _programExercise(
|
final programExercise = _programExercise(
|
||||||
'program-exercise-chain-1',
|
'program-exercise-chain-1',
|
||||||
now,
|
now,
|
||||||
programId: 'program-chain-1',
|
programId: 'program-chain-1',
|
||||||
position: 0,
|
position: 0,
|
||||||
autoStartNextTimedStepSnapshot: false,
|
autoStartNextTimedStepSnapshot: false,
|
||||||
autoStartNextTimedStepOverride: true,
|
autoStartNextTimedStepOverride: true,
|
||||||
);
|
);
|
||||||
await programRepository.save(
|
await programRepository.save(
|
||||||
Program(
|
Program(
|
||||||
metadata: _metadata('program-chain-1', now),
|
metadata: _metadata('program-chain-1', now),
|
||||||
name: 'Programme',
|
name: 'Programme',
|
||||||
defaultRestSeconds: 60,
|
defaultRestSeconds: 60,
|
||||||
exercises: [programExercise],
|
exercises: [programExercise],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
final restoredProgram = await programRepository.findById('program-chain-1');
|
final restoredProgram = await programRepository.findById(
|
||||||
expect(
|
'program-chain-1',
|
||||||
restoredProgram!.exercises.single.autoStartNextTimedStepSnapshot,
|
);
|
||||||
isFalse,
|
expect(
|
||||||
);
|
restoredProgram!.exercises.single.autoStartNextTimedStepSnapshot,
|
||||||
expect(
|
isFalse,
|
||||||
restoredProgram.exercises.single.autoStartNextTimedStepOverride,
|
);
|
||||||
isTrue,
|
expect(
|
||||||
);
|
restoredProgram.exercises.single.autoStartNextTimedStepOverride,
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
|
||||||
final templateProgram = WorkoutTemplateProgram(
|
final templateProgram = WorkoutTemplateProgram(
|
||||||
metadata: _metadata('template-program-chain-1', now),
|
metadata: _metadata('template-program-chain-1', now),
|
||||||
workoutTemplateId: 'template-chain-1',
|
workoutTemplateId: 'template-chain-1',
|
||||||
sourceProgramId: 'program-chain-1',
|
sourceProgramId: 'program-chain-1',
|
||||||
position: 0,
|
position: 0,
|
||||||
programNameSnapshot: 'Programme',
|
programNameSnapshot: 'Programme',
|
||||||
defaultRestSecondsSnapshot: 60,
|
defaultRestSecondsSnapshot: 60,
|
||||||
programSnapshotJson: jsonEncode({
|
programSnapshotJson: jsonEncode({
|
||||||
'exercises': [programExercise.toSnapshotJson()],
|
'exercises': [programExercise.toSnapshotJson()],
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
await templateRepository.save(
|
await templateRepository.save(
|
||||||
WorkoutTemplate(
|
WorkoutTemplate(
|
||||||
metadata: _metadata('template-chain-1', now),
|
metadata: _metadata('template-chain-1', now),
|
||||||
name: 'Séance',
|
name: 'Séance',
|
||||||
programs: [templateProgram],
|
programs: [templateProgram],
|
||||||
overrides: [
|
overrides: [
|
||||||
WorkoutTemplateExerciseOverride(
|
WorkoutTemplateExerciseOverride(
|
||||||
metadata: _metadata('override-chain-1', now),
|
metadata: _metadata('override-chain-1', now),
|
||||||
workoutTemplateProgramId: templateProgram.metadata.id,
|
workoutTemplateProgramId: templateProgram.metadata.id,
|
||||||
snapshotProgramExerciseId: programExercise.metadata.id,
|
snapshotProgramExerciseId: programExercise.metadata.id,
|
||||||
),
|
),
|
||||||
WorkoutTemplateExerciseOverride(
|
WorkoutTemplateExerciseOverride(
|
||||||
metadata: _metadata('override-chain-2', now),
|
metadata: _metadata('override-chain-2', now),
|
||||||
workoutTemplateProgramId: templateProgram.metadata.id,
|
workoutTemplateProgramId: templateProgram.metadata.id,
|
||||||
snapshotProgramExerciseId: 'program-exercise-chain-2',
|
snapshotProgramExerciseId: 'program-exercise-chain-2',
|
||||||
autoStartNextTimedStepOverride: false,
|
autoStartNextTimedStepOverride: false,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
final restoredTemplate = await templateRepository.findById(
|
final restoredTemplate = await templateRepository.findById(
|
||||||
'template-chain-1',
|
'template-chain-1',
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
restoredTemplate!.overrides
|
restoredTemplate!.overrides
|
||||||
.singleWhere((override) => override.metadata.id == 'override-chain-1')
|
.singleWhere(
|
||||||
.autoStartNextTimedStepOverride,
|
(override) => override.metadata.id == 'override-chain-1',
|
||||||
isNull,
|
)
|
||||||
);
|
.autoStartNextTimedStepOverride,
|
||||||
expect(
|
isNull,
|
||||||
restoredTemplate.overrides
|
);
|
||||||
.singleWhere((override) => override.metadata.id == 'override-chain-2')
|
expect(
|
||||||
.autoStartNextTimedStepOverride,
|
restoredTemplate.overrides
|
||||||
isFalse,
|
.singleWhere(
|
||||||
);
|
(override) => override.metadata.id == 'override-chain-2',
|
||||||
});
|
)
|
||||||
|
.autoStartNextTimedStepOverride,
|
||||||
|
isFalse,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test('exercise repository round-trips configured steps', () async {
|
test('exercise repository round-trips configured steps', () async {
|
||||||
final now = DateTime.utc(2026, 7, 17, 12);
|
final now = DateTime.utc(2026, 7, 17, 12);
|
||||||
|
|||||||
@ -435,6 +435,58 @@ void main() {
|
|||||||
expect(saved.steps.single.defaultTargetValue, 12);
|
expect(saved.steps.single.defaultTargetValue, 12);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'le réglage d’enchaînement automatique des étapes est sauvegardé',
|
||||||
|
(tester) async {
|
||||||
|
final exerciseRepository = _FakeExerciseRepository();
|
||||||
|
|
||||||
|
await tester.binding.setSurfaceSize(const Size(400, 3600));
|
||||||
|
addTearDown(() => tester.binding.setSurfaceSize(null));
|
||||||
|
|
||||||
|
await _pumpExerciseForm(tester, exerciseRepository);
|
||||||
|
await tester.enterText(
|
||||||
|
find.widgetWithText(TextFormField, 'Nom'),
|
||||||
|
'Combo',
|
||||||
|
);
|
||||||
|
await tester.enterText(
|
||||||
|
find.widgetWithText(TextFormField, 'Temps par défaut (s)'),
|
||||||
|
'30',
|
||||||
|
);
|
||||||
|
await tester.tap(
|
||||||
|
find.widgetWithText(
|
||||||
|
SwitchListTile,
|
||||||
|
'Rythmer cet exercice avec des étapes',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
await tester.tap(
|
||||||
|
find.widgetWithText(
|
||||||
|
SwitchListTile,
|
||||||
|
'Enchaîner automatiquement les chronos consécutifs',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
await tester.tap(find.text('Ajouter une étape'));
|
||||||
|
await tester.pump();
|
||||||
|
await tester.enterText(
|
||||||
|
find.widgetWithText(TextFormField, 'Nom de l’étape').last,
|
||||||
|
'Dribble main droite',
|
||||||
|
);
|
||||||
|
await tester.enterText(
|
||||||
|
find.widgetWithText(TextFormField, 'Durée par défaut (s)').last,
|
||||||
|
'12',
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.ensureVisible(find.text('Enregistrer'));
|
||||||
|
await tester.tap(find.text('Enregistrer'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
final saved = exerciseRepository.saved.single;
|
||||||
|
expect(saved.steps, hasLength(1));
|
||||||
|
expect(saved.autoStartNextTimedStep, isFalse);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
testWidgets('désigner une image comme icône l’indique visuellement', (
|
testWidgets('désigner une image comme icône l’indique visuellement', (
|
||||||
tester,
|
tester,
|
||||||
) async {
|
) async {
|
||||||
|
|||||||
@ -306,6 +306,165 @@ void main() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'personnaliser l’enchaînement automatique transmet l’override au programme',
|
||||||
|
(tester) async {
|
||||||
|
final steps = [
|
||||||
|
_step(
|
||||||
|
id: 'step-1',
|
||||||
|
position: 0,
|
||||||
|
name: 'Course',
|
||||||
|
type: ExerciseStepType.time,
|
||||||
|
),
|
||||||
|
_step(
|
||||||
|
id: 'step-2',
|
||||||
|
position: 1,
|
||||||
|
name: 'Retour',
|
||||||
|
type: ExerciseStepType.time,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
final programRepository = _FakeProgramRepository();
|
||||||
|
final exerciseRepository = _FakeExerciseRepository()
|
||||||
|
..exercises.add(
|
||||||
|
Exercise(
|
||||||
|
metadata: _metadata('exercise-steps'),
|
||||||
|
name: 'Navettes',
|
||||||
|
hasTimeMeasure: true,
|
||||||
|
hasRepsMeasure: false,
|
||||||
|
hasScoreMeasure: false,
|
||||||
|
defaultTargetTimeSeconds: 30,
|
||||||
|
steps: steps,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.binding.setSurfaceSize(const Size(400, 2400));
|
||||||
|
addTearDown(() => tester.binding.setSurfaceSize(null));
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: ProgramFormScreen(
|
||||||
|
programUseCases: _programUseCases(
|
||||||
|
programRepository,
|
||||||
|
exerciseRepository,
|
||||||
|
),
|
||||||
|
exerciseUseCases: _exerciseUseCases(exerciseRepository),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.enterText(
|
||||||
|
find.widgetWithText(TextFormField, 'Nom'),
|
||||||
|
'Programme étapes',
|
||||||
|
);
|
||||||
|
await tester.tap(find.text('Ajouter un exercice'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.text('Navettes'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.byTooltip("Personnaliser l'exercice"));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.tap(
|
||||||
|
find.widgetWithText(
|
||||||
|
SwitchListTile,
|
||||||
|
'Enchaîner automatiquement les chronos consécutifs',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
expect(
|
||||||
|
find.textContaining('Personnalisé pour ce programme'),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.tap(find.text('Enregistrer'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.text('Enregistrer'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
final savedExercise = programRepository.programs.single.exercises.single;
|
||||||
|
expect(savedExercise.autoStartNextTimedStepSnapshot, isTrue);
|
||||||
|
expect(savedExercise.autoStartNextTimedStepOverride, isFalse);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'revenir au réglage de l’exercice remet l’override programme à null',
|
||||||
|
(tester) async {
|
||||||
|
final steps = [
|
||||||
|
_step(
|
||||||
|
id: 'step-1',
|
||||||
|
position: 0,
|
||||||
|
name: 'Course',
|
||||||
|
type: ExerciseStepType.time,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
final programRepository = _FakeProgramRepository();
|
||||||
|
final exerciseRepository = _FakeExerciseRepository()
|
||||||
|
..exercises.add(
|
||||||
|
Exercise(
|
||||||
|
metadata: _metadata('exercise-steps'),
|
||||||
|
name: 'Navettes',
|
||||||
|
hasTimeMeasure: true,
|
||||||
|
hasRepsMeasure: false,
|
||||||
|
hasScoreMeasure: false,
|
||||||
|
defaultTargetTimeSeconds: 30,
|
||||||
|
autoStartNextTimedStep: false,
|
||||||
|
steps: steps,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.binding.setSurfaceSize(const Size(400, 2400));
|
||||||
|
addTearDown(() => tester.binding.setSurfaceSize(null));
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: ProgramFormScreen(
|
||||||
|
programUseCases: _programUseCases(
|
||||||
|
programRepository,
|
||||||
|
exerciseRepository,
|
||||||
|
),
|
||||||
|
exerciseUseCases: _exerciseUseCases(exerciseRepository),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.enterText(
|
||||||
|
find.widgetWithText(TextFormField, 'Nom'),
|
||||||
|
'Programme étapes',
|
||||||
|
);
|
||||||
|
await tester.tap(find.text('Ajouter un exercice'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.text('Navettes'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.byTooltip("Personnaliser l'exercice"));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.tap(
|
||||||
|
find.widgetWithText(
|
||||||
|
SwitchListTile,
|
||||||
|
'Enchaîner automatiquement les chronos consécutifs',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
expect(
|
||||||
|
find.textContaining('Personnalisé pour ce programme'),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.tap(find.text("Revenir au réglage de l'exercice"));
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.textContaining("Réglage de l'exercice"), findsOneWidget);
|
||||||
|
|
||||||
|
await tester.tap(find.text('Enregistrer'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.text('Enregistrer'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
final savedExercise = programRepository.programs.single.exercises.single;
|
||||||
|
expect(savedExercise.autoStartNextTimedStepSnapshot, isFalse);
|
||||||
|
expect(savedExercise.autoStartNextTimedStepOverride, isNull);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
testWidgets('affiche l’objectif de chrono pour un score en mode chrono', (
|
testWidgets('affiche l’objectif de chrono pour un score en mode chrono', (
|
||||||
tester,
|
tester,
|
||||||
) async {
|
) async {
|
||||||
|
|||||||
@ -44,6 +44,117 @@ void main() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'personnaliser l’enchaînement automatique transmet l’override séance',
|
||||||
|
(tester) async {
|
||||||
|
final program = WorkoutTemplateProgramDraft(
|
||||||
|
clientKey: 'program-1',
|
||||||
|
programNameSnapshot: 'Circuit',
|
||||||
|
defaultRestSecondsSnapshot: 60,
|
||||||
|
programSnapshotJson: '{}',
|
||||||
|
exercises: [
|
||||||
|
WorkoutTemplateExerciseDraft(
|
||||||
|
snapshotExerciseId: 'exercise-1',
|
||||||
|
exerciseNameSnapshot: 'Navettes',
|
||||||
|
setsCount: 3,
|
||||||
|
timeEnabled: true,
|
||||||
|
repsEnabled: false,
|
||||||
|
scoreEnabled: false,
|
||||||
|
exerciseStepsSnapshot: [
|
||||||
|
_step(
|
||||||
|
id: 'step-1',
|
||||||
|
position: 0,
|
||||||
|
name: 'Course',
|
||||||
|
type: ExerciseStepType.time,
|
||||||
|
),
|
||||||
|
_step(
|
||||||
|
id: 'step-2',
|
||||||
|
position: 1,
|
||||||
|
name: 'Retour',
|
||||||
|
type: ExerciseStepType.time,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
autoStartNextTimedStepSnapshot: true,
|
||||||
|
targetTimeSeconds: 30,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.binding.setSurfaceSize(const Size(400, 1400));
|
||||||
|
addTearDown(() => tester.binding.setSurfaceSize(null));
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(home: WorkoutTemplateProgramDetailScreen(program: program)),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.tap(
|
||||||
|
find.widgetWithText(
|
||||||
|
SwitchListTile,
|
||||||
|
'Enchaîner automatiquement les chronos consécutifs',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find.textContaining('Personnalisé pour cette séance'),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
final override = program.toOverrides().single;
|
||||||
|
expect(override.autoStartNextTimedStepOverride, isFalse);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'revenir au réglage du programme remet l’override séance à null',
|
||||||
|
(tester) async {
|
||||||
|
final program = WorkoutTemplateProgramDraft(
|
||||||
|
clientKey: 'program-1',
|
||||||
|
programNameSnapshot: 'Circuit',
|
||||||
|
defaultRestSecondsSnapshot: 60,
|
||||||
|
programSnapshotJson: '{}',
|
||||||
|
exercises: [
|
||||||
|
WorkoutTemplateExerciseDraft(
|
||||||
|
snapshotExerciseId: 'exercise-1',
|
||||||
|
exerciseNameSnapshot: 'Navettes',
|
||||||
|
setsCount: 3,
|
||||||
|
timeEnabled: true,
|
||||||
|
repsEnabled: false,
|
||||||
|
scoreEnabled: false,
|
||||||
|
exerciseStepsSnapshot: [
|
||||||
|
_step(
|
||||||
|
id: 'step-1',
|
||||||
|
position: 0,
|
||||||
|
name: 'Course',
|
||||||
|
type: ExerciseStepType.time,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
autoStartNextTimedStepSnapshot: false,
|
||||||
|
autoStartNextTimedStepOverride: true,
|
||||||
|
targetTimeSeconds: 30,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.binding.setSurfaceSize(const Size(400, 1400));
|
||||||
|
addTearDown(() => tester.binding.setSurfaceSize(null));
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(home: WorkoutTemplateProgramDetailScreen(program: program)),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find.textContaining('Personnalisé pour cette séance'),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.tap(find.text('Revenir au réglage du programme'));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(find.textContaining('Réglage du programme'), findsOneWidget);
|
||||||
|
expect(program.toOverrides(), isEmpty);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
testWidgets(
|
testWidgets(
|
||||||
'lancer une séance alors qu’une autre est en cours affiche un message',
|
'lancer une séance alors qu’une autre est en cours affiche un message',
|
||||||
(tester) async {
|
(tester) async {
|
||||||
@ -308,6 +419,21 @@ EntityMetadata _metadata(String id) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ExerciseStep _step({
|
||||||
|
required String id,
|
||||||
|
required int position,
|
||||||
|
required String name,
|
||||||
|
required ExerciseStepType type,
|
||||||
|
}) {
|
||||||
|
return ExerciseStep(
|
||||||
|
id: id,
|
||||||
|
position: position,
|
||||||
|
name: name,
|
||||||
|
type: type,
|
||||||
|
defaultTargetValue: type == ExerciseStepType.time ? 30 : 5,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
final class _FakeClock implements Clock {
|
final class _FakeClock implements Clock {
|
||||||
const _FakeClock(this.value);
|
const _FakeClock(this.value);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user