feat(core): consolide modele donnees stats par scope, types metier d'exercice et correctif contrainte active_exercise_step_progress_states
Couche domaine/application/infra locale partagee par #179 (stockage stats par etape/serie/exercice/seance), #182 (correctif contrainte UNIQUE active_exercise_step_progress_states) et #183 (modele/persistance types metier d'exercice). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -21,6 +21,18 @@ enum ExerciseCategory {
|
||||
uncategorized,
|
||||
}
|
||||
|
||||
enum BusinessExerciseType {
|
||||
shoot,
|
||||
freeThrows,
|
||||
dribble,
|
||||
finishing,
|
||||
conditioning,
|
||||
defense,
|
||||
mobility,
|
||||
highIntensity,
|
||||
recovery,
|
||||
}
|
||||
|
||||
enum ActiveWorkoutStatus { running, paused, savedExit, completed, abandoned }
|
||||
|
||||
enum SetResultStatus { completed, skipped }
|
||||
@ -333,6 +345,7 @@ final class Exercise {
|
||||
this.defaultTargetScoreTimeMs,
|
||||
this.autoStartNextTimedStep = true,
|
||||
this.category = ExerciseCategory.uncategorized,
|
||||
List<BusinessExerciseType> businessTypes = const [],
|
||||
this.isExample = false,
|
||||
List<String> tags = const [],
|
||||
List<ExerciseStep> steps = const [],
|
||||
@ -347,6 +360,7 @@ final class Exercise {
|
||||
iconMediaId,
|
||||
_resolvedExerciseImageMediaIds(imageMediaId, imageMediaIds),
|
||||
),
|
||||
businessTypes = _validatedBusinessExerciseTypes(businessTypes),
|
||||
steps = _validatedExerciseSteps(steps) {
|
||||
_requireAtLeastOneMeasure(
|
||||
hasTime: hasTimeMeasure,
|
||||
@ -406,6 +420,7 @@ final class Exercise {
|
||||
final int? defaultTargetScoreTimeMs;
|
||||
final bool autoStartNextTimedStep;
|
||||
final ExerciseCategory category;
|
||||
final List<BusinessExerciseType> businessTypes;
|
||||
final bool isExample;
|
||||
final List<String> tags;
|
||||
final List<ExerciseStep> steps;
|
||||
@ -420,6 +435,10 @@ final class Exercise {
|
||||
String? get imageMediaId =>
|
||||
imageMediaIds.isEmpty ? null : imageMediaIds.first;
|
||||
|
||||
List<BusinessExerciseType> get effectiveBusinessTypes => businessTypes.isEmpty
|
||||
? _businessTypesFromCategory(category)
|
||||
: businessTypes;
|
||||
|
||||
Exercise archive(DateTime now) {
|
||||
return copyWith(archivedAt: now, metadata: metadata.touch(now));
|
||||
}
|
||||
@ -444,6 +463,7 @@ final class Exercise {
|
||||
Object? defaultTargetScoreTimeMs = _unchanged,
|
||||
bool? autoStartNextTimedStep,
|
||||
ExerciseCategory? category,
|
||||
Object? businessTypes = _unchanged,
|
||||
bool? isExample,
|
||||
Object? tags = _unchanged,
|
||||
Object? steps = _unchanged,
|
||||
@ -496,6 +516,9 @@ final class Exercise {
|
||||
autoStartNextTimedStep:
|
||||
autoStartNextTimedStep ?? this.autoStartNextTimedStep,
|
||||
category: category ?? this.category,
|
||||
businessTypes: businessTypes == _unchanged
|
||||
? this.businessTypes
|
||||
: businessTypes as List<BusinessExerciseType>,
|
||||
isExample: isExample ?? this.isExample,
|
||||
tags: tags == _unchanged ? this.tags : tags as List<String>,
|
||||
steps: steps == _unchanged ? this.steps : steps as List<ExerciseStep>,
|
||||
@ -1675,6 +1698,68 @@ final class WorkoutTelemetryAggregate {
|
||||
final double? totalCaloriesKcal;
|
||||
}
|
||||
|
||||
final class WorkoutTelemetryGraphPoint {
|
||||
WorkoutTelemetryGraphPoint({
|
||||
required this.capturedAt,
|
||||
required this.elapsedMs,
|
||||
this.heartRateBpm,
|
||||
this.distanceMeters,
|
||||
this.caloriesKcal,
|
||||
required this.startsAfterGap,
|
||||
}) {
|
||||
_requireNonNegative(elapsedMs, 'Telemetry graph elapsed ms');
|
||||
_requireNullablePositive(heartRateBpm, 'Heart rate bpm');
|
||||
_requireNullableNonNegativeDouble(distanceMeters, 'Distance meters');
|
||||
_requireNullableNonNegativeDouble(caloriesKcal, 'Calories kcal');
|
||||
if (heartRateBpm == null &&
|
||||
distanceMeters == null &&
|
||||
caloriesKcal == null) {
|
||||
throw const DomainException(
|
||||
'Telemetry graph point must contain at least one metric.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final DateTime capturedAt;
|
||||
final int elapsedMs;
|
||||
final int? heartRateBpm;
|
||||
final double? distanceMeters;
|
||||
final double? caloriesKcal;
|
||||
final bool startsAfterGap;
|
||||
}
|
||||
|
||||
final class WorkoutTelemetryGraphSeries {
|
||||
WorkoutTelemetryGraphSeries({
|
||||
required this.scope,
|
||||
this.programIndex,
|
||||
this.exerciseIndex,
|
||||
this.setIndex,
|
||||
this.passageIndex,
|
||||
this.stepIndex,
|
||||
required this.points,
|
||||
this.minHeartRateBpm,
|
||||
this.maxHeartRateBpm,
|
||||
}) {
|
||||
_requireNullableNonNegative(programIndex, 'Program index');
|
||||
_requireNullableNonNegative(exerciseIndex, 'Exercise index');
|
||||
_requireNullableNonNegative(setIndex, 'Set index');
|
||||
_requireNullableNonNegative(passageIndex, 'Passage index');
|
||||
_requireNullableNonNegative(stepIndex, 'Step index');
|
||||
_requireNullablePositive(minHeartRateBpm, 'Min heart rate bpm');
|
||||
_requireNullablePositive(maxHeartRateBpm, 'Max heart rate bpm');
|
||||
}
|
||||
|
||||
final WorkoutTelemetryAggregateScope scope;
|
||||
final int? programIndex;
|
||||
final int? exerciseIndex;
|
||||
final int? setIndex;
|
||||
final int? passageIndex;
|
||||
final int? stepIndex;
|
||||
final List<WorkoutTelemetryGraphPoint> points;
|
||||
final int? minHeartRateBpm;
|
||||
final int? maxHeartRateBpm;
|
||||
}
|
||||
|
||||
final class WorkoutHistorySetResult {
|
||||
WorkoutHistorySetResult({
|
||||
required this.metadata,
|
||||
@ -1924,6 +2009,33 @@ List<ExerciseStep> _validatedExerciseSteps(List<ExerciseStep> steps) {
|
||||
return List.unmodifiable(steps);
|
||||
}
|
||||
|
||||
List<BusinessExerciseType> _validatedBusinessExerciseTypes(
|
||||
List<BusinessExerciseType> types,
|
||||
) {
|
||||
final unique = <BusinessExerciseType>[];
|
||||
for (final type in types) {
|
||||
if (!unique.contains(type)) {
|
||||
unique.add(type);
|
||||
}
|
||||
}
|
||||
return List.unmodifiable(unique);
|
||||
}
|
||||
|
||||
List<BusinessExerciseType> _businessTypesFromCategory(
|
||||
ExerciseCategory category,
|
||||
) {
|
||||
return switch (category) {
|
||||
ExerciseCategory.shoot => const [BusinessExerciseType.shoot],
|
||||
ExerciseCategory.freeThrows => const [BusinessExerciseType.freeThrows],
|
||||
ExerciseCategory.dribble => const [BusinessExerciseType.dribble],
|
||||
ExerciseCategory.finishing => const [BusinessExerciseType.finishing],
|
||||
ExerciseCategory.conditioning => const [BusinessExerciseType.conditioning],
|
||||
ExerciseCategory.defense => const [BusinessExerciseType.defense],
|
||||
ExerciseCategory.mobility => const [BusinessExerciseType.mobility],
|
||||
ExerciseCategory.uncategorized => const [],
|
||||
};
|
||||
}
|
||||
|
||||
void _requireAtLeastOneMeasure({
|
||||
required bool hasTime,
|
||||
required bool hasReps,
|
||||
|
||||
Reference in New Issue
Block a user