feat(exercice): valeurs par défaut d'exercice (tickets #39/#40, parent #30)
Étend le modèle Drift (tables.dart, app_database.dart/.g.dart), les entités/use cases/repositories et les écrans bibliothèque d'exercices et programme pour supporter des valeurs par défaut par exercice (répétitions/temps/poids proposés à la création d'une série). flutter analyze propre, 51/51 tests verts, build APK debug validé. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -25,12 +25,19 @@ final class AppBootstrap implements AppDependencies {
|
||||
});
|
||||
|
||||
final AppDatabase database;
|
||||
@override
|
||||
final ExerciseUseCases exerciseUseCases;
|
||||
@override
|
||||
final MediaUseCases mediaUseCases;
|
||||
@override
|
||||
final ProgramUseCases programUseCases;
|
||||
@override
|
||||
final WorkoutTemplateUseCases workoutTemplateUseCases;
|
||||
@override
|
||||
final ActiveWorkoutSessionUseCases activeWorkoutSessionUseCases;
|
||||
@override
|
||||
final CloseWorkoutSessionUseCase closeWorkoutSessionUseCase;
|
||||
@override
|
||||
final WorkoutHistoryUseCases workoutHistoryUseCases;
|
||||
final SyncGateway syncGateway;
|
||||
|
||||
|
||||
@ -3,6 +3,8 @@ import 'dart:convert';
|
||||
import '../domain/domain.dart';
|
||||
import 'ports.dart';
|
||||
|
||||
const Object _useCaseUnchanged = Object();
|
||||
|
||||
final class ExerciseUseCases {
|
||||
const ExerciseUseCases({
|
||||
required this.repository,
|
||||
@ -27,7 +29,21 @@ final class ExerciseUseCases {
|
||||
ScoreInputMode scoreInputMode = ScoreInputMode.manual,
|
||||
String? scoreLabel,
|
||||
String? scoreUnit,
|
||||
int? defaultTargetTimeSeconds,
|
||||
int? defaultTargetReps,
|
||||
double? defaultTargetScore,
|
||||
int? defaultTargetScoreTimeMs,
|
||||
}) async {
|
||||
_validateExerciseDefaultTargets(
|
||||
hasTimeMeasure: hasTimeMeasure,
|
||||
hasRepsMeasure: hasRepsMeasure,
|
||||
hasScoreMeasure: hasScoreMeasure,
|
||||
scoreInputMode: scoreInputMode,
|
||||
defaultTargetTimeSeconds: defaultTargetTimeSeconds,
|
||||
defaultTargetReps: defaultTargetReps,
|
||||
defaultTargetScore: defaultTargetScore,
|
||||
defaultTargetScoreTimeMs: defaultTargetScoreTimeMs,
|
||||
);
|
||||
final now = clock.now();
|
||||
final exercise = Exercise(
|
||||
metadata: _newMetadata(ids, originDeviceId, now),
|
||||
@ -41,6 +57,10 @@ final class ExerciseUseCases {
|
||||
scoreInputMode: scoreInputMode,
|
||||
scoreLabel: scoreLabel,
|
||||
scoreUnit: scoreUnit,
|
||||
defaultTargetTimeSeconds: defaultTargetTimeSeconds,
|
||||
defaultTargetReps: defaultTargetReps,
|
||||
defaultTargetScore: defaultTargetScore,
|
||||
defaultTargetScoreTimeMs: defaultTargetScoreTimeMs,
|
||||
);
|
||||
await repository.save(exercise);
|
||||
return exercise;
|
||||
@ -66,11 +86,39 @@ final class ExerciseUseCases {
|
||||
ScoreInputMode scoreInputMode = ScoreInputMode.manual,
|
||||
String? scoreLabel,
|
||||
String? scoreUnit,
|
||||
Object? defaultTargetTimeSeconds = _useCaseUnchanged,
|
||||
Object? defaultTargetReps = _useCaseUnchanged,
|
||||
Object? defaultTargetScore = _useCaseUnchanged,
|
||||
Object? defaultTargetScoreTimeMs = _useCaseUnchanged,
|
||||
}) async {
|
||||
final exercise = await repository.findById(id);
|
||||
if (exercise == null) {
|
||||
throw const DomainException('Exercise not found.');
|
||||
}
|
||||
final resolvedDefaultTargetTimeSeconds =
|
||||
defaultTargetTimeSeconds == _useCaseUnchanged
|
||||
? exercise.defaultTargetTimeSeconds
|
||||
: defaultTargetTimeSeconds as int?;
|
||||
final resolvedDefaultTargetReps = defaultTargetReps == _useCaseUnchanged
|
||||
? exercise.defaultTargetReps
|
||||
: defaultTargetReps as int?;
|
||||
final resolvedDefaultTargetScore = defaultTargetScore == _useCaseUnchanged
|
||||
? exercise.defaultTargetScore
|
||||
: (defaultTargetScore as num?)?.toDouble();
|
||||
final resolvedDefaultTargetScoreTimeMs =
|
||||
defaultTargetScoreTimeMs == _useCaseUnchanged
|
||||
? exercise.defaultTargetScoreTimeMs
|
||||
: defaultTargetScoreTimeMs as int?;
|
||||
_validateExerciseDefaultTargets(
|
||||
hasTimeMeasure: hasTimeMeasure,
|
||||
hasRepsMeasure: hasRepsMeasure,
|
||||
hasScoreMeasure: hasScoreMeasure,
|
||||
scoreInputMode: scoreInputMode,
|
||||
defaultTargetTimeSeconds: resolvedDefaultTargetTimeSeconds,
|
||||
defaultTargetReps: resolvedDefaultTargetReps,
|
||||
defaultTargetScore: resolvedDefaultTargetScore,
|
||||
defaultTargetScoreTimeMs: resolvedDefaultTargetScoreTimeMs,
|
||||
);
|
||||
final updated = exercise.copyWith(
|
||||
metadata: exercise.metadata.touch(clock.now()),
|
||||
name: name,
|
||||
@ -83,6 +131,10 @@ final class ExerciseUseCases {
|
||||
scoreInputMode: scoreInputMode,
|
||||
scoreLabel: scoreLabel,
|
||||
scoreUnit: scoreUnit,
|
||||
defaultTargetTimeSeconds: resolvedDefaultTargetTimeSeconds,
|
||||
defaultTargetReps: resolvedDefaultTargetReps,
|
||||
defaultTargetScore: resolvedDefaultTargetScore,
|
||||
defaultTargetScoreTimeMs: resolvedDefaultTargetScoreTimeMs,
|
||||
);
|
||||
await repository.save(updated);
|
||||
return updated;
|
||||
@ -1271,6 +1323,49 @@ final class SetResultPositionState {
|
||||
final ActiveSetResult? result;
|
||||
}
|
||||
|
||||
void _validateExerciseDefaultTargets({
|
||||
required bool hasTimeMeasure,
|
||||
required bool hasRepsMeasure,
|
||||
required bool hasScoreMeasure,
|
||||
required ScoreInputMode scoreInputMode,
|
||||
required int? defaultTargetTimeSeconds,
|
||||
required int? defaultTargetReps,
|
||||
required double? defaultTargetScore,
|
||||
required int? defaultTargetScoreTimeMs,
|
||||
}) {
|
||||
if (hasTimeMeasure &&
|
||||
(defaultTargetTimeSeconds == null || defaultTargetTimeSeconds <= 0)) {
|
||||
throw const DomainException(
|
||||
'Default target time seconds must be positive.',
|
||||
);
|
||||
}
|
||||
if (hasRepsMeasure && (defaultTargetReps == null || defaultTargetReps <= 0)) {
|
||||
throw const DomainException('Default target reps must be positive.');
|
||||
}
|
||||
if (hasScoreMeasure &&
|
||||
scoreInputMode == ScoreInputMode.manual &&
|
||||
(defaultTargetScore == null || defaultTargetScore <= 0)) {
|
||||
throw const DomainException('Default target score must be positive.');
|
||||
}
|
||||
if (defaultTargetScore != null &&
|
||||
(!hasScoreMeasure || scoreInputMode != ScoreInputMode.manual)) {
|
||||
throw const DomainException(
|
||||
'Default target score requires manual score mode.',
|
||||
);
|
||||
}
|
||||
if (defaultTargetScoreTimeMs != null && defaultTargetScoreTimeMs <= 0) {
|
||||
throw const DomainException(
|
||||
'Default target score time ms must be positive.',
|
||||
);
|
||||
}
|
||||
if (defaultTargetScoreTimeMs != null &&
|
||||
(!hasScoreMeasure || scoreInputMode != ScoreInputMode.stopwatch)) {
|
||||
throw const DomainException(
|
||||
'Default target score time ms requires stopwatch score mode.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _validateOverrideTargets(
|
||||
String programSnapshotJson,
|
||||
WorkoutTemplateExerciseOverrideConfig input,
|
||||
|
||||
Reference in New Issue
Block a user