merge(develop): clôture lot #91 Wear OS - fréquence cardiaque, notifications, finitions montre

Intègre le commit de clôture du lot #91 resté non fusionné. Conflits
attendus sur entities.dart, workout_execution_screen.dart et
watch_companion_projection_test.dart : arbitrage Architect - garder la
base feature/ticket91-wear-os-watch-sync sur ces 3 fichiers puis réinjecter
uniquement les invariants chronos utiles (Durée / Temps réalisé).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

# Conflicts:
#	lib/domain/entities.dart
#	lib/presentation/workout_execution_screen.dart
#	test/application/watch_companion_projection_test.dart
This commit is contained in:
2026-07-28 11:35:22 +02:00
80 changed files with 12296 additions and 891 deletions

View File

@ -381,6 +381,11 @@ final class Exercise {
targetScore: defaultTargetScore,
targetScoreTimeMs: defaultTargetScoreTimeMs,
);
_requireLinkedStepSeriesScoreShape(
steps: steps,
scoreEnabled: hasScoreMeasure,
scoreInputMode: scoreInputMode,
);
}
final EntityMetadata metadata;
@ -514,6 +519,7 @@ final class ExerciseStep {
this.scoreUnit,
this.defaultTargetScore,
this.defaultTargetScoreTimeMs,
this.linkedToSeriesScore = false,
}) : id = _nonBlank(id, 'Exercise step id'),
name = _nonBlank(name, 'Exercise step name') {
_requireNonNegative(position, 'Exercise step position');
@ -525,6 +531,7 @@ final class ExerciseStep {
scoreUnit: scoreUnit,
defaultTargetScore: defaultTargetScore,
defaultTargetScoreTimeMs: defaultTargetScoreTimeMs,
linkedToSeriesScore: linkedToSeriesScore,
);
}
@ -539,6 +546,7 @@ final class ExerciseStep {
final String? scoreUnit;
final double? defaultTargetScore;
final int? defaultTargetScoreTimeMs;
final bool linkedToSeriesScore;
Map<String, Object?> toSnapshotJson() => {
'id': id,
@ -552,6 +560,7 @@ final class ExerciseStep {
'scoreUnit': scoreUnit,
'defaultTargetScore': defaultTargetScore,
'defaultTargetScoreTimeMs': defaultTargetScoreTimeMs,
'linkedToSeriesScore': linkedToSeriesScore,
};
}
@ -664,6 +673,11 @@ final class ProgramExercise {
targetScore: targetScore,
targetScoreTimeMs: targetScoreTimeMs,
);
_requireLinkedStepSeriesScoreShape(
steps: exerciseStepsSnapshot,
scoreEnabled: scoreEnabled,
scoreInputMode: scoreInputModeSnapshot,
);
}
final EntityMetadata metadata;
@ -1106,6 +1120,47 @@ final class ActiveScoreStopwatchState {
}
}
final class ActiveManualScoreState {
ActiveManualScoreState({
required this.metadata,
required this.activeWorkoutSessionId,
required this.programIndex,
required this.exerciseIndex,
required this.setIndex,
required this.value,
required this.updatedAt,
}) {
_requireNonNegative(programIndex, 'Program index');
_requireNonNegative(exerciseIndex, 'Exercise index');
_requireNonNegative(setIndex, 'Set index');
_requireNullableNonNegativeDouble(value, 'Manual score value');
}
final EntityMetadata metadata;
final String activeWorkoutSessionId;
final int programIndex;
final int exerciseIndex;
final int setIndex;
final double value;
final DateTime updatedAt;
ActiveManualScoreState copyWith({
EntityMetadata? metadata,
double? value,
DateTime? updatedAt,
}) {
return ActiveManualScoreState(
metadata: metadata ?? this.metadata,
activeWorkoutSessionId: activeWorkoutSessionId,
programIndex: programIndex,
exerciseIndex: exerciseIndex,
setIndex: setIndex,
value: value ?? this.value,
updatedAt: updatedAt ?? this.updatedAt,
);
}
}
final class ActiveSetTimerState {
ActiveSetTimerState({
required this.metadata,
@ -1411,7 +1466,7 @@ final class ActiveExerciseStepResult {
}
final class WorkoutHistory {
const WorkoutHistory({
WorkoutHistory({
required this.metadata,
this.sourceWorkoutTemplateId,
this.sourceActiveWorkoutSessionId,
@ -1421,9 +1476,17 @@ final class WorkoutHistory {
required this.totalActiveMs,
required this.completed,
required this.historySnapshotJson,
this.averageHeartRateBpm,
this.maxHeartRateBpm,
this.results = const [],
this.stepResults = const [],
});
}) {
_requireNullablePositiveDouble(
averageHeartRateBpm,
'Average heart rate bpm',
);
_requireNullablePositive(maxHeartRateBpm, 'Max heart rate bpm');
}
final EntityMetadata metadata;
final String? sourceWorkoutTemplateId;
@ -1434,8 +1497,36 @@ final class WorkoutHistory {
final int totalActiveMs;
final bool completed;
final String historySnapshotJson;
final double? averageHeartRateBpm;
final int? maxHeartRateBpm;
final List<WorkoutHistorySetResult> results;
final List<WorkoutHistoryStepResult> stepResults;
WorkoutHistory copyWith({
EntityMetadata? metadata,
Object? averageHeartRateBpm = _unchanged,
Object? maxHeartRateBpm = _unchanged,
}) {
return WorkoutHistory(
metadata: metadata ?? this.metadata,
sourceWorkoutTemplateId: sourceWorkoutTemplateId,
sourceActiveWorkoutSessionId: sourceActiveWorkoutSessionId,
nameSnapshot: nameSnapshot,
startedAt: startedAt,
endedAt: endedAt,
totalActiveMs: totalActiveMs,
completed: completed,
historySnapshotJson: historySnapshotJson,
averageHeartRateBpm: averageHeartRateBpm == _unchanged
? this.averageHeartRateBpm
: averageHeartRateBpm as double?,
maxHeartRateBpm: maxHeartRateBpm == _unchanged
? this.maxHeartRateBpm
: maxHeartRateBpm as int?,
results: results,
stepResults: stepResults,
);
}
}
final class WorkoutHistorySetResult {
@ -1727,6 +1818,12 @@ void _requireNullableNonNegativeDouble(double? value, String label) {
}
}
void _requireNullablePositiveDouble(double? value, String label) {
if (value != null && value <= 0) {
throw DomainException('$label must be positive.');
}
}
void _requireScoreTargetShape({
required bool scoreEnabled,
required ScoreInputMode scoreInputMode,
@ -1759,12 +1856,14 @@ void _validateExerciseStepScoreShape({
required String? scoreUnit,
required double? defaultTargetScore,
required int? defaultTargetScoreTimeMs,
required bool linkedToSeriesScore,
}) {
if (!hasScore) {
if (scoreLabel != null ||
scoreUnit != null ||
defaultTargetScore != null ||
defaultTargetScoreTimeMs != null) {
defaultTargetScoreTimeMs != null ||
linkedToSeriesScore) {
throw const DomainException(
'Disabled exercise step score must not define score values.',
);
@ -1790,6 +1889,11 @@ void _validateExerciseStepScoreShape({
);
}
case ScoreInputMode.stopwatch:
if (linkedToSeriesScore) {
throw const DomainException(
'Linked series score requires manual exercise step score.',
);
}
if (scoreLabel != null ||
scoreUnit != null ||
defaultTargetScore != null) {
@ -1826,6 +1930,19 @@ void _requireScoreResultShape({
}
}
void _requireLinkedStepSeriesScoreShape({
required List<ExerciseStep> steps,
required bool scoreEnabled,
required ScoreInputMode scoreInputMode,
}) {
if (steps.any((step) => step.linkedToSeriesScore) &&
(!scoreEnabled || scoreInputMode != ScoreInputMode.manual)) {
throw const DomainException(
'Linked step scores require manual series score.',
);
}
}
void _validateExerciseStepResult({
required int programIndex,
required int exerciseIndex,