feat(bibliothèque): migration Drift v16, seed basket et seeding au démarrage (ticket #80)
Ajoute la migration Drift v16 (enum ExerciseCategory, champ isExample sur exercises/programs/workout_templates, table local_seed_metadata), le contenu seed basket déterministe (21 exercices, 1 programme, 1 séance- modèle) dans lib/application/starter_content/, les ports StarterSeedStateRepository/StarterContentRepository et le use case SeedStarterContentUseCase branché dans AppBootstrap.create(). Validé GO par QA/Main : dart analyze propre, suite complète 167 tests (4 échecs préexistants sans rapport avec ce lot), 4 nouveaux tests seed tous verts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -68,6 +68,7 @@ final class AppBootstrap implements AppDependencies {
|
|||||||
final syncMetadataRepository = DriftSyncMetadataRepository(database);
|
final syncMetadataRepository = DriftSyncMetadataRepository(database);
|
||||||
final mappingRepository = DriftRemoteResourceMappingRepository(database);
|
final mappingRepository = DriftRemoteResourceMappingRepository(database);
|
||||||
final localSyncChangeRepository = DriftLocalSyncChangeRepository(database);
|
final localSyncChangeRepository = DriftLocalSyncChangeRepository(database);
|
||||||
|
final starterSeedRepository = DriftStarterSeedRepository(database);
|
||||||
final shareInboxRepository = DriftShareInboxRepository(database);
|
final shareInboxRepository = DriftShareInboxRepository(database);
|
||||||
final pendingShareActionRepository = DriftPendingShareActionRepository(
|
final pendingShareActionRepository = DriftPendingShareActionRepository(
|
||||||
database,
|
database,
|
||||||
@ -79,6 +80,12 @@ final class AppBootstrap implements AppDependencies {
|
|||||||
final ids = LocalIdGenerator();
|
final ids = LocalIdGenerator();
|
||||||
const clock = SystemClock();
|
const clock = SystemClock();
|
||||||
const originDeviceId = 'local-device';
|
const originDeviceId = 'local-device';
|
||||||
|
await SeedStarterContentUseCase(
|
||||||
|
seedStateRepository: starterSeedRepository,
|
||||||
|
contentRepository: starterSeedRepository,
|
||||||
|
clock: clock,
|
||||||
|
originDeviceId: originDeviceId,
|
||||||
|
).run();
|
||||||
final remoteAuthApi = HttpRemoteAuthApi(
|
final remoteAuthApi = HttpRemoteAuthApi(
|
||||||
HttpApiClient(baseUrl: Uri.parse(HttpApiClient.defaultBaseUrl)),
|
HttpApiClient(baseUrl: Uri.parse(HttpApiClient.defaultBaseUrl)),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -5,4 +5,6 @@
|
|||||||
library;
|
library;
|
||||||
|
|
||||||
export 'ports.dart';
|
export 'ports.dart';
|
||||||
|
export 'starter_content/basket_starter_seed_v1.dart';
|
||||||
|
export 'starter_content/starter_content.dart';
|
||||||
export 'use_cases.dart';
|
export 'use_cases.dart';
|
||||||
|
|||||||
@ -15,6 +15,28 @@ abstract interface class IdGenerator {
|
|||||||
String newId();
|
String newId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final class StarterContent {
|
||||||
|
const StarterContent({
|
||||||
|
required this.exercises,
|
||||||
|
required this.program,
|
||||||
|
required this.workoutTemplate,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<Exercise> exercises;
|
||||||
|
final Program program;
|
||||||
|
final WorkoutTemplate workoutTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract interface class StarterSeedStateRepository {
|
||||||
|
Future<int> readAppliedStarterSeedVersion();
|
||||||
|
Future<void> writeAppliedStarterSeedVersion(int version, DateTime appliedAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract interface class StarterContentRepository {
|
||||||
|
Future<bool> isLocalContentEmpty();
|
||||||
|
Future<void> insertStarterContent(StarterContent content);
|
||||||
|
}
|
||||||
|
|
||||||
enum RemoteAuthFailure {
|
enum RemoteAuthFailure {
|
||||||
invalidCredentials,
|
invalidCredentials,
|
||||||
emailAlreadyUsed,
|
emailAlreadyUsed,
|
||||||
|
|||||||
526
lib/application/starter_content/basket_starter_seed_v1.dart
Normal file
526
lib/application/starter_content/basket_starter_seed_v1.dart
Normal file
@ -0,0 +1,526 @@
|
|||||||
|
import '../../domain/domain.dart';
|
||||||
|
import 'starter_content.dart';
|
||||||
|
|
||||||
|
const basketStarterExerciseSeedsV1 = [
|
||||||
|
StarterExerciseSeed(
|
||||||
|
id: 'starter-v1-exercise-form-shooting',
|
||||||
|
name: 'Form shooting proche panier',
|
||||||
|
description: 'Travail technique près du cercle, priorité au geste propre.',
|
||||||
|
category: ExerciseCategory.shoot,
|
||||||
|
hasRepsMeasure: true,
|
||||||
|
hasScoreMeasure: true,
|
||||||
|
scoreLabel: 'Réussites',
|
||||||
|
scoreUnit: 'paniers',
|
||||||
|
defaultTargetReps: 25,
|
||||||
|
defaultTargetScore: 20,
|
||||||
|
),
|
||||||
|
StarterExerciseSeed(
|
||||||
|
id: 'starter-v1-exercise-spot-shooting-5-positions',
|
||||||
|
name: 'Spot shooting 5 positions',
|
||||||
|
description: '5 tirs depuis 5 spots autour de la raquette ou du périmètre.',
|
||||||
|
category: ExerciseCategory.shoot,
|
||||||
|
hasRepsMeasure: true,
|
||||||
|
hasScoreMeasure: true,
|
||||||
|
scoreLabel: 'Réussites',
|
||||||
|
scoreUnit: 'paniers',
|
||||||
|
defaultTargetReps: 1,
|
||||||
|
defaultTargetScore: 15,
|
||||||
|
steps: [
|
||||||
|
StarterExerciseStepSeed(
|
||||||
|
id: 'starter-v1-step-spot-shooting-right-corner',
|
||||||
|
name: 'Coin droit',
|
||||||
|
type: ExerciseStepType.reps,
|
||||||
|
defaultTargetValue: 5,
|
||||||
|
),
|
||||||
|
StarterExerciseStepSeed(
|
||||||
|
id: 'starter-v1-step-spot-shooting-right-wing',
|
||||||
|
name: 'Aile droite',
|
||||||
|
type: ExerciseStepType.reps,
|
||||||
|
defaultTargetValue: 5,
|
||||||
|
),
|
||||||
|
StarterExerciseStepSeed(
|
||||||
|
id: 'starter-v1-step-spot-shooting-top',
|
||||||
|
name: 'Face cercle',
|
||||||
|
type: ExerciseStepType.reps,
|
||||||
|
defaultTargetValue: 5,
|
||||||
|
),
|
||||||
|
StarterExerciseStepSeed(
|
||||||
|
id: 'starter-v1-step-spot-shooting-left-wing',
|
||||||
|
name: 'Aile gauche',
|
||||||
|
type: ExerciseStepType.reps,
|
||||||
|
defaultTargetValue: 5,
|
||||||
|
),
|
||||||
|
StarterExerciseStepSeed(
|
||||||
|
id: 'starter-v1-step-spot-shooting-left-corner',
|
||||||
|
name: 'Coin gauche',
|
||||||
|
type: ExerciseStepType.reps,
|
||||||
|
defaultTargetValue: 5,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
StarterExerciseSeed(
|
||||||
|
id: 'starter-v1-exercise-catch-and-shoot',
|
||||||
|
name: 'Catch & shoot',
|
||||||
|
description: 'Recevoir, armer vite, tirer équilibré.',
|
||||||
|
category: ExerciseCategory.shoot,
|
||||||
|
hasRepsMeasure: true,
|
||||||
|
hasScoreMeasure: true,
|
||||||
|
scoreLabel: 'Réussites',
|
||||||
|
scoreUnit: 'paniers',
|
||||||
|
defaultTargetReps: 20,
|
||||||
|
defaultTargetScore: 12,
|
||||||
|
),
|
||||||
|
StarterExerciseSeed(
|
||||||
|
id: 'starter-v1-exercise-pull-up-shooting',
|
||||||
|
name: 'Tir après dribble',
|
||||||
|
description: 'Création d\'espace puis tir en rythme.',
|
||||||
|
category: ExerciseCategory.shoot,
|
||||||
|
hasRepsMeasure: true,
|
||||||
|
hasScoreMeasure: true,
|
||||||
|
scoreLabel: 'Réussites',
|
||||||
|
scoreUnit: 'paniers',
|
||||||
|
defaultTargetReps: 20,
|
||||||
|
defaultTargetScore: 10,
|
||||||
|
steps: [
|
||||||
|
StarterExerciseStepSeed(
|
||||||
|
id: 'starter-v1-step-pull-up-right',
|
||||||
|
name: 'Départ main droite',
|
||||||
|
type: ExerciseStepType.reps,
|
||||||
|
defaultTargetValue: 10,
|
||||||
|
),
|
||||||
|
StarterExerciseStepSeed(
|
||||||
|
id: 'starter-v1-step-pull-up-left',
|
||||||
|
name: 'Départ main gauche',
|
||||||
|
type: ExerciseStepType.reps,
|
||||||
|
defaultTargetValue: 10,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
StarterExerciseSeed(
|
||||||
|
id: 'starter-v1-exercise-free-throw-routine',
|
||||||
|
name: 'Routine lancers francs',
|
||||||
|
description: 'Série calme avec routine complète avant chaque tir.',
|
||||||
|
category: ExerciseCategory.freeThrows,
|
||||||
|
hasRepsMeasure: true,
|
||||||
|
hasScoreMeasure: true,
|
||||||
|
scoreLabel: 'Réussites',
|
||||||
|
scoreUnit: 'lancers',
|
||||||
|
defaultTargetReps: 20,
|
||||||
|
defaultTargetScore: 16,
|
||||||
|
),
|
||||||
|
StarterExerciseSeed(
|
||||||
|
id: 'starter-v1-exercise-fatigue-free-throws',
|
||||||
|
name: 'Lancers francs sous fatigue',
|
||||||
|
description: 'Lancers francs après effort court.',
|
||||||
|
category: ExerciseCategory.freeThrows,
|
||||||
|
hasRepsMeasure: true,
|
||||||
|
hasScoreMeasure: true,
|
||||||
|
scoreLabel: 'Réussites',
|
||||||
|
scoreUnit: 'lancers',
|
||||||
|
defaultTargetReps: 5,
|
||||||
|
defaultTargetScore: 8,
|
||||||
|
steps: [
|
||||||
|
StarterExerciseStepSeed(
|
||||||
|
id: 'starter-v1-step-fatigue-free-throws-baseline-run',
|
||||||
|
name: 'Course ligne de fond',
|
||||||
|
type: ExerciseStepType.time,
|
||||||
|
defaultTargetValue: 15,
|
||||||
|
),
|
||||||
|
StarterExerciseStepSeed(
|
||||||
|
id: 'starter-v1-step-fatigue-free-throws-two-shots',
|
||||||
|
name: '2 lancers francs',
|
||||||
|
type: ExerciseStepType.reps,
|
||||||
|
defaultTargetValue: 2,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
StarterExerciseSeed(
|
||||||
|
id: 'starter-v1-exercise-stationary-dribble-alternating',
|
||||||
|
name: 'Dribble stationnaire alterné',
|
||||||
|
description: 'Contrôle de balle enchaîné main droite/main gauche.',
|
||||||
|
category: ExerciseCategory.dribble,
|
||||||
|
hasTimeMeasure: true,
|
||||||
|
defaultTargetTimeSeconds: 60,
|
||||||
|
steps: [
|
||||||
|
StarterExerciseStepSeed(
|
||||||
|
id: 'starter-v1-step-stationary-dribble-right-low',
|
||||||
|
name: 'Main droite basse',
|
||||||
|
type: ExerciseStepType.time,
|
||||||
|
defaultTargetValue: 15,
|
||||||
|
),
|
||||||
|
StarterExerciseStepSeed(
|
||||||
|
id: 'starter-v1-step-stationary-dribble-left-low',
|
||||||
|
name: 'Main gauche basse',
|
||||||
|
type: ExerciseStepType.time,
|
||||||
|
defaultTargetValue: 15,
|
||||||
|
),
|
||||||
|
StarterExerciseStepSeed(
|
||||||
|
id: 'starter-v1-step-stationary-dribble-crossovers',
|
||||||
|
name: 'Crossovers',
|
||||||
|
type: ExerciseStepType.time,
|
||||||
|
defaultTargetValue: 15,
|
||||||
|
),
|
||||||
|
StarterExerciseStepSeed(
|
||||||
|
id: 'starter-v1-step-stationary-dribble-between-legs',
|
||||||
|
name: 'Entre les jambes',
|
||||||
|
type: ExerciseStepType.time,
|
||||||
|
defaultTargetValue: 15,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
StarterExerciseSeed(
|
||||||
|
id: 'starter-v1-exercise-combo-change-of-hand',
|
||||||
|
name: 'Combo changement de main',
|
||||||
|
description: 'Enchaîner les changements de main avec régularité.',
|
||||||
|
category: ExerciseCategory.dribble,
|
||||||
|
hasRepsMeasure: true,
|
||||||
|
defaultTargetReps: 10,
|
||||||
|
steps: [
|
||||||
|
StarterExerciseStepSeed(
|
||||||
|
id: 'starter-v1-step-combo-crossover',
|
||||||
|
name: 'Crossover',
|
||||||
|
type: ExerciseStepType.reps,
|
||||||
|
defaultTargetValue: 1,
|
||||||
|
),
|
||||||
|
StarterExerciseStepSeed(
|
||||||
|
id: 'starter-v1-step-combo-between-legs',
|
||||||
|
name: 'Entre les jambes',
|
||||||
|
type: ExerciseStepType.reps,
|
||||||
|
defaultTargetValue: 1,
|
||||||
|
),
|
||||||
|
StarterExerciseStepSeed(
|
||||||
|
id: 'starter-v1-step-combo-behind-back',
|
||||||
|
name: 'Dans le dos',
|
||||||
|
type: ExerciseStepType.reps,
|
||||||
|
defaultTargetValue: 1,
|
||||||
|
),
|
||||||
|
StarterExerciseStepSeed(
|
||||||
|
id: 'starter-v1-step-combo-explosive-start',
|
||||||
|
name: 'Départ explosif',
|
||||||
|
type: ExerciseStepType.reps,
|
||||||
|
defaultTargetValue: 1,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
StarterExerciseSeed(
|
||||||
|
id: 'starter-v1-exercise-cone-slalom',
|
||||||
|
name: 'Slalom cônes',
|
||||||
|
description: 'Conduite de balle en vitesse sur parcours.',
|
||||||
|
category: ExerciseCategory.dribble,
|
||||||
|
hasRepsMeasure: true,
|
||||||
|
hasScoreMeasure: true,
|
||||||
|
scoreInputMode: ScoreInputMode.stopwatch,
|
||||||
|
scoreLabel: 'Temps parcours',
|
||||||
|
defaultTargetReps: 6,
|
||||||
|
defaultTargetScoreTimeMs: 12000,
|
||||||
|
),
|
||||||
|
StarterExerciseSeed(
|
||||||
|
id: 'starter-v1-exercise-mikan-drill',
|
||||||
|
name: 'Mikan drill',
|
||||||
|
description: 'Alternance de finitions près du cercle.',
|
||||||
|
category: ExerciseCategory.finishing,
|
||||||
|
hasRepsMeasure: true,
|
||||||
|
hasScoreMeasure: true,
|
||||||
|
scoreLabel: 'Réussites',
|
||||||
|
scoreUnit: 'paniers',
|
||||||
|
defaultTargetReps: 20,
|
||||||
|
defaultTargetScore: 16,
|
||||||
|
),
|
||||||
|
StarterExerciseSeed(
|
||||||
|
id: 'starter-v1-exercise-weak-hand-finishes',
|
||||||
|
name: 'Finitions main faible',
|
||||||
|
description: 'Layups et finitions contrôlées main faible.',
|
||||||
|
category: ExerciseCategory.finishing,
|
||||||
|
hasRepsMeasure: true,
|
||||||
|
hasScoreMeasure: true,
|
||||||
|
scoreLabel: 'Réussites',
|
||||||
|
scoreUnit: 'paniers',
|
||||||
|
defaultTargetReps: 20,
|
||||||
|
defaultTargetScore: 12,
|
||||||
|
),
|
||||||
|
StarterExerciseSeed(
|
||||||
|
id: 'starter-v1-exercise-alternating-layups',
|
||||||
|
name: 'Layups alternés droite/gauche',
|
||||||
|
description: 'Alterner les côtés avec course courte.',
|
||||||
|
category: ExerciseCategory.finishing,
|
||||||
|
hasRepsMeasure: true,
|
||||||
|
hasScoreMeasure: true,
|
||||||
|
scoreLabel: 'Réussites',
|
||||||
|
scoreUnit: 'paniers',
|
||||||
|
defaultTargetReps: 20,
|
||||||
|
defaultTargetScore: 16,
|
||||||
|
steps: [
|
||||||
|
StarterExerciseStepSeed(
|
||||||
|
id: 'starter-v1-step-alternating-layup-right',
|
||||||
|
name: 'Layup côté droit',
|
||||||
|
type: ExerciseStepType.reps,
|
||||||
|
defaultTargetValue: 1,
|
||||||
|
),
|
||||||
|
StarterExerciseStepSeed(
|
||||||
|
id: 'starter-v1-step-alternating-layup-left',
|
||||||
|
name: 'Layup côté gauche',
|
||||||
|
type: ExerciseStepType.reps,
|
||||||
|
defaultTargetValue: 1,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
StarterExerciseSeed(
|
||||||
|
id: 'starter-v1-exercise-contact-finishing',
|
||||||
|
name: 'Finition avec contact',
|
||||||
|
description: 'Absorber le contact puis finir près du cercle.',
|
||||||
|
category: ExerciseCategory.finishing,
|
||||||
|
hasRepsMeasure: true,
|
||||||
|
hasScoreMeasure: true,
|
||||||
|
scoreLabel: 'Réussites',
|
||||||
|
scoreUnit: 'paniers',
|
||||||
|
defaultTargetReps: 12,
|
||||||
|
defaultTargetScore: 8,
|
||||||
|
),
|
||||||
|
StarterExerciseSeed(
|
||||||
|
id: 'starter-v1-exercise-half-court-suicides',
|
||||||
|
name: 'Suicides demi-terrain',
|
||||||
|
description: 'Aller-retour progressif sur lignes du terrain.',
|
||||||
|
category: ExerciseCategory.conditioning,
|
||||||
|
hasRepsMeasure: true,
|
||||||
|
hasScoreMeasure: true,
|
||||||
|
scoreInputMode: ScoreInputMode.stopwatch,
|
||||||
|
scoreLabel: 'Temps total',
|
||||||
|
defaultTargetReps: 4,
|
||||||
|
defaultTargetScoreTimeMs: 45000,
|
||||||
|
),
|
||||||
|
StarterExerciseSeed(
|
||||||
|
id: 'starter-v1-exercise-line-hops',
|
||||||
|
name: 'Line hops',
|
||||||
|
description: 'Petits sauts rapides de part et d\'autre d\'une ligne.',
|
||||||
|
category: ExerciseCategory.conditioning,
|
||||||
|
hasTimeMeasure: true,
|
||||||
|
hasScoreMeasure: true,
|
||||||
|
scoreLabel: 'Contacts',
|
||||||
|
scoreUnit: 'touches',
|
||||||
|
defaultTargetTimeSeconds: 30,
|
||||||
|
defaultTargetScore: 50,
|
||||||
|
),
|
||||||
|
StarterExerciseSeed(
|
||||||
|
id: 'starter-v1-exercise-baseline-sprint',
|
||||||
|
name: 'Sprint ligne de fond',
|
||||||
|
description: 'Départs explosifs répétés.',
|
||||||
|
category: ExerciseCategory.conditioning,
|
||||||
|
hasRepsMeasure: true,
|
||||||
|
hasScoreMeasure: true,
|
||||||
|
scoreInputMode: ScoreInputMode.stopwatch,
|
||||||
|
scoreLabel: 'Meilleur sprint',
|
||||||
|
defaultTargetReps: 10,
|
||||||
|
defaultTargetScoreTimeMs: 5000,
|
||||||
|
),
|
||||||
|
StarterExerciseSeed(
|
||||||
|
id: 'starter-v1-exercise-defensive-slides',
|
||||||
|
name: 'Slides défensifs',
|
||||||
|
description: 'Déplacements latéraux bas et contrôlés.',
|
||||||
|
category: ExerciseCategory.defense,
|
||||||
|
hasTimeMeasure: true,
|
||||||
|
defaultTargetTimeSeconds: 45,
|
||||||
|
),
|
||||||
|
StarterExerciseSeed(
|
||||||
|
id: 'starter-v1-exercise-closeout-slide',
|
||||||
|
name: 'Closeout + slide',
|
||||||
|
description: 'Fermer l\'espace puis contenir latéralement.',
|
||||||
|
category: ExerciseCategory.defense,
|
||||||
|
hasRepsMeasure: true,
|
||||||
|
defaultTargetReps: 10,
|
||||||
|
steps: [
|
||||||
|
StarterExerciseStepSeed(
|
||||||
|
id: 'starter-v1-step-closeout-slide-closeout',
|
||||||
|
name: 'Closeout',
|
||||||
|
type: ExerciseStepType.reps,
|
||||||
|
defaultTargetValue: 1,
|
||||||
|
),
|
||||||
|
StarterExerciseStepSeed(
|
||||||
|
id: 'starter-v1-step-closeout-slide-right',
|
||||||
|
name: 'Slide droite',
|
||||||
|
type: ExerciseStepType.reps,
|
||||||
|
defaultTargetValue: 1,
|
||||||
|
),
|
||||||
|
StarterExerciseStepSeed(
|
||||||
|
id: 'starter-v1-step-closeout-slide-left',
|
||||||
|
name: 'Slide gauche',
|
||||||
|
type: ExerciseStepType.reps,
|
||||||
|
defaultTargetValue: 1,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
StarterExerciseSeed(
|
||||||
|
id: 'starter-v1-exercise-rebound-outlet-pass',
|
||||||
|
name: 'Rebond + outlet pass',
|
||||||
|
description: 'Capturer le rebond puis ressortir proprement.',
|
||||||
|
category: ExerciseCategory.defense,
|
||||||
|
hasRepsMeasure: true,
|
||||||
|
hasScoreMeasure: true,
|
||||||
|
scoreLabel: 'Réussites',
|
||||||
|
scoreUnit: 'actions',
|
||||||
|
defaultTargetReps: 12,
|
||||||
|
defaultTargetScore: 10,
|
||||||
|
steps: [
|
||||||
|
StarterExerciseStepSeed(
|
||||||
|
id: 'starter-v1-step-rebound-outlet-rebound',
|
||||||
|
name: 'Rebond',
|
||||||
|
type: ExerciseStepType.reps,
|
||||||
|
defaultTargetValue: 1,
|
||||||
|
),
|
||||||
|
StarterExerciseStepSeed(
|
||||||
|
id: 'starter-v1-step-rebound-outlet-pass',
|
||||||
|
name: 'Outlet pass',
|
||||||
|
type: ExerciseStepType.reps,
|
||||||
|
defaultTargetValue: 1,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
StarterExerciseSeed(
|
||||||
|
id: 'starter-v1-exercise-basket-hip-mobility',
|
||||||
|
name: 'Mobilité hanches basket',
|
||||||
|
description: 'Préparer hanches, appuis et amplitude.',
|
||||||
|
category: ExerciseCategory.mobility,
|
||||||
|
hasTimeMeasure: true,
|
||||||
|
defaultTargetTimeSeconds: 300,
|
||||||
|
steps: [
|
||||||
|
StarterExerciseStepSeed(
|
||||||
|
id: 'starter-v1-step-hip-mobility-openers',
|
||||||
|
name: 'Ouverture hanches',
|
||||||
|
type: ExerciseStepType.time,
|
||||||
|
defaultTargetValue: 60,
|
||||||
|
),
|
||||||
|
StarterExerciseStepSeed(
|
||||||
|
id: 'starter-v1-step-hip-mobility-lunges',
|
||||||
|
name: 'Fentes dynamiques',
|
||||||
|
type: ExerciseStepType.time,
|
||||||
|
defaultTargetValue: 60,
|
||||||
|
),
|
||||||
|
StarterExerciseStepSeed(
|
||||||
|
id: 'starter-v1-step-hip-mobility-squat-hold',
|
||||||
|
name: 'Squat hold',
|
||||||
|
type: ExerciseStepType.time,
|
||||||
|
defaultTargetValue: 60,
|
||||||
|
),
|
||||||
|
StarterExerciseStepSeed(
|
||||||
|
id: 'starter-v1-step-hip-mobility-adductors',
|
||||||
|
name: 'Adducteurs',
|
||||||
|
type: ExerciseStepType.time,
|
||||||
|
defaultTargetValue: 60,
|
||||||
|
),
|
||||||
|
StarterExerciseStepSeed(
|
||||||
|
id: 'starter-v1-step-hip-mobility-breathing',
|
||||||
|
name: 'Respiration basse',
|
||||||
|
type: ExerciseStepType.time,
|
||||||
|
defaultTargetValue: 60,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
StarterExerciseSeed(
|
||||||
|
id: 'starter-v1-exercise-ankles-calves',
|
||||||
|
name: 'Chevilles et mollets',
|
||||||
|
description: 'Préparer les appuis, réceptions et changements de direction.',
|
||||||
|
category: ExerciseCategory.mobility,
|
||||||
|
hasTimeMeasure: true,
|
||||||
|
defaultTargetTimeSeconds: 180,
|
||||||
|
steps: [
|
||||||
|
StarterExerciseStepSeed(
|
||||||
|
id: 'starter-v1-step-ankles-calves-right-ankle',
|
||||||
|
name: 'Mobilité cheville droite',
|
||||||
|
type: ExerciseStepType.time,
|
||||||
|
defaultTargetValue: 60,
|
||||||
|
),
|
||||||
|
StarterExerciseStepSeed(
|
||||||
|
id: 'starter-v1-step-ankles-calves-left-ankle',
|
||||||
|
name: 'Mobilité cheville gauche',
|
||||||
|
type: ExerciseStepType.time,
|
||||||
|
defaultTargetValue: 60,
|
||||||
|
),
|
||||||
|
StarterExerciseStepSeed(
|
||||||
|
id: 'starter-v1-step-ankles-calves-dynamic-calves',
|
||||||
|
name: 'Mollets dynamiques',
|
||||||
|
type: ExerciseStepType.time,
|
||||||
|
defaultTargetValue: 60,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
const basketStarterProgramSeedV1 = StarterProgramSeed(
|
||||||
|
id: 'starter-v1-program-basket-foundations-45-min',
|
||||||
|
name: 'Fondations basket - 45 min',
|
||||||
|
defaultRestSeconds: 45,
|
||||||
|
exercises: [
|
||||||
|
StarterProgramExerciseSeed(
|
||||||
|
id: 'starter-v1-program-exercise-hip-mobility',
|
||||||
|
exerciseId: 'starter-v1-exercise-basket-hip-mobility',
|
||||||
|
setsCount: 1,
|
||||||
|
enabledMeasures: {WorkoutMeasure.time},
|
||||||
|
targetTimeSeconds: 300,
|
||||||
|
restSecondsOverride: 30,
|
||||||
|
),
|
||||||
|
StarterProgramExerciseSeed(
|
||||||
|
id: 'starter-v1-program-exercise-stationary-dribble',
|
||||||
|
exerciseId: 'starter-v1-exercise-stationary-dribble-alternating',
|
||||||
|
setsCount: 2,
|
||||||
|
enabledMeasures: {WorkoutMeasure.time},
|
||||||
|
targetTimeSeconds: 60,
|
||||||
|
restSecondsOverride: 30,
|
||||||
|
),
|
||||||
|
StarterProgramExerciseSeed(
|
||||||
|
id: 'starter-v1-program-exercise-combo-change-of-hand',
|
||||||
|
exerciseId: 'starter-v1-exercise-combo-change-of-hand',
|
||||||
|
setsCount: 2,
|
||||||
|
enabledMeasures: {WorkoutMeasure.reps},
|
||||||
|
targetReps: 10,
|
||||||
|
restSecondsOverride: 45,
|
||||||
|
),
|
||||||
|
StarterProgramExerciseSeed(
|
||||||
|
id: 'starter-v1-program-exercise-form-shooting',
|
||||||
|
exerciseId: 'starter-v1-exercise-form-shooting',
|
||||||
|
setsCount: 3,
|
||||||
|
enabledMeasures: {WorkoutMeasure.reps, WorkoutMeasure.score},
|
||||||
|
targetReps: 25,
|
||||||
|
targetScore: 20,
|
||||||
|
restSecondsOverride: 45,
|
||||||
|
),
|
||||||
|
StarterProgramExerciseSeed(
|
||||||
|
id: 'starter-v1-program-exercise-spot-shooting',
|
||||||
|
exerciseId: 'starter-v1-exercise-spot-shooting-5-positions',
|
||||||
|
setsCount: 3,
|
||||||
|
enabledMeasures: {WorkoutMeasure.reps, WorkoutMeasure.score},
|
||||||
|
targetReps: 1,
|
||||||
|
targetScore: 15,
|
||||||
|
restSecondsOverride: 60,
|
||||||
|
),
|
||||||
|
StarterProgramExerciseSeed(
|
||||||
|
id: 'starter-v1-program-exercise-alternating-layups',
|
||||||
|
exerciseId: 'starter-v1-exercise-alternating-layups',
|
||||||
|
setsCount: 3,
|
||||||
|
enabledMeasures: {WorkoutMeasure.reps, WorkoutMeasure.score},
|
||||||
|
targetReps: 20,
|
||||||
|
targetScore: 16,
|
||||||
|
restSecondsOverride: 45,
|
||||||
|
),
|
||||||
|
StarterProgramExerciseSeed(
|
||||||
|
id: 'starter-v1-program-exercise-free-throw-routine',
|
||||||
|
exerciseId: 'starter-v1-exercise-free-throw-routine',
|
||||||
|
setsCount: 4,
|
||||||
|
enabledMeasures: {WorkoutMeasure.reps, WorkoutMeasure.score},
|
||||||
|
targetReps: 10,
|
||||||
|
targetScore: 8,
|
||||||
|
restSecondsOverride: 30,
|
||||||
|
),
|
||||||
|
StarterProgramExerciseSeed(
|
||||||
|
id: 'starter-v1-program-exercise-defensive-slides',
|
||||||
|
exerciseId: 'starter-v1-exercise-defensive-slides',
|
||||||
|
setsCount: 4,
|
||||||
|
enabledMeasures: {WorkoutMeasure.time},
|
||||||
|
targetTimeSeconds: 45,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const basketStarterWorkoutTemplateSeedV1 = StarterWorkoutTemplateSeed(
|
||||||
|
id: 'starter-v1-workout-template-basket-foundations-example',
|
||||||
|
name: 'Séance exemple - Fondations basket',
|
||||||
|
programSnapshotId: 'starter-v1-workout-template-program-basket-foundations',
|
||||||
|
);
|
||||||
204
lib/application/starter_content/starter_content.dart
Normal file
204
lib/application/starter_content/starter_content.dart
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
import '../../domain/domain.dart';
|
||||||
|
import '../ports.dart';
|
||||||
|
|
||||||
|
const int starterSeedVersion = 1;
|
||||||
|
|
||||||
|
final class StarterExerciseSeed {
|
||||||
|
const StarterExerciseSeed({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
required this.description,
|
||||||
|
required this.category,
|
||||||
|
this.hasTimeMeasure = false,
|
||||||
|
this.hasRepsMeasure = false,
|
||||||
|
this.hasScoreMeasure = false,
|
||||||
|
this.scoreInputMode = ScoreInputMode.manual,
|
||||||
|
this.scoreLabel,
|
||||||
|
this.scoreUnit,
|
||||||
|
this.defaultTargetTimeSeconds,
|
||||||
|
this.defaultTargetReps,
|
||||||
|
this.defaultTargetScore,
|
||||||
|
this.defaultTargetScoreTimeMs,
|
||||||
|
this.steps = const [],
|
||||||
|
});
|
||||||
|
|
||||||
|
final String id;
|
||||||
|
final String name;
|
||||||
|
final String description;
|
||||||
|
final ExerciseCategory category;
|
||||||
|
final bool hasTimeMeasure;
|
||||||
|
final bool hasRepsMeasure;
|
||||||
|
final bool hasScoreMeasure;
|
||||||
|
final ScoreInputMode scoreInputMode;
|
||||||
|
final String? scoreLabel;
|
||||||
|
final String? scoreUnit;
|
||||||
|
final int? defaultTargetTimeSeconds;
|
||||||
|
final int? defaultTargetReps;
|
||||||
|
final double? defaultTargetScore;
|
||||||
|
final int? defaultTargetScoreTimeMs;
|
||||||
|
final List<StarterExerciseStepSeed> steps;
|
||||||
|
}
|
||||||
|
|
||||||
|
final class StarterExerciseStepSeed {
|
||||||
|
const StarterExerciseStepSeed({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
required this.type,
|
||||||
|
required this.defaultTargetValue,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String id;
|
||||||
|
final String name;
|
||||||
|
final ExerciseStepType type;
|
||||||
|
final int defaultTargetValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
final class StarterProgramSeed {
|
||||||
|
const StarterProgramSeed({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
required this.defaultRestSeconds,
|
||||||
|
required this.exercises,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String id;
|
||||||
|
final String name;
|
||||||
|
final int defaultRestSeconds;
|
||||||
|
final List<StarterProgramExerciseSeed> exercises;
|
||||||
|
}
|
||||||
|
|
||||||
|
final class StarterProgramExerciseSeed {
|
||||||
|
const StarterProgramExerciseSeed({
|
||||||
|
required this.id,
|
||||||
|
required this.exerciseId,
|
||||||
|
required this.setsCount,
|
||||||
|
required this.enabledMeasures,
|
||||||
|
this.targetTimeSeconds,
|
||||||
|
this.targetReps,
|
||||||
|
this.targetScore,
|
||||||
|
this.targetScoreTimeMs,
|
||||||
|
this.restSecondsOverride,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String id;
|
||||||
|
final String exerciseId;
|
||||||
|
final int setsCount;
|
||||||
|
final Set<WorkoutMeasure> enabledMeasures;
|
||||||
|
final int? targetTimeSeconds;
|
||||||
|
final int? targetReps;
|
||||||
|
final double? targetScore;
|
||||||
|
final int? targetScoreTimeMs;
|
||||||
|
final int? restSecondsOverride;
|
||||||
|
}
|
||||||
|
|
||||||
|
final class StarterWorkoutTemplateSeed {
|
||||||
|
const StarterWorkoutTemplateSeed({
|
||||||
|
required this.id,
|
||||||
|
required this.name,
|
||||||
|
required this.programSnapshotId,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String id;
|
||||||
|
final String name;
|
||||||
|
final String programSnapshotId;
|
||||||
|
}
|
||||||
|
|
||||||
|
StarterContent buildStarterContent({
|
||||||
|
required List<StarterExerciseSeed> exerciseSeeds,
|
||||||
|
required StarterProgramSeed programSeed,
|
||||||
|
required StarterWorkoutTemplateSeed templateSeed,
|
||||||
|
required DateTime now,
|
||||||
|
required String originDeviceId,
|
||||||
|
}) {
|
||||||
|
final exercisesById = <String, Exercise>{};
|
||||||
|
for (final seed in exerciseSeeds) {
|
||||||
|
exercisesById[seed.id] = Exercise(
|
||||||
|
metadata: _metadata(seed.id, now, originDeviceId),
|
||||||
|
name: seed.name,
|
||||||
|
description: seed.description,
|
||||||
|
hasTimeMeasure: seed.hasTimeMeasure,
|
||||||
|
hasRepsMeasure: seed.hasRepsMeasure,
|
||||||
|
hasScoreMeasure: seed.hasScoreMeasure,
|
||||||
|
scoreInputMode: seed.scoreInputMode,
|
||||||
|
scoreLabel: seed.scoreLabel,
|
||||||
|
scoreUnit: seed.scoreUnit,
|
||||||
|
defaultTargetTimeSeconds: seed.defaultTargetTimeSeconds,
|
||||||
|
defaultTargetReps: seed.defaultTargetReps,
|
||||||
|
defaultTargetScore: seed.defaultTargetScore,
|
||||||
|
defaultTargetScoreTimeMs: seed.defaultTargetScoreTimeMs,
|
||||||
|
category: seed.category,
|
||||||
|
isExample: true,
|
||||||
|
steps: [
|
||||||
|
for (var index = 0; index < seed.steps.length; index += 1)
|
||||||
|
ExerciseStep(
|
||||||
|
id: seed.steps[index].id,
|
||||||
|
position: index,
|
||||||
|
name: seed.steps[index].name,
|
||||||
|
type: seed.steps[index].type,
|
||||||
|
defaultTargetValue: seed.steps[index].defaultTargetValue,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final programExercises = <ProgramExercise>[];
|
||||||
|
for (var index = 0; index < programSeed.exercises.length; index += 1) {
|
||||||
|
final seed = programSeed.exercises[index];
|
||||||
|
final exercise = exercisesById[seed.exerciseId];
|
||||||
|
if (exercise == null) {
|
||||||
|
throw DomainException('Unknown starter exercise id: ${seed.exerciseId}');
|
||||||
|
}
|
||||||
|
programExercises.add(
|
||||||
|
ProgramExercise.snapshotFromExercise(
|
||||||
|
metadata: _metadata(seed.id, now, originDeviceId),
|
||||||
|
programId: programSeed.id,
|
||||||
|
exercise: exercise,
|
||||||
|
position: index,
|
||||||
|
setsCount: seed.setsCount,
|
||||||
|
enabledMeasures: seed.enabledMeasures,
|
||||||
|
targetTimeSeconds: seed.targetTimeSeconds,
|
||||||
|
targetReps: seed.targetReps,
|
||||||
|
targetScore: seed.targetScore,
|
||||||
|
targetScoreTimeMs: seed.targetScoreTimeMs,
|
||||||
|
restSecondsOverride: seed.restSecondsOverride,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final program = Program(
|
||||||
|
metadata: _metadata(programSeed.id, now, originDeviceId),
|
||||||
|
name: programSeed.name,
|
||||||
|
defaultRestSeconds: programSeed.defaultRestSeconds,
|
||||||
|
isExample: true,
|
||||||
|
exercises: programExercises,
|
||||||
|
);
|
||||||
|
final templateProgram = WorkoutTemplateProgram.snapshotFromProgram(
|
||||||
|
metadata: _metadata(templateSeed.programSnapshotId, now, originDeviceId),
|
||||||
|
workoutTemplateId: templateSeed.id,
|
||||||
|
program: program,
|
||||||
|
position: 0,
|
||||||
|
);
|
||||||
|
final template = WorkoutTemplate(
|
||||||
|
metadata: _metadata(templateSeed.id, now, originDeviceId),
|
||||||
|
name: templateSeed.name,
|
||||||
|
isExample: true,
|
||||||
|
programs: [templateProgram],
|
||||||
|
);
|
||||||
|
|
||||||
|
return StarterContent(
|
||||||
|
exercises: exercisesById.values.toList(growable: false),
|
||||||
|
program: program,
|
||||||
|
workoutTemplate: template,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
EntityMetadata _metadata(String id, DateTime now, String originDeviceId) {
|
||||||
|
return EntityMetadata(
|
||||||
|
id: id,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
originDeviceId: originDeviceId,
|
||||||
|
syncState: SyncState.localOnly,
|
||||||
|
localRevision: 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -2,9 +2,76 @@ import 'dart:convert';
|
|||||||
|
|
||||||
import '../domain/domain.dart';
|
import '../domain/domain.dart';
|
||||||
import 'ports.dart';
|
import 'ports.dart';
|
||||||
|
import 'starter_content/basket_starter_seed_v1.dart';
|
||||||
|
import 'starter_content/starter_content.dart';
|
||||||
|
|
||||||
const Object _useCaseUnchanged = Object();
|
const Object _useCaseUnchanged = Object();
|
||||||
|
|
||||||
|
enum StarterSeedStatus { inserted, skippedAlreadyApplied, skippedNotEmpty }
|
||||||
|
|
||||||
|
final class StarterSeedResult {
|
||||||
|
const StarterSeedResult({required this.status, required this.appliedVersion});
|
||||||
|
|
||||||
|
final StarterSeedStatus status;
|
||||||
|
final int appliedVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
final class SeedStarterContentUseCase {
|
||||||
|
const SeedStarterContentUseCase({
|
||||||
|
required this.seedStateRepository,
|
||||||
|
required this.contentRepository,
|
||||||
|
required this.clock,
|
||||||
|
required this.originDeviceId,
|
||||||
|
});
|
||||||
|
|
||||||
|
final StarterSeedStateRepository seedStateRepository;
|
||||||
|
final StarterContentRepository contentRepository;
|
||||||
|
final Clock clock;
|
||||||
|
final String originDeviceId;
|
||||||
|
|
||||||
|
Future<StarterSeedResult> run() async {
|
||||||
|
final appliedVersion = await seedStateRepository
|
||||||
|
.readAppliedStarterSeedVersion();
|
||||||
|
if (appliedVersion >= starterSeedVersion) {
|
||||||
|
return const StarterSeedResult(
|
||||||
|
status: StarterSeedStatus.skippedAlreadyApplied,
|
||||||
|
appliedVersion: starterSeedVersion,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final now = clock.now();
|
||||||
|
final isEmpty = await contentRepository.isLocalContentEmpty();
|
||||||
|
if (!isEmpty) {
|
||||||
|
await seedStateRepository.writeAppliedStarterSeedVersion(
|
||||||
|
starterSeedVersion,
|
||||||
|
now,
|
||||||
|
);
|
||||||
|
return const StarterSeedResult(
|
||||||
|
status: StarterSeedStatus.skippedNotEmpty,
|
||||||
|
appliedVersion: starterSeedVersion,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await contentRepository.insertStarterContent(
|
||||||
|
buildStarterContent(
|
||||||
|
exerciseSeeds: basketStarterExerciseSeedsV1,
|
||||||
|
programSeed: basketStarterProgramSeedV1,
|
||||||
|
templateSeed: basketStarterWorkoutTemplateSeedV1,
|
||||||
|
now: now,
|
||||||
|
originDeviceId: originDeviceId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await seedStateRepository.writeAppliedStarterSeedVersion(
|
||||||
|
starterSeedVersion,
|
||||||
|
now,
|
||||||
|
);
|
||||||
|
return const StarterSeedResult(
|
||||||
|
status: StarterSeedStatus.inserted,
|
||||||
|
appliedVersion: starterSeedVersion,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final class AuthUseCases {
|
final class AuthUseCases {
|
||||||
const AuthUseCases({
|
const AuthUseCases({
|
||||||
required this.tokenStore,
|
required this.tokenStore,
|
||||||
|
|||||||
@ -10,6 +10,17 @@ enum ScoreInputMode { manual, stopwatch }
|
|||||||
|
|
||||||
enum ExerciseStepType { time, reps }
|
enum ExerciseStepType { time, reps }
|
||||||
|
|
||||||
|
enum ExerciseCategory {
|
||||||
|
shoot,
|
||||||
|
freeThrows,
|
||||||
|
dribble,
|
||||||
|
finishing,
|
||||||
|
conditioning,
|
||||||
|
defense,
|
||||||
|
mobility,
|
||||||
|
uncategorized,
|
||||||
|
}
|
||||||
|
|
||||||
enum ActiveWorkoutStatus { running, paused, savedExit, completed, abandoned }
|
enum ActiveWorkoutStatus { running, paused, savedExit, completed, abandoned }
|
||||||
|
|
||||||
enum SetResultStatus { completed, skipped }
|
enum SetResultStatus { completed, skipped }
|
||||||
@ -321,6 +332,8 @@ final class Exercise {
|
|||||||
this.defaultTargetScore,
|
this.defaultTargetScore,
|
||||||
this.defaultTargetScoreTimeMs,
|
this.defaultTargetScoreTimeMs,
|
||||||
this.autoStartNextTimedStep = true,
|
this.autoStartNextTimedStep = true,
|
||||||
|
this.category = ExerciseCategory.uncategorized,
|
||||||
|
this.isExample = false,
|
||||||
List<ExerciseStep> steps = const [],
|
List<ExerciseStep> steps = const [],
|
||||||
this.archivedAt,
|
this.archivedAt,
|
||||||
}) : name = _nonBlank(name, 'Exercise name'),
|
}) : name = _nonBlank(name, 'Exercise name'),
|
||||||
@ -379,6 +392,8 @@ final class Exercise {
|
|||||||
final double? defaultTargetScore;
|
final double? defaultTargetScore;
|
||||||
final int? defaultTargetScoreTimeMs;
|
final int? defaultTargetScoreTimeMs;
|
||||||
final bool autoStartNextTimedStep;
|
final bool autoStartNextTimedStep;
|
||||||
|
final ExerciseCategory category;
|
||||||
|
final bool isExample;
|
||||||
final List<ExerciseStep> steps;
|
final List<ExerciseStep> steps;
|
||||||
final DateTime? archivedAt;
|
final DateTime? archivedAt;
|
||||||
|
|
||||||
@ -414,6 +429,8 @@ final class Exercise {
|
|||||||
Object? defaultTargetScore = _unchanged,
|
Object? defaultTargetScore = _unchanged,
|
||||||
Object? defaultTargetScoreTimeMs = _unchanged,
|
Object? defaultTargetScoreTimeMs = _unchanged,
|
||||||
bool? autoStartNextTimedStep,
|
bool? autoStartNextTimedStep,
|
||||||
|
ExerciseCategory? category,
|
||||||
|
bool? isExample,
|
||||||
Object? steps = _unchanged,
|
Object? steps = _unchanged,
|
||||||
Object? archivedAt = _unchanged,
|
Object? archivedAt = _unchanged,
|
||||||
}) {
|
}) {
|
||||||
@ -463,6 +480,8 @@ final class Exercise {
|
|||||||
: defaultTargetScoreTimeMs as int?,
|
: defaultTargetScoreTimeMs as int?,
|
||||||
autoStartNextTimedStep:
|
autoStartNextTimedStep:
|
||||||
autoStartNextTimedStep ?? this.autoStartNextTimedStep,
|
autoStartNextTimedStep ?? this.autoStartNextTimedStep,
|
||||||
|
category: category ?? this.category,
|
||||||
|
isExample: isExample ?? this.isExample,
|
||||||
steps: steps == _unchanged ? this.steps : steps as List<ExerciseStep>,
|
steps: steps == _unchanged ? this.steps : steps as List<ExerciseStep>,
|
||||||
archivedAt: archivedAt == _unchanged
|
archivedAt: archivedAt == _unchanged
|
||||||
? this.archivedAt
|
? this.archivedAt
|
||||||
@ -532,6 +551,7 @@ final class Program {
|
|||||||
required this.metadata,
|
required this.metadata,
|
||||||
required String name,
|
required String name,
|
||||||
required this.defaultRestSeconds,
|
required this.defaultRestSeconds,
|
||||||
|
this.isExample = false,
|
||||||
this.exercises = const [],
|
this.exercises = const [],
|
||||||
}) : name = _nonBlank(name, 'Program name') {
|
}) : name = _nonBlank(name, 'Program name') {
|
||||||
_requireNonNegative(defaultRestSeconds, 'Default rest seconds');
|
_requireNonNegative(defaultRestSeconds, 'Default rest seconds');
|
||||||
@ -540,6 +560,7 @@ final class Program {
|
|||||||
final EntityMetadata metadata;
|
final EntityMetadata metadata;
|
||||||
final String name;
|
final String name;
|
||||||
final int defaultRestSeconds;
|
final int defaultRestSeconds;
|
||||||
|
final bool isExample;
|
||||||
final List<ProgramExercise> exercises;
|
final List<ProgramExercise> exercises;
|
||||||
|
|
||||||
Program copyWith({
|
Program copyWith({
|
||||||
@ -547,11 +568,13 @@ final class Program {
|
|||||||
EntityMetadata? metadata,
|
EntityMetadata? metadata,
|
||||||
String? name,
|
String? name,
|
||||||
int? defaultRestSeconds,
|
int? defaultRestSeconds,
|
||||||
|
bool? isExample,
|
||||||
}) {
|
}) {
|
||||||
return Program(
|
return Program(
|
||||||
metadata: metadata ?? this.metadata,
|
metadata: metadata ?? this.metadata,
|
||||||
name: name ?? this.name,
|
name: name ?? this.name,
|
||||||
defaultRestSeconds: defaultRestSeconds ?? this.defaultRestSeconds,
|
defaultRestSeconds: defaultRestSeconds ?? this.defaultRestSeconds,
|
||||||
|
isExample: isExample ?? this.isExample,
|
||||||
exercises: exercises ?? this.exercises,
|
exercises: exercises ?? this.exercises,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -745,6 +768,7 @@ final class WorkoutTemplate {
|
|||||||
required this.metadata,
|
required this.metadata,
|
||||||
required String name,
|
required String name,
|
||||||
this.lastStartedAt,
|
this.lastStartedAt,
|
||||||
|
this.isExample = false,
|
||||||
this.programs = const [],
|
this.programs = const [],
|
||||||
this.overrides = const [],
|
this.overrides = const [],
|
||||||
}) : name = _nonBlank(name, 'Workout template name');
|
}) : name = _nonBlank(name, 'Workout template name');
|
||||||
@ -752,6 +776,7 @@ final class WorkoutTemplate {
|
|||||||
final EntityMetadata metadata;
|
final EntityMetadata metadata;
|
||||||
final String name;
|
final String name;
|
||||||
final DateTime? lastStartedAt;
|
final DateTime? lastStartedAt;
|
||||||
|
final bool isExample;
|
||||||
final List<WorkoutTemplateProgram> programs;
|
final List<WorkoutTemplateProgram> programs;
|
||||||
final List<WorkoutTemplateExerciseOverride> overrides;
|
final List<WorkoutTemplateExerciseOverride> overrides;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,6 +18,7 @@ part 'app_database.g.dart';
|
|||||||
Exercises,
|
Exercises,
|
||||||
ExerciseImages,
|
ExerciseImages,
|
||||||
ExerciseSteps,
|
ExerciseSteps,
|
||||||
|
LocalSeedMetadata,
|
||||||
MediaAssets,
|
MediaAssets,
|
||||||
OnlineAccountSessions,
|
OnlineAccountSessions,
|
||||||
PendingShareActions,
|
PendingShareActions,
|
||||||
@ -47,7 +48,7 @@ final class AppDatabase extends _$AppDatabase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get schemaVersion => 15;
|
int get schemaVersion => 16;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
MigrationStrategy get migration {
|
MigrationStrategy get migration {
|
||||||
@ -55,6 +56,7 @@ final class AppDatabase extends _$AppDatabase {
|
|||||||
onCreate: (migrator) async {
|
onCreate: (migrator) async {
|
||||||
await migrator.createAll();
|
await migrator.createAll();
|
||||||
await _migrateToSchema15();
|
await _migrateToSchema15();
|
||||||
|
await _migrateToSchema16(migrator);
|
||||||
await _createIndexes();
|
await _createIndexes();
|
||||||
},
|
},
|
||||||
onUpgrade: (migrator, from, to) async {
|
onUpgrade: (migrator, from, to) async {
|
||||||
@ -108,6 +110,9 @@ final class AppDatabase extends _$AppDatabase {
|
|||||||
if (from < 15) {
|
if (from < 15) {
|
||||||
await _migrateToSchema15();
|
await _migrateToSchema15();
|
||||||
}
|
}
|
||||||
|
if (from < 16) {
|
||||||
|
await _migrateToSchema16(migrator);
|
||||||
|
}
|
||||||
await _createIndexes();
|
await _createIndexes();
|
||||||
},
|
},
|
||||||
beforeOpen: (details) async {
|
beforeOpen: (details) async {
|
||||||
@ -148,6 +153,22 @@ final class AppDatabase extends _$AppDatabase {
|
|||||||
'CREATE INDEX IF NOT EXISTS idx_exercise_steps_exercise_id_position '
|
'CREATE INDEX IF NOT EXISTS idx_exercise_steps_exercise_id_position '
|
||||||
'ON exercise_steps (exercise_id, position)',
|
'ON exercise_steps (exercise_id, position)',
|
||||||
);
|
);
|
||||||
|
await customStatement(
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_exercises_category '
|
||||||
|
'ON exercises (category)',
|
||||||
|
);
|
||||||
|
await customStatement(
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_exercises_is_example '
|
||||||
|
'ON exercises (is_example)',
|
||||||
|
);
|
||||||
|
await customStatement(
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_programs_is_example '
|
||||||
|
'ON programs (is_example)',
|
||||||
|
);
|
||||||
|
await customStatement(
|
||||||
|
'CREATE INDEX IF NOT EXISTS idx_workout_templates_is_example '
|
||||||
|
'ON workout_templates (is_example)',
|
||||||
|
);
|
||||||
await customStatement(
|
await customStatement(
|
||||||
'CREATE INDEX IF NOT EXISTS idx_online_account_sessions_logged_in '
|
'CREATE INDEX IF NOT EXISTS idx_online_account_sessions_logged_in '
|
||||||
'ON online_account_sessions (is_logged_in, updated_at)',
|
'ON online_account_sessions (is_logged_in, updated_at)',
|
||||||
@ -637,6 +658,57 @@ CREATE TABLE IF NOT EXISTS active_set_timer_states (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _migrateToSchema16(Migrator migrator) async {
|
||||||
|
await _addColumnIfMissing(
|
||||||
|
tableName: 'exercises',
|
||||||
|
columnName: 'category',
|
||||||
|
definition:
|
||||||
|
"category TEXT NOT NULL DEFAULT 'uncategorized' CHECK "
|
||||||
|
"(category IN ('shoot', 'freeThrows', 'dribble', 'finishing', "
|
||||||
|
"'conditioning', 'defense', 'mobility', 'uncategorized'))",
|
||||||
|
);
|
||||||
|
await _addColumnIfMissing(
|
||||||
|
tableName: 'exercises',
|
||||||
|
columnName: 'is_example',
|
||||||
|
definition:
|
||||||
|
'is_example INTEGER NOT NULL DEFAULT 0 '
|
||||||
|
'CHECK (is_example IN (0, 1))',
|
||||||
|
);
|
||||||
|
await _addColumnIfMissing(
|
||||||
|
tableName: 'programs',
|
||||||
|
columnName: 'is_example',
|
||||||
|
definition:
|
||||||
|
'is_example INTEGER NOT NULL DEFAULT 0 '
|
||||||
|
'CHECK (is_example IN (0, 1))',
|
||||||
|
);
|
||||||
|
await _addColumnIfMissing(
|
||||||
|
tableName: 'workout_templates',
|
||||||
|
columnName: 'is_example',
|
||||||
|
definition:
|
||||||
|
'is_example INTEGER NOT NULL DEFAULT 0 '
|
||||||
|
'CHECK (is_example IN (0, 1))',
|
||||||
|
);
|
||||||
|
await customStatement(
|
||||||
|
'CREATE TABLE IF NOT EXISTS local_seed_metadata ('
|
||||||
|
'key TEXT NOT NULL PRIMARY KEY, '
|
||||||
|
'version INTEGER NOT NULL CHECK (version >= 0), '
|
||||||
|
'applied_at INTEGER NOT NULL'
|
||||||
|
')',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _addColumnIfMissing({
|
||||||
|
required String tableName,
|
||||||
|
required String columnName,
|
||||||
|
required String definition,
|
||||||
|
}) async {
|
||||||
|
final columns = await customSelect('PRAGMA table_info($tableName)').get();
|
||||||
|
final exists = columns.any((row) => row.data['name'] == columnName);
|
||||||
|
if (!exists) {
|
||||||
|
await customStatement('ALTER TABLE $tableName ADD COLUMN $definition');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<bool> _hasColumn(String tableName, String columnName) async {
|
Future<bool> _hasColumn(String tableName, String columnName) async {
|
||||||
final rows = await customSelect('PRAGMA table_info($tableName)').get();
|
final rows = await customSelect('PRAGMA table_info($tableName)').get();
|
||||||
return rows.any((row) => row.data['name'] == columnName);
|
return rows.any((row) => row.data['name'] == columnName);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -22,6 +22,7 @@ final class DriftExerciseRepository implements ExerciseRepository {
|
|||||||
row,
|
row,
|
||||||
await _imageMediaIdsForExercise(id),
|
await _imageMediaIdsForExercise(id),
|
||||||
await _stepsForExercise(id),
|
await _stepsForExercise(id),
|
||||||
|
await _exerciseStarterMetadata(id),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,6 +42,7 @@ final class DriftExerciseRepository implements ExerciseRepository {
|
|||||||
row,
|
row,
|
||||||
await _imageMediaIdsForExercise(row.id),
|
await _imageMediaIdsForExercise(row.id),
|
||||||
await _stepsForExercise(row.id),
|
await _stepsForExercise(row.id),
|
||||||
|
await _exerciseStarterMetadata(row.id),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -73,6 +75,7 @@ final class DriftExerciseRepository implements ExerciseRepository {
|
|||||||
.into(database.exercises)
|
.into(database.exercises)
|
||||||
.insertOnConflictUpdate(_exerciseCompanion(exercise)),
|
.insertOnConflictUpdate(_exerciseCompanion(exercise)),
|
||||||
);
|
);
|
||||||
|
await _writeExerciseStarterMetadata(database, exercise);
|
||||||
await _replaceExerciseImages(database, exercise);
|
await _replaceExerciseImages(database, exercise);
|
||||||
await _replaceExerciseSteps(database, exercise);
|
await _replaceExerciseSteps(database, exercise);
|
||||||
});
|
});
|
||||||
@ -103,6 +106,113 @@ final class DriftExerciseRepository implements ExerciseRepository {
|
|||||||
.get();
|
.get();
|
||||||
return rows.map(_exerciseStepFromRow).toList();
|
return rows.map(_exerciseStepFromRow).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<_ExerciseStarterMetadata> _exerciseStarterMetadata(
|
||||||
|
String exerciseId,
|
||||||
|
) async {
|
||||||
|
final row = await database
|
||||||
|
.customSelect(
|
||||||
|
'SELECT category, is_example FROM exercises WHERE id = ? LIMIT 1',
|
||||||
|
variables: [Variable<String>(exerciseId)],
|
||||||
|
)
|
||||||
|
.getSingleOrNull();
|
||||||
|
if (row == null) {
|
||||||
|
return const _ExerciseStarterMetadata();
|
||||||
|
}
|
||||||
|
return _ExerciseStarterMetadata(
|
||||||
|
category: _exerciseCategoryFromDb(
|
||||||
|
row.readNullable<String>('category') ?? 'uncategorized',
|
||||||
|
),
|
||||||
|
isExample: (row.readNullable<int>('is_example') ?? 0) == 1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class DriftStarterSeedRepository
|
||||||
|
implements StarterSeedStateRepository, StarterContentRepository {
|
||||||
|
const DriftStarterSeedRepository(this.database);
|
||||||
|
|
||||||
|
static const _starterSeedKey = 'starter';
|
||||||
|
|
||||||
|
final db.AppDatabase database;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<int> readAppliedStarterSeedVersion() async {
|
||||||
|
final row = await database
|
||||||
|
.customSelect(
|
||||||
|
'SELECT version FROM local_seed_metadata WHERE key = ? LIMIT 1',
|
||||||
|
variables: [Variable<String>(_starterSeedKey)],
|
||||||
|
)
|
||||||
|
.getSingleOrNull();
|
||||||
|
return row?.read<int>('version') ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> writeAppliedStarterSeedVersion(
|
||||||
|
int version,
|
||||||
|
DateTime appliedAt,
|
||||||
|
) async {
|
||||||
|
await database.customStatement(
|
||||||
|
'INSERT INTO local_seed_metadata (key, version, applied_at) '
|
||||||
|
'VALUES (?, ?, ?) '
|
||||||
|
'ON CONFLICT(key) DO UPDATE SET '
|
||||||
|
'version = excluded.version, applied_at = excluded.applied_at',
|
||||||
|
[_starterSeedKey, version, appliedAt.toUtc().millisecondsSinceEpoch],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> isLocalContentEmpty() async {
|
||||||
|
final exerciseCount = await _tableCount(database.exercises.actualTableName);
|
||||||
|
final programCount = await _tableCount(database.programs.actualTableName);
|
||||||
|
final templateCount = await _tableCount(
|
||||||
|
database.workoutTemplates.actualTableName,
|
||||||
|
);
|
||||||
|
return exerciseCount == 0 && programCount == 0 && templateCount == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> insertStarterContent(StarterContent content) async {
|
||||||
|
await database.transaction(() async {
|
||||||
|
for (final exercise in content.exercises) {
|
||||||
|
await database
|
||||||
|
.into(database.exercises)
|
||||||
|
.insertOnConflictUpdate(_exerciseCompanion(exercise));
|
||||||
|
await _writeExerciseStarterMetadata(database, exercise);
|
||||||
|
await _replaceExerciseSteps(database, exercise);
|
||||||
|
}
|
||||||
|
await database
|
||||||
|
.into(database.programs)
|
||||||
|
.insertOnConflictUpdate(_programCompanion(content.program));
|
||||||
|
await _writeProgramStarterMetadata(database, content.program);
|
||||||
|
for (final exercise in content.program.exercises) {
|
||||||
|
await database
|
||||||
|
.into(database.programExercises)
|
||||||
|
.insertOnConflictUpdate(_programExerciseCompanion(exercise));
|
||||||
|
}
|
||||||
|
await database
|
||||||
|
.into(database.workoutTemplates)
|
||||||
|
.insertOnConflictUpdate(
|
||||||
|
_workoutTemplateCompanion(content.workoutTemplate),
|
||||||
|
);
|
||||||
|
await _writeWorkoutTemplateStarterMetadata(
|
||||||
|
database,
|
||||||
|
content.workoutTemplate,
|
||||||
|
);
|
||||||
|
for (final program in content.workoutTemplate.programs) {
|
||||||
|
await database
|
||||||
|
.into(database.workoutTemplatePrograms)
|
||||||
|
.insertOnConflictUpdate(_workoutTemplateProgramCompanion(program));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> _tableCount(String tableName) async {
|
||||||
|
final row = await database
|
||||||
|
.customSelect('SELECT COUNT(*) AS count FROM $tableName')
|
||||||
|
.getSingle();
|
||||||
|
return row.read<int>('count');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final class DriftMediaAssetRepository implements MediaAssetRepository {
|
final class DriftMediaAssetRepository implements MediaAssetRepository {
|
||||||
@ -381,11 +491,11 @@ final class DriftLocalSyncChangeRepository
|
|||||||
Future<bool> _applyRemotePayload(RemoteSyncedItem item) async {
|
Future<bool> _applyRemotePayload(RemoteSyncedItem item) async {
|
||||||
switch (item.resourceType) {
|
switch (item.resourceType) {
|
||||||
case SyncResourceType.exercise:
|
case SyncResourceType.exercise:
|
||||||
|
final exercise = _exerciseFromPayload(item);
|
||||||
await database
|
await database
|
||||||
.into(database.exercises)
|
.into(database.exercises)
|
||||||
.insertOnConflictUpdate(
|
.insertOnConflictUpdate(_exerciseCompanion(exercise));
|
||||||
_exerciseCompanion(_exerciseFromPayload(item)),
|
await _writeExerciseStarterMetadata(database, exercise);
|
||||||
);
|
|
||||||
return true;
|
return true;
|
||||||
case SyncResourceType.mediaAsset:
|
case SyncResourceType.mediaAsset:
|
||||||
await database
|
await database
|
||||||
@ -400,6 +510,7 @@ final class DriftLocalSyncChangeRepository
|
|||||||
await database
|
await database
|
||||||
.into(database.programs)
|
.into(database.programs)
|
||||||
.insertOnConflictUpdate(_programCompanion(program));
|
.insertOnConflictUpdate(_programCompanion(program));
|
||||||
|
await _writeProgramStarterMetadata(database, program);
|
||||||
for (final exercise in program.exercises) {
|
for (final exercise in program.exercises) {
|
||||||
await database
|
await database
|
||||||
.into(database.programExercises)
|
.into(database.programExercises)
|
||||||
@ -413,6 +524,7 @@ final class DriftLocalSyncChangeRepository
|
|||||||
await database
|
await database
|
||||||
.into(database.workoutTemplates)
|
.into(database.workoutTemplates)
|
||||||
.insertOnConflictUpdate(_workoutTemplateCompanion(template));
|
.insertOnConflictUpdate(_workoutTemplateCompanion(template));
|
||||||
|
await _writeWorkoutTemplateStarterMetadata(database, template);
|
||||||
for (final program in template.programs) {
|
for (final program in template.programs) {
|
||||||
await database
|
await database
|
||||||
.into(database.workoutTemplatePrograms)
|
.into(database.workoutTemplatePrograms)
|
||||||
@ -442,6 +554,7 @@ final class DriftLocalSyncChangeRepository
|
|||||||
row,
|
row,
|
||||||
await DriftExerciseRepository(database)._imageMediaIdsForExercise(id),
|
await DriftExerciseRepository(database)._imageMediaIdsForExercise(id),
|
||||||
await DriftExerciseRepository(database)._stepsForExercise(id),
|
await DriftExerciseRepository(database)._stepsForExercise(id),
|
||||||
|
await DriftExerciseRepository(database)._exerciseStarterMetadata(id),
|
||||||
);
|
);
|
||||||
return _LocalSyncSnapshot.fromMetadata(
|
return _LocalSyncSnapshot.fromMetadata(
|
||||||
exercise.metadata,
|
exercise.metadata,
|
||||||
@ -666,6 +779,7 @@ final class DriftProgramRepository implements ProgramRepository {
|
|||||||
return _programFromRow(
|
return _programFromRow(
|
||||||
row,
|
row,
|
||||||
exercises.map(_programExerciseFromRow).toList(),
|
exercises.map(_programExerciseFromRow).toList(),
|
||||||
|
isExample: await _isProgramExample(id),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -687,7 +801,11 @@ final class DriftProgramRepository implements ProgramRepository {
|
|||||||
..orderBy([(table) => OrderingTerm.asc(table.position)]))
|
..orderBy([(table) => OrderingTerm.asc(table.position)]))
|
||||||
.get();
|
.get();
|
||||||
programs.add(
|
programs.add(
|
||||||
_programFromRow(row, exercises.map(_programExerciseFromRow).toList()),
|
_programFromRow(
|
||||||
|
row,
|
||||||
|
exercises.map(_programExerciseFromRow).toList(),
|
||||||
|
isExample: await _isProgramExample(row.id),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return programs;
|
return programs;
|
||||||
@ -705,6 +823,7 @@ final class DriftProgramRepository implements ProgramRepository {
|
|||||||
.into(database.programs)
|
.into(database.programs)
|
||||||
.insertOnConflictUpdate(_programCompanion(program)),
|
.insertOnConflictUpdate(_programCompanion(program)),
|
||||||
);
|
);
|
||||||
|
await _writeProgramStarterMetadata(database, program);
|
||||||
for (final exercise in program.exercises) {
|
for (final exercise in program.exercises) {
|
||||||
await saveExercise(exercise);
|
await saveExercise(exercise);
|
||||||
}
|
}
|
||||||
@ -756,6 +875,16 @@ final class DriftProgramRepository implements ProgramRepository {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> _isProgramExample(String id) async {
|
||||||
|
final row = await database
|
||||||
|
.customSelect(
|
||||||
|
'SELECT is_example FROM programs WHERE id = ? LIMIT 1',
|
||||||
|
variables: [Variable<String>(id)],
|
||||||
|
)
|
||||||
|
.getSingleOrNull();
|
||||||
|
return (row?.readNullable<int>('is_example') ?? 0) == 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final class DriftWorkoutTemplateRepository
|
final class DriftWorkoutTemplateRepository
|
||||||
@ -797,6 +926,7 @@ final class DriftWorkoutTemplateRepository
|
|||||||
row,
|
row,
|
||||||
programs.map(_workoutTemplateProgramFromRow).toList(),
|
programs.map(_workoutTemplateProgramFromRow).toList(),
|
||||||
overrides.map(_workoutTemplateExerciseOverrideFromRow).toList(),
|
overrides.map(_workoutTemplateExerciseOverrideFromRow).toList(),
|
||||||
|
isExample: await _isWorkoutTemplateExample(id),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -829,6 +959,7 @@ final class DriftWorkoutTemplateRepository
|
|||||||
.into(database.workoutTemplates)
|
.into(database.workoutTemplates)
|
||||||
.insertOnConflictUpdate(_workoutTemplateCompanion(template)),
|
.insertOnConflictUpdate(_workoutTemplateCompanion(template)),
|
||||||
);
|
);
|
||||||
|
await _writeWorkoutTemplateStarterMetadata(database, template);
|
||||||
for (final program in template.programs) {
|
for (final program in template.programs) {
|
||||||
await saveProgram(program);
|
await saveProgram(program);
|
||||||
}
|
}
|
||||||
@ -942,6 +1073,16 @@ final class DriftWorkoutTemplateRepository
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> _isWorkoutTemplateExample(String id) async {
|
||||||
|
final row = await database
|
||||||
|
.customSelect(
|
||||||
|
'SELECT is_example FROM workout_templates WHERE id = ? LIMIT 1',
|
||||||
|
variables: [Variable<String>(id)],
|
||||||
|
)
|
||||||
|
.getSingleOrNull();
|
||||||
|
return (row?.readNullable<int>('is_example') ?? 0) == 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final class DriftActiveSessionRepository implements ActiveSessionRepository {
|
final class DriftActiveSessionRepository implements ActiveSessionRepository {
|
||||||
@ -1681,6 +1822,50 @@ String _exerciseImageId(String exerciseId, String mediaId) {
|
|||||||
return 'exercise-image:$exerciseId:$mediaId';
|
return 'exercise-image:$exerciseId:$mediaId';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final class _ExerciseStarterMetadata {
|
||||||
|
const _ExerciseStarterMetadata({
|
||||||
|
this.category = domain.ExerciseCategory.uncategorized,
|
||||||
|
this.isExample = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
final domain.ExerciseCategory category;
|
||||||
|
final bool isExample;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _writeExerciseStarterMetadata(
|
||||||
|
db.AppDatabase database,
|
||||||
|
domain.Exercise exercise,
|
||||||
|
) async {
|
||||||
|
await database.customStatement(
|
||||||
|
'UPDATE exercises SET category = ?, is_example = ? WHERE id = ?',
|
||||||
|
[
|
||||||
|
_exerciseCategoryToDb(exercise.category),
|
||||||
|
exercise.isExample ? 1 : 0,
|
||||||
|
exercise.metadata.id,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _writeProgramStarterMetadata(
|
||||||
|
db.AppDatabase database,
|
||||||
|
domain.Program program,
|
||||||
|
) async {
|
||||||
|
await database.customStatement(
|
||||||
|
'UPDATE programs SET is_example = ? WHERE id = ?',
|
||||||
|
[program.isExample ? 1 : 0, program.metadata.id],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _writeWorkoutTemplateStarterMetadata(
|
||||||
|
db.AppDatabase database,
|
||||||
|
domain.WorkoutTemplate template,
|
||||||
|
) async {
|
||||||
|
await database.customStatement(
|
||||||
|
'UPDATE workout_templates SET is_example = ? WHERE id = ?',
|
||||||
|
[template.isExample ? 1 : 0, template.metadata.id],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<String> _operationForMutation({
|
Future<String> _operationForMutation({
|
||||||
required db.AppDatabase database,
|
required db.AppDatabase database,
|
||||||
required String tableName,
|
required String tableName,
|
||||||
@ -1994,6 +2179,7 @@ domain.Exercise _exerciseFromRow(
|
|||||||
db.Exercise row,
|
db.Exercise row,
|
||||||
List<String> imageMediaIds,
|
List<String> imageMediaIds,
|
||||||
List<domain.ExerciseStep> steps,
|
List<domain.ExerciseStep> steps,
|
||||||
|
_ExerciseStarterMetadata starterMetadata,
|
||||||
) {
|
) {
|
||||||
return domain.Exercise(
|
return domain.Exercise(
|
||||||
metadata: _metadataFromRow(row),
|
metadata: _metadataFromRow(row),
|
||||||
@ -2013,6 +2199,8 @@ domain.Exercise _exerciseFromRow(
|
|||||||
defaultTargetScore: row.defaultTargetScore,
|
defaultTargetScore: row.defaultTargetScore,
|
||||||
defaultTargetScoreTimeMs: row.defaultTargetScoreTimeMs,
|
defaultTargetScoreTimeMs: row.defaultTargetScoreTimeMs,
|
||||||
autoStartNextTimedStep: row.autoStartNextTimedStep,
|
autoStartNextTimedStep: row.autoStartNextTimedStep,
|
||||||
|
category: starterMetadata.category,
|
||||||
|
isExample: starterMetadata.isExample,
|
||||||
steps: steps,
|
steps: steps,
|
||||||
archivedAt: _utcOrNull(row.archivedAt),
|
archivedAt: _utcOrNull(row.archivedAt),
|
||||||
);
|
);
|
||||||
@ -2200,12 +2388,14 @@ db.ProgramsCompanion _programCompanion(domain.Program program) {
|
|||||||
|
|
||||||
domain.Program _programFromRow(
|
domain.Program _programFromRow(
|
||||||
db.Program row,
|
db.Program row,
|
||||||
List<domain.ProgramExercise> exercises,
|
List<domain.ProgramExercise> exercises, {
|
||||||
) {
|
bool isExample = false,
|
||||||
|
}) {
|
||||||
return domain.Program(
|
return domain.Program(
|
||||||
metadata: _metadataFromRow(row),
|
metadata: _metadataFromRow(row),
|
||||||
name: row.name,
|
name: row.name,
|
||||||
defaultRestSeconds: row.defaultRestSeconds,
|
defaultRestSeconds: row.defaultRestSeconds,
|
||||||
|
isExample: isExample,
|
||||||
exercises: exercises,
|
exercises: exercises,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -2328,12 +2518,14 @@ db.WorkoutTemplatesCompanion _workoutTemplateCompanion(
|
|||||||
domain.WorkoutTemplate _workoutTemplateFromRow(
|
domain.WorkoutTemplate _workoutTemplateFromRow(
|
||||||
db.WorkoutTemplate row,
|
db.WorkoutTemplate row,
|
||||||
List<domain.WorkoutTemplateProgram> programs,
|
List<domain.WorkoutTemplateProgram> programs,
|
||||||
List<domain.WorkoutTemplateExerciseOverride> overrides,
|
List<domain.WorkoutTemplateExerciseOverride> overrides, {
|
||||||
) {
|
bool isExample = false,
|
||||||
|
}) {
|
||||||
return domain.WorkoutTemplate(
|
return domain.WorkoutTemplate(
|
||||||
metadata: _metadataFromRow(row),
|
metadata: _metadataFromRow(row),
|
||||||
name: row.name,
|
name: row.name,
|
||||||
lastStartedAt: _utcOrNull(row.lastStartedAt),
|
lastStartedAt: _utcOrNull(row.lastStartedAt),
|
||||||
|
isExample: isExample,
|
||||||
programs: programs,
|
programs: programs,
|
||||||
overrides: overrides,
|
overrides: overrides,
|
||||||
);
|
);
|
||||||
@ -3146,6 +3338,8 @@ Map<String, Object?> _exercisePayload(domain.Exercise exercise) => {
|
|||||||
'defaultTargetScore': exercise.defaultTargetScore,
|
'defaultTargetScore': exercise.defaultTargetScore,
|
||||||
'defaultTargetScoreTimeMs': exercise.defaultTargetScoreTimeMs,
|
'defaultTargetScoreTimeMs': exercise.defaultTargetScoreTimeMs,
|
||||||
'autoStartNextTimedStep': exercise.autoStartNextTimedStep,
|
'autoStartNextTimedStep': exercise.autoStartNextTimedStep,
|
||||||
|
'category': exercise.category.name,
|
||||||
|
'isExample': exercise.isExample,
|
||||||
'steps': exercise.steps.map((step) => step.toSnapshotJson()).toList(),
|
'steps': exercise.steps.map((step) => step.toSnapshotJson()).toList(),
|
||||||
'archivedAt': exercise.archivedAt?.toUtc().toIso8601String(),
|
'archivedAt': exercise.archivedAt?.toUtc().toIso8601String(),
|
||||||
};
|
};
|
||||||
@ -3170,6 +3364,7 @@ Map<String, Object?> _programPayload(domain.Program program) => {
|
|||||||
'id': program.metadata.id,
|
'id': program.metadata.id,
|
||||||
'name': program.name,
|
'name': program.name,
|
||||||
'defaultRestSeconds': program.defaultRestSeconds,
|
'defaultRestSeconds': program.defaultRestSeconds,
|
||||||
|
'isExample': program.isExample,
|
||||||
'exercises': program.exercises
|
'exercises': program.exercises
|
||||||
.map((exercise) => exercise.toSnapshotJson())
|
.map((exercise) => exercise.toSnapshotJson())
|
||||||
.toList(),
|
.toList(),
|
||||||
@ -3181,6 +3376,7 @@ Map<String, Object?> _workoutTemplatePayload(domain.WorkoutTemplate template) =>
|
|||||||
'id': template.metadata.id,
|
'id': template.metadata.id,
|
||||||
'name': template.name,
|
'name': template.name,
|
||||||
'lastStartedAt': template.lastStartedAt?.toUtc().toIso8601String(),
|
'lastStartedAt': template.lastStartedAt?.toUtc().toIso8601String(),
|
||||||
|
'isExample': template.isExample,
|
||||||
'programs': template.programs
|
'programs': template.programs
|
||||||
.map(
|
.map(
|
||||||
(program) => {
|
(program) => {
|
||||||
@ -3246,6 +3442,10 @@ domain.Exercise _exerciseFromPayload(RemoteSyncedItem item) {
|
|||||||
defaultTargetScore: (payload['defaultTargetScore'] as num?)?.toDouble(),
|
defaultTargetScore: (payload['defaultTargetScore'] as num?)?.toDouble(),
|
||||||
defaultTargetScoreTimeMs: payload['defaultTargetScoreTimeMs'] as int?,
|
defaultTargetScoreTimeMs: payload['defaultTargetScoreTimeMs'] as int?,
|
||||||
autoStartNextTimedStep: payload['autoStartNextTimedStep'] as bool? ?? true,
|
autoStartNextTimedStep: payload['autoStartNextTimedStep'] as bool? ?? true,
|
||||||
|
category: _exerciseCategoryFromDb(
|
||||||
|
payload['category'] as String? ?? 'uncategorized',
|
||||||
|
),
|
||||||
|
isExample: payload['isExample'] as bool? ?? false,
|
||||||
steps: _stepsFromPayload(payload['steps']),
|
steps: _stepsFromPayload(payload['steps']),
|
||||||
archivedAt: _dateTimeFromPayload(payload['archivedAt']),
|
archivedAt: _dateTimeFromPayload(payload['archivedAt']),
|
||||||
);
|
);
|
||||||
@ -3275,6 +3475,7 @@ domain.Program _programFromPayload(RemoteSyncedItem item) {
|
|||||||
metadata: metadata,
|
metadata: metadata,
|
||||||
name: _stringFromPayload(payload, 'name', item.clientId),
|
name: _stringFromPayload(payload, 'name', item.clientId),
|
||||||
defaultRestSeconds: payload['defaultRestSeconds'] as int? ?? 0,
|
defaultRestSeconds: payload['defaultRestSeconds'] as int? ?? 0,
|
||||||
|
isExample: payload['isExample'] as bool? ?? false,
|
||||||
exercises: _programExercisesFromPayload(payload['exercises'], metadata),
|
exercises: _programExercisesFromPayload(payload['exercises'], metadata),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -3286,6 +3487,7 @@ domain.WorkoutTemplate _workoutTemplateFromPayload(RemoteSyncedItem item) {
|
|||||||
metadata: metadata,
|
metadata: metadata,
|
||||||
name: _stringFromPayload(payload, 'name', item.clientId),
|
name: _stringFromPayload(payload, 'name', item.clientId),
|
||||||
lastStartedAt: _dateTimeFromPayload(payload['lastStartedAt']),
|
lastStartedAt: _dateTimeFromPayload(payload['lastStartedAt']),
|
||||||
|
isExample: payload['isExample'] as bool? ?? false,
|
||||||
programs: _workoutTemplateProgramsFromPayload(
|
programs: _workoutTemplateProgramsFromPayload(
|
||||||
payload['programs'],
|
payload['programs'],
|
||||||
metadata,
|
metadata,
|
||||||
@ -3693,6 +3895,31 @@ domain.ExerciseStepType _exerciseStepTypeFromDb(String value) =>
|
|||||||
_ => throw domain.DomainException('Unknown exercise step type: $value'),
|
_ => throw domain.DomainException('Unknown exercise step type: $value'),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
String _exerciseCategoryToDb(domain.ExerciseCategory category) =>
|
||||||
|
switch (category) {
|
||||||
|
domain.ExerciseCategory.shoot => 'shoot',
|
||||||
|
domain.ExerciseCategory.freeThrows => 'freeThrows',
|
||||||
|
domain.ExerciseCategory.dribble => 'dribble',
|
||||||
|
domain.ExerciseCategory.finishing => 'finishing',
|
||||||
|
domain.ExerciseCategory.conditioning => 'conditioning',
|
||||||
|
domain.ExerciseCategory.defense => 'defense',
|
||||||
|
domain.ExerciseCategory.mobility => 'mobility',
|
||||||
|
domain.ExerciseCategory.uncategorized => 'uncategorized',
|
||||||
|
};
|
||||||
|
|
||||||
|
domain.ExerciseCategory _exerciseCategoryFromDb(String value) =>
|
||||||
|
switch (value) {
|
||||||
|
'shoot' => domain.ExerciseCategory.shoot,
|
||||||
|
'freeThrows' => domain.ExerciseCategory.freeThrows,
|
||||||
|
'dribble' => domain.ExerciseCategory.dribble,
|
||||||
|
'finishing' => domain.ExerciseCategory.finishing,
|
||||||
|
'conditioning' => domain.ExerciseCategory.conditioning,
|
||||||
|
'defense' => domain.ExerciseCategory.defense,
|
||||||
|
'mobility' => domain.ExerciseCategory.mobility,
|
||||||
|
'uncategorized' => domain.ExerciseCategory.uncategorized,
|
||||||
|
_ => throw domain.DomainException('Unknown exercise category: $value'),
|
||||||
|
};
|
||||||
|
|
||||||
String _setResultStatusToDb(domain.SetResultStatus status) => switch (status) {
|
String _setResultStatusToDb(domain.SetResultStatus status) => switch (status) {
|
||||||
domain.SetResultStatus.completed => 'completed',
|
domain.SetResultStatus.completed => 'completed',
|
||||||
domain.SetResultStatus.skipped => 'skipped',
|
domain.SetResultStatus.skipped => 'skipped',
|
||||||
@ -3776,7 +4003,7 @@ List<String> _decodeImageMediaIdsSnapshot(
|
|||||||
String? fallbackImageMediaId,
|
String? fallbackImageMediaId,
|
||||||
) {
|
) {
|
||||||
if (encoded == null || encoded.trim().isEmpty) {
|
if (encoded == null || encoded.trim().isEmpty) {
|
||||||
return [if (fallbackImageMediaId != null) fallbackImageMediaId];
|
return [?fallbackImageMediaId];
|
||||||
}
|
}
|
||||||
final decoded = jsonDecode(encoded);
|
final decoded = jsonDecode(encoded);
|
||||||
if (decoded is! List) {
|
if (decoded is! List) {
|
||||||
|
|||||||
@ -193,6 +193,9 @@ class Exercises extends SyncableTable {
|
|||||||
IntColumn get defaultTargetScoreTimeMs => integer().nullable()();
|
IntColumn get defaultTargetScoreTimeMs => integer().nullable()();
|
||||||
BoolColumn get autoStartNextTimedStep =>
|
BoolColumn get autoStartNextTimedStep =>
|
||||||
boolean().withDefault(const Constant(true))();
|
boolean().withDefault(const Constant(true))();
|
||||||
|
TextColumn get category =>
|
||||||
|
text().withDefault(const Constant('uncategorized'))();
|
||||||
|
BoolColumn get isExample => boolean().withDefault(const Constant(false))();
|
||||||
DateTimeColumn get archivedAt => dateTime().nullable()();
|
DateTimeColumn get archivedAt => dateTime().nullable()();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -209,6 +212,8 @@ class Exercises extends SyncableTable {
|
|||||||
'CHECK (default_target_score IS NULL OR default_target_score >= 0)',
|
'CHECK (default_target_score IS NULL OR default_target_score >= 0)',
|
||||||
'CHECK (default_target_score_time_ms IS NULL OR '
|
'CHECK (default_target_score_time_ms IS NULL OR '
|
||||||
'default_target_score_time_ms > 0)',
|
'default_target_score_time_ms > 0)',
|
||||||
|
"CHECK (category IN ('shoot', 'freeThrows', 'dribble', 'finishing', "
|
||||||
|
"'conditioning', 'defense', 'mobility', 'uncategorized'))",
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -278,6 +283,7 @@ class Programs extends SyncableTable {
|
|||||||
|
|
||||||
TextColumn get name => text().withLength(min: 1)();
|
TextColumn get name => text().withLength(min: 1)();
|
||||||
IntColumn get defaultRestSeconds => integer()();
|
IntColumn get defaultRestSeconds => integer()();
|
||||||
|
BoolColumn get isExample => boolean().withDefault(const Constant(false))();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<String> get customConstraints => ['CHECK (default_rest_seconds >= 0)'];
|
List<String> get customConstraints => ['CHECK (default_rest_seconds >= 0)'];
|
||||||
@ -360,6 +366,20 @@ class WorkoutTemplates extends SyncableTable {
|
|||||||
|
|
||||||
TextColumn get name => text().withLength(min: 1)();
|
TextColumn get name => text().withLength(min: 1)();
|
||||||
DateTimeColumn get lastStartedAt => dateTime().nullable()();
|
DateTimeColumn get lastStartedAt => dateTime().nullable()();
|
||||||
|
BoolColumn get isExample => boolean().withDefault(const Constant(false))();
|
||||||
|
}
|
||||||
|
|
||||||
|
class LocalSeedMetadata extends Table {
|
||||||
|
@override
|
||||||
|
String get tableName => 'local_seed_metadata';
|
||||||
|
|
||||||
|
TextColumn get key => text()();
|
||||||
|
IntColumn get version =>
|
||||||
|
integer().customConstraint('NOT NULL CHECK (version >= 0)')();
|
||||||
|
DateTimeColumn get appliedAt => dateTime()();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Set<Column> get primaryKey => {key};
|
||||||
}
|
}
|
||||||
|
|
||||||
class WorkoutTemplatePrograms extends SyncableTable {
|
class WorkoutTemplatePrograms extends SyncableTable {
|
||||||
@ -653,7 +673,7 @@ class ActiveExerciseStepResults extends SyncableTable {
|
|||||||
'CHECK (step_index >= 0)',
|
'CHECK (step_index >= 0)',
|
||||||
"CHECK (step_type_snapshot IN ('time', 'reps'))",
|
"CHECK (step_type_snapshot IN ('time', 'reps'))",
|
||||||
'CHECK (target_value_snapshot > 0)',
|
'CHECK (target_value_snapshot > 0)',
|
||||||
"CHECK (score_input_mode_snapshot IS NULL OR "
|
'CHECK (score_input_mode_snapshot IS NULL OR '
|
||||||
"score_input_mode_snapshot IN ('manual', 'stopwatch'))",
|
"score_input_mode_snapshot IN ('manual', 'stopwatch'))",
|
||||||
"CHECK (status IN ('completed', 'skipped'))",
|
"CHECK (status IN ('completed', 'skipped'))",
|
||||||
'CHECK (actual_time_ms IS NULL OR actual_time_ms >= 0)',
|
'CHECK (actual_time_ms IS NULL OR actual_time_ms >= 0)',
|
||||||
@ -813,7 +833,7 @@ class WorkoutHistoryStepResults extends SyncableTable {
|
|||||||
'CHECK (step_index >= 0)',
|
'CHECK (step_index >= 0)',
|
||||||
"CHECK (step_type_snapshot IN ('time', 'reps'))",
|
"CHECK (step_type_snapshot IN ('time', 'reps'))",
|
||||||
'CHECK (target_value_snapshot > 0)',
|
'CHECK (target_value_snapshot > 0)',
|
||||||
"CHECK (score_input_mode_snapshot IS NULL OR "
|
'CHECK (score_input_mode_snapshot IS NULL OR '
|
||||||
"score_input_mode_snapshot IN ('manual', 'stopwatch'))",
|
"score_input_mode_snapshot IN ('manual', 'stopwatch'))",
|
||||||
"CHECK (status IN ('completed', 'skipped'))",
|
"CHECK (status IN ('completed', 'skipped'))",
|
||||||
'CHECK (actual_time_ms IS NULL OR actual_time_ms >= 0)',
|
'CHECK (actual_time_ms IS NULL OR actual_time_ms >= 0)',
|
||||||
|
|||||||
@ -98,6 +98,151 @@ void main() {
|
|||||||
expect(restored!.defaultTargetScore, 0);
|
expect(restored!.defaultTargetScore, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('starter seed populates a fresh database once', () async {
|
||||||
|
final seedRepository = local.DriftStarterSeedRepository(database);
|
||||||
|
final result = await SeedStarterContentUseCase(
|
||||||
|
seedStateRepository: seedRepository,
|
||||||
|
contentRepository: seedRepository,
|
||||||
|
clock: _FakeClock(DateTime.utc(2026, 7, 21, 8)),
|
||||||
|
originDeviceId: 'local-device',
|
||||||
|
).run();
|
||||||
|
|
||||||
|
expect(result.status, StarterSeedStatus.inserted);
|
||||||
|
expect(await seedRepository.readAppliedStarterSeedVersion(), 1);
|
||||||
|
|
||||||
|
final exercises = await exerciseRepository.listActive();
|
||||||
|
final programs = await programRepository.listActive();
|
||||||
|
final templates = await templateRepository.listActive();
|
||||||
|
expect(exercises, hasLength(21));
|
||||||
|
expect(programs, hasLength(1));
|
||||||
|
expect(templates, hasLength(1));
|
||||||
|
expect(exercises.every((exercise) => exercise.isExample), isTrue);
|
||||||
|
expect(programs.single.isExample, isTrue);
|
||||||
|
expect(templates.single.isExample, isTrue);
|
||||||
|
expect(exercises.map((exercise) => exercise.category).toSet(), {
|
||||||
|
ExerciseCategory.shoot,
|
||||||
|
ExerciseCategory.freeThrows,
|
||||||
|
ExerciseCategory.dribble,
|
||||||
|
ExerciseCategory.finishing,
|
||||||
|
ExerciseCategory.conditioning,
|
||||||
|
ExerciseCategory.defense,
|
||||||
|
ExerciseCategory.mobility,
|
||||||
|
});
|
||||||
|
|
||||||
|
final secondRun = await SeedStarterContentUseCase(
|
||||||
|
seedStateRepository: seedRepository,
|
||||||
|
contentRepository: seedRepository,
|
||||||
|
clock: _FakeClock(DateTime.utc(2026, 7, 21, 9)),
|
||||||
|
originDeviceId: 'local-device',
|
||||||
|
).run();
|
||||||
|
expect(secondRun.status, StarterSeedStatus.skippedAlreadyApplied);
|
||||||
|
expect(await exerciseRepository.listActive(), hasLength(21));
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'starter seed marks non-empty database without inserting examples',
|
||||||
|
() async {
|
||||||
|
final now = DateTime.utc(2026, 7, 21, 8);
|
||||||
|
await exerciseRepository.save(
|
||||||
|
Exercise(
|
||||||
|
metadata: _metadata('user-exercise-1', now),
|
||||||
|
name: 'Exercice utilisateur',
|
||||||
|
hasTimeMeasure: false,
|
||||||
|
hasRepsMeasure: true,
|
||||||
|
hasScoreMeasure: false,
|
||||||
|
defaultTargetReps: 10,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final seedRepository = local.DriftStarterSeedRepository(database);
|
||||||
|
|
||||||
|
final result = await SeedStarterContentUseCase(
|
||||||
|
seedStateRepository: seedRepository,
|
||||||
|
contentRepository: seedRepository,
|
||||||
|
clock: _FakeClock(now),
|
||||||
|
originDeviceId: 'local-device',
|
||||||
|
).run();
|
||||||
|
|
||||||
|
expect(result.status, StarterSeedStatus.skippedNotEmpty);
|
||||||
|
expect(await seedRepository.readAppliedStarterSeedVersion(), 1);
|
||||||
|
expect(await exerciseRepository.listActive(), hasLength(1));
|
||||||
|
expect(await programRepository.listActive(), isEmpty);
|
||||||
|
expect(await templateRepository.listActive(), isEmpty);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test('starter seed does not reappear after example deletion', () async {
|
||||||
|
final seedRepository = local.DriftStarterSeedRepository(database);
|
||||||
|
await SeedStarterContentUseCase(
|
||||||
|
seedStateRepository: seedRepository,
|
||||||
|
contentRepository: seedRepository,
|
||||||
|
clock: _FakeClock(DateTime.utc(2026, 7, 21, 8)),
|
||||||
|
originDeviceId: 'local-device',
|
||||||
|
).run();
|
||||||
|
|
||||||
|
final exercise = (await exerciseRepository.listActive()).first;
|
||||||
|
await exerciseRepository.save(
|
||||||
|
exercise.copyWith(
|
||||||
|
metadata: exercise.metadata.markDeleted(DateTime.utc(2026, 7, 21, 9)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await SeedStarterContentUseCase(
|
||||||
|
seedStateRepository: seedRepository,
|
||||||
|
contentRepository: seedRepository,
|
||||||
|
clock: _FakeClock(DateTime.utc(2026, 7, 21, 10)),
|
||||||
|
originDeviceId: 'local-device',
|
||||||
|
).run();
|
||||||
|
|
||||||
|
expect(await exerciseRepository.listActive(), hasLength(20));
|
||||||
|
expect(await exerciseRepository.findById(exercise.metadata.id), isNotNull);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'starter program and workout template snapshot exercise steps',
|
||||||
|
() async {
|
||||||
|
final seedRepository = local.DriftStarterSeedRepository(database);
|
||||||
|
await SeedStarterContentUseCase(
|
||||||
|
seedStateRepository: seedRepository,
|
||||||
|
contentRepository: seedRepository,
|
||||||
|
clock: _FakeClock(DateTime.utc(2026, 7, 21, 8)),
|
||||||
|
originDeviceId: 'local-device',
|
||||||
|
).run();
|
||||||
|
|
||||||
|
final program = (await programRepository.listActive()).single;
|
||||||
|
expect(program.name, 'Fondations basket - 45 min');
|
||||||
|
expect(program.exercises, hasLength(8));
|
||||||
|
expect(
|
||||||
|
program.exercises
|
||||||
|
.map((exercise) => exercise.setsCount)
|
||||||
|
.reduce((total, count) => total + count),
|
||||||
|
22,
|
||||||
|
);
|
||||||
|
final spotShooting = program.exercises.singleWhere(
|
||||||
|
(exercise) =>
|
||||||
|
exercise.exerciseNameSnapshot == 'Spot shooting 5 positions',
|
||||||
|
);
|
||||||
|
expect(spotShooting.exerciseStepsSnapshot, hasLength(5));
|
||||||
|
expect(spotShooting.exerciseStepsSnapshot.map((step) => step.name), [
|
||||||
|
'Coin droit',
|
||||||
|
'Aile droite',
|
||||||
|
'Face cercle',
|
||||||
|
'Aile gauche',
|
||||||
|
'Coin gauche',
|
||||||
|
]);
|
||||||
|
|
||||||
|
final template = (await templateRepository.listActive()).single;
|
||||||
|
expect(template.name, 'Séance exemple - Fondations basket');
|
||||||
|
expect(template.programs, hasLength(1));
|
||||||
|
expect(template.programs.single.sourceProgramId, program.metadata.id);
|
||||||
|
final snapshot =
|
||||||
|
jsonDecode(template.programs.single.programSnapshotJson)
|
||||||
|
as Map<String, Object?>;
|
||||||
|
expect(snapshot['programId'], program.metadata.id);
|
||||||
|
expect(snapshot['name'], program.name);
|
||||||
|
expect(snapshot['exercises'], isA<List<Object?>>());
|
||||||
|
expect(snapshot['exercises'] as List<Object?>, hasLength(8));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'step chaining settings round-trip through drift repositories',
|
'step chaining settings round-trip through drift repositories',
|
||||||
() async {
|
() async {
|
||||||
|
|||||||
Reference in New Issue
Block a user