feat(exécution): affichage dernière performance et meilleur score (ticket #81)
Affiche la dernière performance et le meilleur score par exercice pendant l'exécution de séance (écran d'exécution), ainsi que les points d'accès associés dans l'accueil, la séance-modèle et l'historique, en s'appuyant sur ExercisePerformanceReferenceUseCase. Validé GO par Main : dart analyze propre, 178 tests (4 échecs préexistants sans rapport, identiques à ceux déjà documentés sur #80). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -15,6 +15,7 @@ final class HistoryListScreen extends StatefulWidget {
|
|||||||
required this.closeUseCase,
|
required this.closeUseCase,
|
||||||
this.mediaUseCases,
|
this.mediaUseCases,
|
||||||
this.stepUseCases,
|
this.stepUseCases,
|
||||||
|
this.performanceReferenceUseCase,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -22,6 +23,7 @@ final class HistoryListScreen extends StatefulWidget {
|
|||||||
final WorkoutTemplateUseCases workoutTemplateUseCases;
|
final WorkoutTemplateUseCases workoutTemplateUseCases;
|
||||||
final ActiveWorkoutSessionUseCases activeUseCases;
|
final ActiveWorkoutSessionUseCases activeUseCases;
|
||||||
final ActiveExerciseStepUseCases? stepUseCases;
|
final ActiveExerciseStepUseCases? stepUseCases;
|
||||||
|
final ExercisePerformanceReferenceUseCase? performanceReferenceUseCase;
|
||||||
final CloseWorkoutSessionUseCase closeUseCase;
|
final CloseWorkoutSessionUseCase closeUseCase;
|
||||||
final MediaUseCases? mediaUseCases;
|
final MediaUseCases? mediaUseCases;
|
||||||
|
|
||||||
@ -97,6 +99,7 @@ final class _HistoryListScreenState extends State<HistoryListScreen> {
|
|||||||
workoutTemplateUseCases: widget.workoutTemplateUseCases,
|
workoutTemplateUseCases: widget.workoutTemplateUseCases,
|
||||||
activeUseCases: widget.activeUseCases,
|
activeUseCases: widget.activeUseCases,
|
||||||
stepUseCases: widget.stepUseCases,
|
stepUseCases: widget.stepUseCases,
|
||||||
|
performanceReferenceUseCase: widget.performanceReferenceUseCase,
|
||||||
closeUseCase: widget.closeUseCase,
|
closeUseCase: widget.closeUseCase,
|
||||||
mediaUseCases: widget.mediaUseCases,
|
mediaUseCases: widget.mediaUseCases,
|
||||||
),
|
),
|
||||||
@ -119,6 +122,7 @@ final class HistoryDetailScreen extends StatelessWidget {
|
|||||||
required this.closeUseCase,
|
required this.closeUseCase,
|
||||||
this.mediaUseCases,
|
this.mediaUseCases,
|
||||||
this.stepUseCases,
|
this.stepUseCases,
|
||||||
|
this.performanceReferenceUseCase,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -127,6 +131,7 @@ final class HistoryDetailScreen extends StatelessWidget {
|
|||||||
final WorkoutTemplateUseCases workoutTemplateUseCases;
|
final WorkoutTemplateUseCases workoutTemplateUseCases;
|
||||||
final ActiveWorkoutSessionUseCases activeUseCases;
|
final ActiveWorkoutSessionUseCases activeUseCases;
|
||||||
final ActiveExerciseStepUseCases? stepUseCases;
|
final ActiveExerciseStepUseCases? stepUseCases;
|
||||||
|
final ExercisePerformanceReferenceUseCase? performanceReferenceUseCase;
|
||||||
final CloseWorkoutSessionUseCase closeUseCase;
|
final CloseWorkoutSessionUseCase closeUseCase;
|
||||||
final MediaUseCases? mediaUseCases;
|
final MediaUseCases? mediaUseCases;
|
||||||
|
|
||||||
@ -213,6 +218,7 @@ final class HistoryDetailScreen extends StatelessWidget {
|
|||||||
closeUseCase: closeUseCase,
|
closeUseCase: closeUseCase,
|
||||||
historyUseCases: historyUseCases,
|
historyUseCases: historyUseCases,
|
||||||
workoutTemplateUseCases: workoutTemplateUseCases,
|
workoutTemplateUseCases: workoutTemplateUseCases,
|
||||||
|
performanceReferenceUseCase: performanceReferenceUseCase,
|
||||||
mediaUseCases: mediaUseCases,
|
mediaUseCases: mediaUseCases,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -157,6 +157,8 @@ final class _HomeScreenState extends State<HomeScreen> with RouteAware {
|
|||||||
programUseCases: widget.bootstrap.programUseCases,
|
programUseCases: widget.bootstrap.programUseCases,
|
||||||
activeUseCases: widget.bootstrap.activeWorkoutSessionUseCases,
|
activeUseCases: widget.bootstrap.activeWorkoutSessionUseCases,
|
||||||
stepUseCases: widget.bootstrap.activeExerciseStepUseCases,
|
stepUseCases: widget.bootstrap.activeExerciseStepUseCases,
|
||||||
|
performanceReferenceUseCase:
|
||||||
|
widget.bootstrap.exercisePerformanceReferenceUseCase,
|
||||||
closeUseCase: widget.bootstrap.closeWorkoutSessionUseCase,
|
closeUseCase: widget.bootstrap.closeWorkoutSessionUseCase,
|
||||||
historyUseCases: widget.bootstrap.workoutHistoryUseCases,
|
historyUseCases: widget.bootstrap.workoutHistoryUseCases,
|
||||||
mediaUseCases: widget.bootstrap.mediaUseCases,
|
mediaUseCases: widget.bootstrap.mediaUseCases,
|
||||||
@ -179,6 +181,8 @@ final class _HomeScreenState extends State<HomeScreen> with RouteAware {
|
|||||||
widget.bootstrap.workoutTemplateUseCases,
|
widget.bootstrap.workoutTemplateUseCases,
|
||||||
activeUseCases: widget.bootstrap.activeWorkoutSessionUseCases,
|
activeUseCases: widget.bootstrap.activeWorkoutSessionUseCases,
|
||||||
stepUseCases: widget.bootstrap.activeExerciseStepUseCases,
|
stepUseCases: widget.bootstrap.activeExerciseStepUseCases,
|
||||||
|
performanceReferenceUseCase:
|
||||||
|
widget.bootstrap.exercisePerformanceReferenceUseCase,
|
||||||
closeUseCase: widget.bootstrap.closeWorkoutSessionUseCase,
|
closeUseCase: widget.bootstrap.closeWorkoutSessionUseCase,
|
||||||
mediaUseCases: widget.bootstrap.mediaUseCases,
|
mediaUseCases: widget.bootstrap.mediaUseCases,
|
||||||
),
|
),
|
||||||
@ -231,6 +235,8 @@ final class _HomeScreenState extends State<HomeScreen> with RouteAware {
|
|||||||
closeUseCase: widget.bootstrap.closeWorkoutSessionUseCase,
|
closeUseCase: widget.bootstrap.closeWorkoutSessionUseCase,
|
||||||
historyUseCases: widget.bootstrap.workoutHistoryUseCases,
|
historyUseCases: widget.bootstrap.workoutHistoryUseCases,
|
||||||
workoutTemplateUseCases: widget.bootstrap.workoutTemplateUseCases,
|
workoutTemplateUseCases: widget.bootstrap.workoutTemplateUseCases,
|
||||||
|
performanceReferenceUseCase:
|
||||||
|
widget.bootstrap.exercisePerformanceReferenceUseCase,
|
||||||
mediaUseCases: widget.bootstrap.mediaUseCases,
|
mediaUseCases: widget.bootstrap.mediaUseCases,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -23,6 +23,7 @@ final class WorkoutExecutionScreen extends StatefulWidget {
|
|||||||
required this.closeUseCase,
|
required this.closeUseCase,
|
||||||
required this.historyUseCases,
|
required this.historyUseCases,
|
||||||
required this.workoutTemplateUseCases,
|
required this.workoutTemplateUseCases,
|
||||||
|
this.performanceReferenceUseCase,
|
||||||
this.mediaUseCases,
|
this.mediaUseCases,
|
||||||
this.mediaAssetLoader,
|
this.mediaAssetLoader,
|
||||||
this.videoMediaBuilder,
|
this.videoMediaBuilder,
|
||||||
@ -37,6 +38,7 @@ final class WorkoutExecutionScreen extends StatefulWidget {
|
|||||||
final CloseWorkoutSessionUseCase closeUseCase;
|
final CloseWorkoutSessionUseCase closeUseCase;
|
||||||
final WorkoutHistoryUseCases historyUseCases;
|
final WorkoutHistoryUseCases historyUseCases;
|
||||||
final WorkoutTemplateUseCases workoutTemplateUseCases;
|
final WorkoutTemplateUseCases workoutTemplateUseCases;
|
||||||
|
final ExercisePerformanceReferenceUseCase? performanceReferenceUseCase;
|
||||||
final MediaUseCases? mediaUseCases;
|
final MediaUseCases? mediaUseCases;
|
||||||
final Future<MediaAsset?> Function(String id)? mediaAssetLoader;
|
final Future<MediaAsset?> Function(String id)? mediaAssetLoader;
|
||||||
final VideoMediaBuilder? videoMediaBuilder;
|
final VideoMediaBuilder? videoMediaBuilder;
|
||||||
@ -71,6 +73,7 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
|||||||
var _lastCountdownBeepSecond = -1;
|
var _lastCountdownBeepSecond = -1;
|
||||||
var _advancingStep = false;
|
var _advancingStep = false;
|
||||||
var _reps = 0;
|
var _reps = 0;
|
||||||
|
Future<ExercisePerformanceReference>? _performanceReference;
|
||||||
final _scoreController = TextEditingController();
|
final _scoreController = TextEditingController();
|
||||||
var _remainingRestSeconds = 0;
|
var _remainingRestSeconds = 0;
|
||||||
|
|
||||||
@ -98,6 +101,7 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
|||||||
? WorkoutExecutionMode.active
|
? WorkoutExecutionMode.active
|
||||||
: WorkoutExecutionMode.paused;
|
: WorkoutExecutionMode.paused;
|
||||||
_reps = _initialRepsFor(_exercise);
|
_reps = _initialRepsFor(_exercise);
|
||||||
|
_performanceReference = _loadPerformanceReference();
|
||||||
_ticker = Timer.periodic(const Duration(seconds: 1), (_) {
|
_ticker = Timer.periodic(const Duration(seconds: 1), (_) {
|
||||||
if (mounted) setState(() {});
|
if (mounted) setState(() {});
|
||||||
});
|
});
|
||||||
@ -181,8 +185,7 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
|||||||
totalSets: _exercise.setsCount,
|
totalSets: _exercise.setsCount,
|
||||||
progressLabel: _plan.progressLabel(_position),
|
progressLabel: _plan.progressLabel(_position),
|
||||||
setTimerLabel: _setTimerLabel,
|
setTimerLabel: _setTimerLabel,
|
||||||
setTimerComplete:
|
setTimerComplete: _setTimer?.status == ActiveSetTimerStatus.stopped,
|
||||||
_setTimer?.status == ActiveSetTimerStatus.stopped,
|
|
||||||
showStartButton: _shouldShowStartExerciseButton,
|
showStartButton: _shouldShowStartExerciseButton,
|
||||||
onStartExercise: _startExercise,
|
onStartExercise: _startExercise,
|
||||||
onOpenMedia: _exercise.hasMedia
|
onOpenMedia: _exercise.hasMedia
|
||||||
@ -197,6 +200,17 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (_performanceReference != null) ...[
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
|
||||||
|
child: PerformanceReferenceCard(
|
||||||
|
reference: _performanceReference!,
|
||||||
|
activeMeasures: _exercise.activePerformanceMeasures,
|
||||||
|
scoreUnit: _exercise.scoreUnit,
|
||||||
|
usesStepPassages: _exercise.steps.isNotEmpty,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
|
padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
|
||||||
@ -1442,6 +1456,10 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
|||||||
_scoreStopwatchLoadGeneration++;
|
_scoreStopwatchLoadGeneration++;
|
||||||
_manualScoreTimeMs = null;
|
_manualScoreTimeMs = null;
|
||||||
_stepProgress = null;
|
_stepProgress = null;
|
||||||
|
_performanceReference = _loadPerformanceReferenceFor(
|
||||||
|
_plan.exerciseAt(position),
|
||||||
|
position.setIndex,
|
||||||
|
);
|
||||||
_resetStepScoreInput();
|
_resetStepScoreInput();
|
||||||
_refreshScoreStopwatchTicker();
|
_refreshScoreStopwatchTicker();
|
||||||
_refreshStepTicker();
|
_refreshStepTicker();
|
||||||
@ -1517,6 +1535,7 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
|||||||
closeUseCase: widget.closeUseCase,
|
closeUseCase: widget.closeUseCase,
|
||||||
historyUseCases: widget.historyUseCases,
|
historyUseCases: widget.historyUseCases,
|
||||||
workoutTemplateUseCases: widget.workoutTemplateUseCases,
|
workoutTemplateUseCases: widget.workoutTemplateUseCases,
|
||||||
|
performanceReferenceUseCase: widget.performanceReferenceUseCase,
|
||||||
mediaUseCases: widget.mediaUseCases,
|
mediaUseCases: widget.mediaUseCases,
|
||||||
mediaAssetLoader: widget.mediaAssetLoader,
|
mediaAssetLoader: widget.mediaAssetLoader,
|
||||||
videoMediaBuilder: widget.videoMediaBuilder,
|
videoMediaBuilder: widget.videoMediaBuilder,
|
||||||
@ -1529,6 +1548,181 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
|||||||
if (!exercise.repsEnabled) return 0;
|
if (!exercise.repsEnabled) return 0;
|
||||||
return exercise.targetReps ?? 0;
|
return exercise.targetReps ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<ExercisePerformanceReference>? _loadPerformanceReference() {
|
||||||
|
return _loadPerformanceReferenceFor(_exercise, _position.setIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ExercisePerformanceReference>? _loadPerformanceReferenceFor(
|
||||||
|
ExecutionExercise exercise,
|
||||||
|
int setIndex,
|
||||||
|
) {
|
||||||
|
final useCase = widget.performanceReferenceUseCase;
|
||||||
|
final sourceExerciseId = exercise.sourceExerciseId;
|
||||||
|
if (useCase == null || sourceExerciseId == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return useCase.getExercisePerformanceReference(
|
||||||
|
exerciseId: sourceExerciseId,
|
||||||
|
activeMeasures: exercise.activePerformanceMeasures,
|
||||||
|
currentSetIndex: setIndex,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class PerformanceReferenceCard extends StatelessWidget {
|
||||||
|
const PerformanceReferenceCard({
|
||||||
|
required this.reference,
|
||||||
|
required this.activeMeasures,
|
||||||
|
this.scoreUnit,
|
||||||
|
this.usesStepPassages = false,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Future<ExercisePerformanceReference> reference;
|
||||||
|
final ActivePerformanceMeasures activeMeasures;
|
||||||
|
final String? scoreUnit;
|
||||||
|
final bool usesStepPassages;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return FutureBuilder<ExercisePerformanceReference>(
|
||||||
|
future: reference,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.connectionState != ConnectionState.done) {
|
||||||
|
return const _PerformanceReferenceShell(
|
||||||
|
child: Center(child: Text('Chargement…')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (snapshot.hasError) {
|
||||||
|
return const _PerformanceReferenceShell(
|
||||||
|
child: Center(child: Text('Références indisponibles')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final reference = snapshot.data;
|
||||||
|
if (reference == null || !activeMeasures.hasAny) {
|
||||||
|
return const _PerformanceReferenceShell(
|
||||||
|
child: Center(child: Text('Aucune référence pour ces mesures')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!reference.hasAnyHistoryForExercise) {
|
||||||
|
return const _PerformanceReferenceShell(
|
||||||
|
child: Center(child: Text('Première fois sur cet exercice')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final lastLabel = _formatSetPerformance(
|
||||||
|
reference.last,
|
||||||
|
activeMeasures,
|
||||||
|
scoreUnit: scoreUnit,
|
||||||
|
usesStepPassages: usesStepPassages,
|
||||||
|
);
|
||||||
|
final recordLabel = _formatMetricPerformance(
|
||||||
|
reference.record,
|
||||||
|
scoreUnit: scoreUnit,
|
||||||
|
usesStepPassages: usesStepPassages,
|
||||||
|
);
|
||||||
|
if (lastLabel == null && recordLabel == null) {
|
||||||
|
return const _PerformanceReferenceShell(
|
||||||
|
child: Center(child: Text('Aucune référence pour ces mesures')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return _PerformanceReferenceShell(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: _PerformanceReferenceColumn(
|
||||||
|
title: 'Dernière',
|
||||||
|
value: lastLabel ?? '—',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: _PerformanceReferenceColumn(
|
||||||
|
title: _recordTitle(reference.record),
|
||||||
|
value: recordLabel ?? '—',
|
||||||
|
alignEnd: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class _PerformanceReferenceShell extends StatelessWidget {
|
||||||
|
const _PerformanceReferenceShell({required this.child});
|
||||||
|
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final tokens = courtBlazerTokensOf(context);
|
||||||
|
return Container(
|
||||||
|
constraints: const BoxConstraints(minHeight: 64),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.colorScheme.surface,
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
border: Border.all(color: tokens.border),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Références',
|
||||||
|
style: theme.textTheme.labelSmall?.copyWith(
|
||||||
|
color: tokens.mutedText,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
child,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class _PerformanceReferenceColumn extends StatelessWidget {
|
||||||
|
const _PerformanceReferenceColumn({
|
||||||
|
required this.title,
|
||||||
|
required this.value,
|
||||||
|
this.alignEnd = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
final String value;
|
||||||
|
final bool alignEnd;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final tokens = courtBlazerTokensOf(context);
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: alignEnd
|
||||||
|
? CrossAxisAlignment.end
|
||||||
|
: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: theme.textTheme.labelSmall?.copyWith(color: tokens.mutedText),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(
|
||||||
|
value,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
textAlign: alignEnd ? TextAlign.end : TextAlign.start,
|
||||||
|
style: theme.textTheme.titleSmall,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final class SetMeasureInput extends StatelessWidget {
|
final class SetMeasureInput extends StatelessWidget {
|
||||||
@ -1726,10 +1920,7 @@ final class _ExecutionContextHeader extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final class _SetActionBar extends StatelessWidget {
|
final class _SetActionBar extends StatelessWidget {
|
||||||
const _SetActionBar({
|
const _SetActionBar({required this.onFinishSet, required this.onSkipSet});
|
||||||
required this.onFinishSet,
|
|
||||||
required this.onSkipSet,
|
|
||||||
});
|
|
||||||
|
|
||||||
final VoidCallback onFinishSet;
|
final VoidCallback onFinishSet;
|
||||||
final VoidCallback onSkipSet;
|
final VoidCallback onSkipSet;
|
||||||
@ -1892,10 +2083,7 @@ final class _StepSequencePanel extends StatelessWidget {
|
|||||||
style: Theme.of(context).textTheme.titleMedium,
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text('SÉQUENCE', style: Theme.of(context).textTheme.labelMedium),
|
||||||
'SÉQUENCE',
|
|
||||||
style: Theme.of(context).textTheme.labelMedium,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
@ -1972,7 +2160,9 @@ final class _BoundedAccentPanel extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Container(height: 2, color: tokens.accent),
|
Container(height: 2, color: tokens.accent),
|
||||||
Expanded(child: Padding(padding: padding, child: child)),
|
Expanded(
|
||||||
|
child: Padding(padding: padding, child: child),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -3790,6 +3980,7 @@ final class ExecutionProgram {
|
|||||||
final class ExecutionExercise {
|
final class ExecutionExercise {
|
||||||
const ExecutionExercise({
|
const ExecutionExercise({
|
||||||
required this.id,
|
required this.id,
|
||||||
|
this.sourceExerciseId,
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.setsCount,
|
required this.setsCount,
|
||||||
required this.timeEnabled,
|
required this.timeEnabled,
|
||||||
@ -3814,6 +4005,7 @@ final class ExecutionExercise {
|
|||||||
) {
|
) {
|
||||||
return ExecutionExercise(
|
return ExecutionExercise(
|
||||||
id: exercise['id'] as String,
|
id: exercise['id'] as String,
|
||||||
|
sourceExerciseId: exercise['sourceExerciseId'] as String?,
|
||||||
name: exercise['exerciseNameSnapshot'] as String,
|
name: exercise['exerciseNameSnapshot'] as String,
|
||||||
setsCount:
|
setsCount:
|
||||||
(override?['setsCountOverride'] as int?) ??
|
(override?['setsCountOverride'] as int?) ??
|
||||||
@ -3848,6 +4040,7 @@ final class ExecutionExercise {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final String id;
|
final String id;
|
||||||
|
final String? sourceExerciseId;
|
||||||
final String name;
|
final String name;
|
||||||
final int setsCount;
|
final int setsCount;
|
||||||
final bool timeEnabled;
|
final bool timeEnabled;
|
||||||
@ -3876,6 +4069,133 @@ final class ExecutionExercise {
|
|||||||
bool get hasMedia {
|
bool get hasMedia {
|
||||||
return imageMediaIds.isNotEmpty || videoMediaId != null;
|
return imageMediaIds.isNotEmpty || videoMediaId != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ActivePerformanceMeasures get activePerformanceMeasures {
|
||||||
|
return ActivePerformanceMeasures(
|
||||||
|
timeEnabled: timeEnabled,
|
||||||
|
repsEnabled: repsEnabled,
|
||||||
|
scoreEnabled: scoreEnabled,
|
||||||
|
scoreInputMode: scoreInputMode,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _formatSetPerformance(
|
||||||
|
WorkoutHistorySetPerformance? performance,
|
||||||
|
ActivePerformanceMeasures measures, {
|
||||||
|
required String? scoreUnit,
|
||||||
|
required bool usesStepPassages,
|
||||||
|
}) {
|
||||||
|
if (performance == null) return null;
|
||||||
|
final values = [
|
||||||
|
if (measures.timeEnabled && performance.actualTimeMs != null)
|
||||||
|
_formatReferenceTime(performance.actualTimeMs!, stopwatch: false),
|
||||||
|
if (measures.repsEnabled && performance.actualReps != null)
|
||||||
|
_formatReferenceReps(
|
||||||
|
performance.actualReps!,
|
||||||
|
usesStepPassages: usesStepPassages,
|
||||||
|
),
|
||||||
|
if (measures.scoreEnabled)
|
||||||
|
_formatReferenceScore(
|
||||||
|
actualScore: performance.actualScore,
|
||||||
|
actualScoreTimeMs: performance.actualScoreTimeMs,
|
||||||
|
scoreInputMode: measures.scoreInputMode,
|
||||||
|
scoreUnit: scoreUnit,
|
||||||
|
),
|
||||||
|
].whereType<String>().toList();
|
||||||
|
if (values.isEmpty) return null;
|
||||||
|
return values.join(' · ');
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _formatMetricPerformance(
|
||||||
|
WorkoutHistoryMetricPerformance? performance, {
|
||||||
|
required String? scoreUnit,
|
||||||
|
required bool usesStepPassages,
|
||||||
|
}) {
|
||||||
|
if (performance == null) return null;
|
||||||
|
return switch (performance.metric) {
|
||||||
|
PerformanceMetric.time =>
|
||||||
|
performance.actualTimeMs == null
|
||||||
|
? null
|
||||||
|
: _formatReferenceTime(performance.actualTimeMs!, stopwatch: false),
|
||||||
|
PerformanceMetric.reps =>
|
||||||
|
performance.actualReps == null
|
||||||
|
? null
|
||||||
|
: _formatReferenceReps(
|
||||||
|
performance.actualReps!,
|
||||||
|
usesStepPassages: usesStepPassages,
|
||||||
|
),
|
||||||
|
PerformanceMetric.score =>
|
||||||
|
performance.scoreInputModeSnapshot == ScoreInputMode.stopwatch
|
||||||
|
? performance.actualScoreTimeMs == null
|
||||||
|
? null
|
||||||
|
: _formatReferenceTime(
|
||||||
|
performance.actualScoreTimeMs!,
|
||||||
|
stopwatch: true,
|
||||||
|
)
|
||||||
|
: performance.actualScore == null
|
||||||
|
? null
|
||||||
|
: _formatReferenceManualScore(performance.actualScore!, scoreUnit),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
String _recordTitle(WorkoutHistoryMetricPerformance? performance) {
|
||||||
|
return switch (performance?.metric) {
|
||||||
|
PerformanceMetric.time => 'Max',
|
||||||
|
PerformanceMetric.score
|
||||||
|
when performance?.scoreInputModeSnapshot == ScoreInputMode.stopwatch =>
|
||||||
|
'Meilleur temps',
|
||||||
|
_ => 'Record',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatReferenceReps(int reps, {required bool usesStepPassages}) {
|
||||||
|
return '$reps ${usesStepPassages ? 'passages' : 'reps'}';
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _formatReferenceScore({
|
||||||
|
required double? actualScore,
|
||||||
|
required int? actualScoreTimeMs,
|
||||||
|
required ScoreInputMode scoreInputMode,
|
||||||
|
required String? scoreUnit,
|
||||||
|
}) {
|
||||||
|
if (scoreInputMode == ScoreInputMode.stopwatch) {
|
||||||
|
return actualScoreTimeMs == null
|
||||||
|
? null
|
||||||
|
: _formatReferenceTime(actualScoreTimeMs, stopwatch: true);
|
||||||
|
}
|
||||||
|
return actualScore == null
|
||||||
|
? null
|
||||||
|
: _formatReferenceManualScore(actualScore, scoreUnit);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatReferenceManualScore(double score, String? scoreUnit) {
|
||||||
|
final value = score == score.roundToDouble()
|
||||||
|
? score.toInt().toString()
|
||||||
|
: score.toStringAsFixed(1);
|
||||||
|
final unit = scoreUnit?.trim();
|
||||||
|
if (unit == null || unit.isEmpty) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return '$value $unit';
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatReferenceTime(int milliseconds, {required bool stopwatch}) {
|
||||||
|
final totalTenths = (milliseconds / 100).round();
|
||||||
|
final minutes = totalTenths ~/ 600;
|
||||||
|
final seconds = (totalTenths ~/ 10) % 60;
|
||||||
|
final tenths = totalTenths % 10;
|
||||||
|
if (minutes == 0) {
|
||||||
|
if (!stopwatch && tenths == 0) {
|
||||||
|
return '$seconds s';
|
||||||
|
}
|
||||||
|
return '${(seconds + tenths / 10).toStringAsFixed(1)} s';
|
||||||
|
}
|
||||||
|
final paddedSeconds = seconds.toString().padLeft(2, '0');
|
||||||
|
if (stopwatch) {
|
||||||
|
return '$minutes:$paddedSeconds.$tenths';
|
||||||
|
}
|
||||||
|
return '$minutes:$paddedSeconds';
|
||||||
}
|
}
|
||||||
|
|
||||||
List<String> _imageMediaIdsFromSnapshot(Map<String, dynamic> exercise) {
|
List<String> _imageMediaIdsFromSnapshot(Map<String, dynamic> exercise) {
|
||||||
|
|||||||
@ -17,6 +17,7 @@ final class WorkoutTemplateListScreen extends StatefulWidget {
|
|||||||
required this.historyUseCases,
|
required this.historyUseCases,
|
||||||
this.mediaUseCases,
|
this.mediaUseCases,
|
||||||
this.stepUseCases,
|
this.stepUseCases,
|
||||||
|
this.performanceReferenceUseCase,
|
||||||
this.shareUseCases,
|
this.shareUseCases,
|
||||||
this.authUseCases,
|
this.authUseCases,
|
||||||
this.syncUseCases,
|
this.syncUseCases,
|
||||||
@ -27,6 +28,7 @@ final class WorkoutTemplateListScreen extends StatefulWidget {
|
|||||||
final ProgramUseCases programUseCases;
|
final ProgramUseCases programUseCases;
|
||||||
final ActiveWorkoutSessionUseCases activeUseCases;
|
final ActiveWorkoutSessionUseCases activeUseCases;
|
||||||
final ActiveExerciseStepUseCases? stepUseCases;
|
final ActiveExerciseStepUseCases? stepUseCases;
|
||||||
|
final ExercisePerformanceReferenceUseCase? performanceReferenceUseCase;
|
||||||
final CloseWorkoutSessionUseCase closeUseCase;
|
final CloseWorkoutSessionUseCase closeUseCase;
|
||||||
final WorkoutHistoryUseCases historyUseCases;
|
final WorkoutHistoryUseCases historyUseCases;
|
||||||
final MediaUseCases? mediaUseCases;
|
final MediaUseCases? mediaUseCases;
|
||||||
@ -166,6 +168,7 @@ final class _WorkoutTemplateListScreenState
|
|||||||
closeUseCase: widget.closeUseCase,
|
closeUseCase: widget.closeUseCase,
|
||||||
historyUseCases: widget.historyUseCases,
|
historyUseCases: widget.historyUseCases,
|
||||||
workoutTemplateUseCases: widget.workoutTemplateUseCases,
|
workoutTemplateUseCases: widget.workoutTemplateUseCases,
|
||||||
|
performanceReferenceUseCase: widget.performanceReferenceUseCase,
|
||||||
mediaUseCases: widget.mediaUseCases,
|
mediaUseCases: widget.mediaUseCases,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user