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:
@ -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 ||
|
||||
|
||||
Reference in New Issue
Block a user