chore(wip): consolidation intermédiaire multi-tickets (sprints Statistiques, UI, Bug resolution, Serveur-client)

Regroupe l'état de travail en cours réalisé dans un même worktree sur
plusieurs tickets/sprints (#85, #136, #145, #155-160, #162-164),
mélangeant des tickets QA et inProgress. Ne constitue pas une feature
terminée : commit de sauvegarde avant triage/split par ticket en
branches feature/* dédiées. Exclut les dossiers d'environnement de
build locaux et le heap dump parasite (.gitignore mis à jour).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-28 16:48:54 +02:00
parent 58272e354a
commit 917777e18b
279 changed files with 13546 additions and 674 deletions

View File

@ -1634,7 +1634,13 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
final rest = await widget.activeUseCases.findActiveRest(
sessionId: _session.metadata.id,
);
if (!mounted || rest == null) {
if (!mounted) {
return false;
}
if (rest == null) {
_activeRestStateId = null;
_restTicker?.cancel();
_restTicker = null;
return false;
}
final after = ExecutionPosition(
@ -2706,6 +2712,96 @@ final class _CurrentStepPane extends StatelessWidget {
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final hasRepsWithStopwatchScore =
step.type == ExerciseStepType.reps &&
step.hasScore &&
step.scoreInputMode == ScoreInputMode.stopwatch;
final hasTimedWithManualScore =
step.type == ExerciseStepType.time &&
step.hasScore &&
step.scoreInputMode == ScoreInputMode.manual;
if (hasRepsWithStopwatchScore || hasTimedWithManualScore) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
step.name,
style: Theme.of(context).textTheme.headlineSmall,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 8),
Expanded(
child: LayoutBuilder(
builder: (context, bodyConstraints) {
return FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.center,
child: SizedBox(
width: bodyConstraints.maxWidth,
height: hasRepsWithStopwatchScore ? 150 : 132,
child: hasRepsWithStopwatchScore
? _RepsWithStopwatchScoreStepBody(
step: step,
elapsedLabel: stepScoreElapsedLabel,
running: stepScoreRunning,
onStart: onStartStepScore,
onStop: onStopStepScore,
onReset: onResetStepScore,
)
: _TimedStepWithManualScoreBody(
step: step,
remainingLabel: remainingLabel,
running:
view.state.status ==
ActiveExerciseStepProgressStatus
.runningTimer,
readyToStart: _isNextTimedStepReady(view),
scoreController: stepScoreController,
onStartTimer: onStartTimer,
),
),
);
},
),
),
const SizedBox(height: 8),
if (hasRepsWithStopwatchScore)
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: onSkipStep,
style: OutlinedButton.styleFrom(
minimumSize: const Size.fromHeight(44),
),
child: const Text('Passer létape'),
),
),
const SizedBox(width: 8),
Expanded(
child: FilledButton.icon(
onPressed: onCompleteStep,
style: FilledButton.styleFrom(
minimumSize: const Size.fromHeight(44),
),
icon: const Icon(Icons.check),
label: const Text('Étape suivante'),
),
),
],
)
else
OutlinedButton(
onPressed: onSkipStep,
style: OutlinedButton.styleFrom(
minimumSize: const Size.fromHeight(44),
),
child: const Text('Passer létape'),
),
],
);
}
return SingleChildScrollView(
padding: const EdgeInsets.only(bottom: 8),
child: ConstrainedBox(
@ -2860,6 +2956,274 @@ final class _TimedStepBody extends StatelessWidget {
}
}
final class _RepsWithStopwatchScoreStepBody extends StatelessWidget {
const _RepsWithStopwatchScoreStepBody({
required this.step,
required this.elapsedLabel,
required this.running,
required this.onStart,
required this.onStop,
required this.onReset,
});
final ExerciseStep step;
final String elapsedLabel;
final bool running;
final VoidCallback onStart;
final VoidCallback onStop;
final VoidCallback onReset;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('RÉPÉTITIONS', style: theme.textTheme.labelLarge),
const SizedBox(height: 4),
FittedBox(
fit: BoxFit.scaleDown,
child: Text(
'${step.defaultTargetValue}',
style: AppTextStyles.timer(
context,
).copyWith(color: theme.colorScheme.primary),
),
),
],
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('CHRONO SCORE', style: theme.textTheme.labelLarge),
const SizedBox(height: 6),
FittedBox(
fit: BoxFit.scaleDown,
child: Text(
elapsedLabel,
style: AppTextStyles.scoreNumber(context),
),
),
const SizedBox(height: 8),
_CompactStepScoreStopwatchControls(
running: running,
onStart: onStart,
onStop: onStop,
onReset: onReset,
),
],
),
),
],
);
}
}
final class _TimedStepWithManualScoreBody extends StatelessWidget {
const _TimedStepWithManualScoreBody({
required this.step,
required this.remainingLabel,
required this.running,
required this.readyToStart,
required this.scoreController,
required this.onStartTimer,
});
final ExerciseStep step;
final String remainingLabel;
final bool running;
final bool readyToStart;
final TextEditingController scoreController;
final VoidCallback onStartTimer;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
flex: 3,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('CHRONO', style: theme.textTheme.labelLarge),
const SizedBox(height: 4),
FittedBox(
fit: BoxFit.scaleDown,
child: Text(
remainingLabel,
style: theme.textTheme.displayMedium?.copyWith(
fontFamily: 'Anton',
fontFeatures: AppTextStyles.timer(context).fontFeatures,
color: theme.colorScheme.primary,
),
),
),
if (!running) ...[
const SizedBox(height: 8),
if (readyToStart) ...[
Text(
'Chrono prêt',
textAlign: TextAlign.center,
style: theme.textTheme.titleSmall?.copyWith(
color: theme.colorScheme.primary,
),
),
const SizedBox(height: 4),
],
IconButton.filled(
tooltip: 'Lancer',
onPressed: onStartTimer,
icon: const Icon(Icons.play_arrow),
),
],
],
),
),
const SizedBox(width: 14),
Expanded(
flex: 2,
child: Center(
child: _StepManualScoreStepper(
controller: scoreController,
initialValue: step.defaultTargetScore,
),
),
),
],
);
}
}
final class _CompactStepScoreStopwatchControls extends StatelessWidget {
const _CompactStepScoreStopwatchControls({
required this.running,
required this.onStart,
required this.onStop,
required this.onReset,
});
final bool running;
final VoidCallback onStart;
final VoidCallback onStop;
final VoidCallback onReset;
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
IconButton.filled(
tooltip: running ? 'Arrêter' : 'Lancer',
onPressed: running ? onStop : onStart,
icon: Icon(running ? Icons.stop : Icons.play_arrow),
),
const SizedBox(width: 8),
IconButton.outlined(
tooltip: 'Réinitialiser',
onPressed: onReset,
icon: const Icon(Icons.restart_alt),
),
],
);
}
}
final class _StepManualScoreStepper extends StatefulWidget {
const _StepManualScoreStepper({
required this.controller,
required this.initialValue,
});
final TextEditingController controller;
final double? initialValue;
@override
State<_StepManualScoreStepper> createState() =>
_StepManualScoreStepperState();
}
final class _StepManualScoreStepperState
extends State<_StepManualScoreStepper> {
double get _value {
final parsed = double.tryParse(widget.controller.text.trim());
return parsed ?? widget.initialValue ?? 0;
}
void _changeBy(double delta) {
final next = (_value + delta).clamp(0, 999).toDouble();
widget.controller.text = _formatManualScoreInput(next);
setState(() {});
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('SCORE', style: theme.textTheme.labelLarge),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
tooltip: 'Diminuer',
onPressed: () => _changeBy(-1),
constraints: const BoxConstraints.tightFor(width: 34, height: 34),
padding: EdgeInsets.zero,
iconSize: 20,
visualDensity: VisualDensity.compact,
style: IconButton.styleFrom(
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
icon: const Icon(Icons.remove),
),
SizedBox(
width: 46,
child: Text(
_formatManualScoreInput(_value),
textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: AppTextStyles.scoreNumber(context),
),
),
IconButton(
tooltip: 'Augmenter',
onPressed: () => _changeBy(1),
constraints: const BoxConstraints.tightFor(width: 34, height: 34),
padding: EdgeInsets.zero,
iconSize: 20,
visualDensity: VisualDensity.compact,
style: IconButton.styleFrom(
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
icon: const Icon(Icons.add),
),
],
),
],
);
}
}
String _formatManualScoreInput(double value) {
return value == value.roundToDouble()
? value.toInt().toString()
: value.toStringAsFixed(1);
}
bool _isNextTimedStepReady(ActiveExerciseStepProgressView view) {
final step = view.currentStep;
if (step == null ||