feat(server): implemente synchronisation et serveur avec fixtures (#187)
- Implémente la couche de synchronisation avec le serveur - Ajoute les fixtures versionnées pour les tests - Met à jour Drift database et repositories pour le support sync - Améliore les tests de synchronisation - Corrige et améliore le watch companion pour la collecte de métriques Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -144,6 +144,7 @@ final class AppBootstrap implements AppDependencies {
|
||||
);
|
||||
final workoutTelemetryUseCases = WorkoutTelemetryUseCases(
|
||||
repository: telemetryRepository,
|
||||
sessionRepository: activeSessionRepository,
|
||||
clock: clock,
|
||||
ids: ids,
|
||||
);
|
||||
|
||||
@ -7,6 +7,7 @@ library;
|
||||
export 'ports.dart';
|
||||
export 'session_notification_use_cases.dart';
|
||||
export 'starter_content/basket_starter_seed_v1.dart';
|
||||
export 'starter_content/basket_starter_seed_v2.dart';
|
||||
export 'starter_content/starter_content.dart';
|
||||
export 'use_cases.dart';
|
||||
export 'watch_companion_use_cases.dart';
|
||||
|
||||
@ -938,6 +938,9 @@ abstract interface class WorkoutHistoryRepository {
|
||||
|
||||
abstract interface class WorkoutTelemetryRepository {
|
||||
Future<bool> saveSample(WorkoutTelemetrySample sample);
|
||||
Future<ActiveWorkoutTelemetryWindowState?> findWindowState(String sessionId);
|
||||
Future<void> saveWindowState(ActiveWorkoutTelemetryWindowState state);
|
||||
Future<void> deleteWindowState(String sessionId);
|
||||
Future<List<WorkoutTelemetrySample>> listSamples(String sessionId);
|
||||
Future<List<WorkoutTelemetrySample>> listSamplesForScope({
|
||||
required String sessionId,
|
||||
|
||||
570
lib/application/starter_content/basket_starter_seed_v2.dart
Normal file
570
lib/application/starter_content/basket_starter_seed_v2.dart
Normal file
@ -0,0 +1,570 @@
|
||||
import '../../domain/domain.dart';
|
||||
import 'starter_content.dart';
|
||||
|
||||
const basketStarterExerciseSeedsV2 = [
|
||||
StarterExerciseSeed(
|
||||
id: 'starter-v2-exercise-form-shooting',
|
||||
name: 'Form shooting proche panier',
|
||||
description: 'Travail technique près du cercle, priorité au geste propre.',
|
||||
businessTypes: [BusinessExerciseType.shoot],
|
||||
tags: ['tir', 'technique'],
|
||||
hasRepsMeasure: true,
|
||||
hasScoreMeasure: true,
|
||||
scoreLabel: 'Réussites',
|
||||
scoreUnit: 'paniers',
|
||||
defaultTargetReps: 25,
|
||||
defaultTargetScore: 20,
|
||||
),
|
||||
StarterExerciseSeed(
|
||||
id: 'starter-v2-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.',
|
||||
businessTypes: [BusinessExerciseType.shoot],
|
||||
tags: ['tir', 'spots'],
|
||||
hasRepsMeasure: true,
|
||||
hasScoreMeasure: true,
|
||||
scoreLabel: 'Réussites',
|
||||
scoreUnit: 'paniers',
|
||||
defaultTargetReps: 1,
|
||||
defaultTargetScore: 15,
|
||||
steps: [
|
||||
StarterExerciseStepSeed(
|
||||
id: 'starter-v2-step-spot-shooting-right-corner',
|
||||
name: 'Coin droit',
|
||||
type: ExerciseStepType.reps,
|
||||
defaultTargetValue: 5,
|
||||
),
|
||||
StarterExerciseStepSeed(
|
||||
id: 'starter-v2-step-spot-shooting-right-wing',
|
||||
name: 'Aile droite',
|
||||
type: ExerciseStepType.reps,
|
||||
defaultTargetValue: 5,
|
||||
),
|
||||
StarterExerciseStepSeed(
|
||||
id: 'starter-v2-step-spot-shooting-top',
|
||||
name: 'Face cercle',
|
||||
type: ExerciseStepType.reps,
|
||||
defaultTargetValue: 5,
|
||||
),
|
||||
StarterExerciseStepSeed(
|
||||
id: 'starter-v2-step-spot-shooting-left-wing',
|
||||
name: 'Aile gauche',
|
||||
type: ExerciseStepType.reps,
|
||||
defaultTargetValue: 5,
|
||||
),
|
||||
StarterExerciseStepSeed(
|
||||
id: 'starter-v2-step-spot-shooting-left-corner',
|
||||
name: 'Coin gauche',
|
||||
type: ExerciseStepType.reps,
|
||||
defaultTargetValue: 5,
|
||||
),
|
||||
],
|
||||
),
|
||||
StarterExerciseSeed(
|
||||
id: 'starter-v2-exercise-free-throw-routine',
|
||||
name: 'Routine lancers francs',
|
||||
description: 'Série calme avec routine complète avant chaque tir.',
|
||||
businessTypes: [BusinessExerciseType.freeThrows],
|
||||
tags: ['lancers francs', 'routine'],
|
||||
hasRepsMeasure: true,
|
||||
hasScoreMeasure: true,
|
||||
scoreLabel: 'Réussites',
|
||||
scoreUnit: 'lancers',
|
||||
defaultTargetReps: 20,
|
||||
defaultTargetScore: 16,
|
||||
),
|
||||
StarterExerciseSeed(
|
||||
id: 'starter-v2-exercise-fatigue-free-throws',
|
||||
name: 'Lancers francs sous fatigue',
|
||||
description: 'Lancers francs après effort court.',
|
||||
businessTypes: [
|
||||
BusinessExerciseType.freeThrows,
|
||||
BusinessExerciseType.highIntensity,
|
||||
],
|
||||
tags: ['lancers francs', 'fatigue'],
|
||||
hasRepsMeasure: true,
|
||||
hasScoreMeasure: true,
|
||||
scoreLabel: 'Réussites',
|
||||
scoreUnit: 'lancers',
|
||||
defaultTargetReps: 5,
|
||||
defaultTargetScore: 8,
|
||||
steps: [
|
||||
StarterExerciseStepSeed(
|
||||
id: 'starter-v2-step-fatigue-free-throws-baseline-run',
|
||||
name: 'Course ligne de fond',
|
||||
type: ExerciseStepType.time,
|
||||
defaultTargetValue: 15,
|
||||
),
|
||||
StarterExerciseStepSeed(
|
||||
id: 'starter-v2-step-fatigue-free-throws-two-shots',
|
||||
name: '2 lancers francs',
|
||||
type: ExerciseStepType.reps,
|
||||
defaultTargetValue: 2,
|
||||
),
|
||||
],
|
||||
),
|
||||
StarterExerciseSeed(
|
||||
id: 'starter-v2-exercise-catch-and-shoot',
|
||||
name: 'Catch & shoot',
|
||||
description: 'Recevoir, armer vite, tirer équilibré.',
|
||||
businessTypes: [BusinessExerciseType.shoot],
|
||||
tags: ['tir', 'réception'],
|
||||
hasRepsMeasure: true,
|
||||
hasScoreMeasure: true,
|
||||
scoreLabel: 'Réussites',
|
||||
scoreUnit: 'paniers',
|
||||
defaultTargetReps: 20,
|
||||
defaultTargetScore: 12,
|
||||
),
|
||||
StarterExerciseSeed(
|
||||
id: 'starter-v2-exercise-pull-up-shooting',
|
||||
name: 'Tir après dribble',
|
||||
description: 'Création d\'espace puis tir en rythme.',
|
||||
businessTypes: [BusinessExerciseType.shoot, BusinessExerciseType.dribble],
|
||||
tags: ['tir', 'dribble'],
|
||||
hasRepsMeasure: true,
|
||||
hasScoreMeasure: true,
|
||||
scoreLabel: 'Réussites',
|
||||
scoreUnit: 'paniers',
|
||||
defaultTargetReps: 20,
|
||||
defaultTargetScore: 10,
|
||||
steps: [
|
||||
StarterExerciseStepSeed(
|
||||
id: 'starter-v2-step-pull-up-right',
|
||||
name: 'Départ main droite',
|
||||
type: ExerciseStepType.reps,
|
||||
defaultTargetValue: 10,
|
||||
),
|
||||
StarterExerciseStepSeed(
|
||||
id: 'starter-v2-step-pull-up-left',
|
||||
name: 'Départ main gauche',
|
||||
type: ExerciseStepType.reps,
|
||||
defaultTargetValue: 10,
|
||||
),
|
||||
],
|
||||
),
|
||||
StarterExerciseSeed(
|
||||
id: 'starter-v2-exercise-stationary-dribble-alternating',
|
||||
name: 'Dribble stationnaire alterné',
|
||||
description: 'Contrôle de balle enchaîné main droite/main gauche.',
|
||||
businessTypes: [BusinessExerciseType.dribble],
|
||||
tags: ['dribble', 'maniement'],
|
||||
hasTimeMeasure: true,
|
||||
defaultTargetTimeSeconds: 60,
|
||||
steps: [
|
||||
StarterExerciseStepSeed(
|
||||
id: 'starter-v2-step-stationary-dribble-right-low',
|
||||
name: 'Main droite basse',
|
||||
type: ExerciseStepType.time,
|
||||
defaultTargetValue: 15,
|
||||
),
|
||||
StarterExerciseStepSeed(
|
||||
id: 'starter-v2-step-stationary-dribble-left-low',
|
||||
name: 'Main gauche basse',
|
||||
type: ExerciseStepType.time,
|
||||
defaultTargetValue: 15,
|
||||
),
|
||||
StarterExerciseStepSeed(
|
||||
id: 'starter-v2-step-stationary-dribble-crossovers',
|
||||
name: 'Crossovers',
|
||||
type: ExerciseStepType.time,
|
||||
defaultTargetValue: 15,
|
||||
),
|
||||
StarterExerciseStepSeed(
|
||||
id: 'starter-v2-step-stationary-dribble-between-legs',
|
||||
name: 'Entre les jambes',
|
||||
type: ExerciseStepType.time,
|
||||
defaultTargetValue: 15,
|
||||
),
|
||||
],
|
||||
),
|
||||
StarterExerciseSeed(
|
||||
id: 'starter-v2-exercise-combo-change-of-hand',
|
||||
name: 'Combo changement de main',
|
||||
description: 'Enchaîner les changements de main avec régularité.',
|
||||
businessTypes: [BusinessExerciseType.dribble],
|
||||
tags: ['dribble', 'coordination'],
|
||||
hasRepsMeasure: true,
|
||||
defaultTargetReps: 10,
|
||||
steps: [
|
||||
StarterExerciseStepSeed(
|
||||
id: 'starter-v2-step-combo-crossover',
|
||||
name: 'Crossover',
|
||||
type: ExerciseStepType.reps,
|
||||
defaultTargetValue: 1,
|
||||
),
|
||||
StarterExerciseStepSeed(
|
||||
id: 'starter-v2-step-combo-between-legs',
|
||||
name: 'Entre les jambes',
|
||||
type: ExerciseStepType.reps,
|
||||
defaultTargetValue: 1,
|
||||
),
|
||||
StarterExerciseStepSeed(
|
||||
id: 'starter-v2-step-combo-behind-back',
|
||||
name: 'Dans le dos',
|
||||
type: ExerciseStepType.reps,
|
||||
defaultTargetValue: 1,
|
||||
),
|
||||
StarterExerciseStepSeed(
|
||||
id: 'starter-v2-step-combo-explosive-start',
|
||||
name: 'Départ explosif',
|
||||
type: ExerciseStepType.reps,
|
||||
defaultTargetValue: 1,
|
||||
),
|
||||
],
|
||||
),
|
||||
StarterExerciseSeed(
|
||||
id: 'starter-v2-exercise-cone-slalom',
|
||||
name: 'Slalom cônes',
|
||||
description: 'Conduite de balle en vitesse sur parcours.',
|
||||
businessTypes: [
|
||||
BusinessExerciseType.dribble,
|
||||
BusinessExerciseType.highIntensity,
|
||||
],
|
||||
tags: ['dribble', 'vitesse'],
|
||||
hasRepsMeasure: true,
|
||||
hasScoreMeasure: true,
|
||||
scoreInputMode: ScoreInputMode.stopwatch,
|
||||
scoreLabel: 'Temps parcours',
|
||||
defaultTargetReps: 6,
|
||||
defaultTargetScoreTimeMs: 12000,
|
||||
),
|
||||
StarterExerciseSeed(
|
||||
id: 'starter-v2-exercise-mikan-drill',
|
||||
name: 'Mikan drill',
|
||||
description: 'Alternance de finitions près du cercle.',
|
||||
businessTypes: [BusinessExerciseType.finishing],
|
||||
tags: ['finition', 'proche cercle'],
|
||||
hasRepsMeasure: true,
|
||||
hasScoreMeasure: true,
|
||||
scoreLabel: 'Réussites',
|
||||
scoreUnit: 'paniers',
|
||||
defaultTargetReps: 20,
|
||||
defaultTargetScore: 16,
|
||||
),
|
||||
StarterExerciseSeed(
|
||||
id: 'starter-v2-exercise-alternating-layups',
|
||||
name: 'Layups alternés droite/gauche',
|
||||
description: 'Alterner les côtés avec course courte.',
|
||||
businessTypes: [BusinessExerciseType.finishing],
|
||||
tags: ['finition', 'layup'],
|
||||
hasRepsMeasure: true,
|
||||
hasScoreMeasure: true,
|
||||
scoreLabel: 'Réussites',
|
||||
scoreUnit: 'paniers',
|
||||
defaultTargetReps: 20,
|
||||
defaultTargetScore: 16,
|
||||
steps: [
|
||||
StarterExerciseStepSeed(
|
||||
id: 'starter-v2-step-alternating-layup-right',
|
||||
name: 'Layup côté droit',
|
||||
type: ExerciseStepType.reps,
|
||||
defaultTargetValue: 1,
|
||||
),
|
||||
StarterExerciseStepSeed(
|
||||
id: 'starter-v2-step-alternating-layup-left',
|
||||
name: 'Layup côté gauche',
|
||||
type: ExerciseStepType.reps,
|
||||
defaultTargetValue: 1,
|
||||
),
|
||||
],
|
||||
),
|
||||
StarterExerciseSeed(
|
||||
id: 'starter-v2-exercise-weak-hand-finishes',
|
||||
name: 'Finitions main faible',
|
||||
description: 'Layups et finitions contrôlées main faible.',
|
||||
businessTypes: [BusinessExerciseType.finishing],
|
||||
tags: ['finition', 'main faible'],
|
||||
hasRepsMeasure: true,
|
||||
hasScoreMeasure: true,
|
||||
scoreLabel: 'Réussites',
|
||||
scoreUnit: 'paniers',
|
||||
defaultTargetReps: 20,
|
||||
defaultTargetScore: 12,
|
||||
),
|
||||
StarterExerciseSeed(
|
||||
id: 'starter-v2-exercise-contact-finishing',
|
||||
name: 'Finition avec contact',
|
||||
description: 'Absorber le contact puis finir près du cercle.',
|
||||
businessTypes: [BusinessExerciseType.finishing],
|
||||
tags: ['finition', 'contact'],
|
||||
hasRepsMeasure: true,
|
||||
hasScoreMeasure: true,
|
||||
scoreLabel: 'Réussites',
|
||||
scoreUnit: 'paniers',
|
||||
defaultTargetReps: 12,
|
||||
defaultTargetScore: 8,
|
||||
),
|
||||
StarterExerciseSeed(
|
||||
id: 'starter-v2-exercise-half-court-suicides',
|
||||
name: 'Suicides demi-terrain',
|
||||
description: 'Aller-retour progressif sur lignes du terrain.',
|
||||
businessTypes: [
|
||||
BusinessExerciseType.conditioning,
|
||||
BusinessExerciseType.highIntensity,
|
||||
],
|
||||
tags: ['condition physique', 'intense'],
|
||||
hasRepsMeasure: true,
|
||||
hasScoreMeasure: true,
|
||||
scoreInputMode: ScoreInputMode.stopwatch,
|
||||
scoreLabel: 'Temps total',
|
||||
defaultTargetReps: 4,
|
||||
defaultTargetScoreTimeMs: 45000,
|
||||
),
|
||||
StarterExerciseSeed(
|
||||
id: 'starter-v2-exercise-baseline-sprint',
|
||||
name: 'Sprint ligne de fond',
|
||||
description: 'Départs explosifs répétés.',
|
||||
businessTypes: [
|
||||
BusinessExerciseType.conditioning,
|
||||
BusinessExerciseType.highIntensity,
|
||||
],
|
||||
tags: ['vitesse', 'intense'],
|
||||
hasRepsMeasure: true,
|
||||
hasScoreMeasure: true,
|
||||
scoreInputMode: ScoreInputMode.stopwatch,
|
||||
scoreLabel: 'Meilleur sprint',
|
||||
defaultTargetReps: 10,
|
||||
defaultTargetScoreTimeMs: 5000,
|
||||
),
|
||||
StarterExerciseSeed(
|
||||
id: 'starter-v2-exercise-line-hops',
|
||||
name: 'Line hops',
|
||||
description: 'Petits sauts rapides de part et d\'autre d\'une ligne.',
|
||||
businessTypes: [
|
||||
BusinessExerciseType.conditioning,
|
||||
BusinessExerciseType.highIntensity,
|
||||
],
|
||||
tags: ['appuis', 'intense'],
|
||||
hasTimeMeasure: true,
|
||||
hasScoreMeasure: true,
|
||||
scoreLabel: 'Contacts',
|
||||
scoreUnit: 'touches',
|
||||
defaultTargetTimeSeconds: 30,
|
||||
defaultTargetScore: 50,
|
||||
),
|
||||
StarterExerciseSeed(
|
||||
id: 'starter-v2-exercise-defensive-slides',
|
||||
name: 'Slides défensifs',
|
||||
description: 'Déplacements latéraux bas et contrôlés.',
|
||||
businessTypes: [BusinessExerciseType.defense],
|
||||
tags: ['défense', 'appuis'],
|
||||
hasTimeMeasure: true,
|
||||
defaultTargetTimeSeconds: 45,
|
||||
),
|
||||
StarterExerciseSeed(
|
||||
id: 'starter-v2-exercise-closeout-slide',
|
||||
name: 'Closeout + slide',
|
||||
description: 'Fermer l\'espace puis contenir latéralement.',
|
||||
businessTypes: [BusinessExerciseType.defense],
|
||||
tags: ['défense', 'closeout'],
|
||||
hasRepsMeasure: true,
|
||||
defaultTargetReps: 10,
|
||||
steps: [
|
||||
StarterExerciseStepSeed(
|
||||
id: 'starter-v2-step-closeout-slide-closeout',
|
||||
name: 'Closeout',
|
||||
type: ExerciseStepType.reps,
|
||||
defaultTargetValue: 1,
|
||||
),
|
||||
StarterExerciseStepSeed(
|
||||
id: 'starter-v2-step-closeout-slide-right',
|
||||
name: 'Slide droite',
|
||||
type: ExerciseStepType.reps,
|
||||
defaultTargetValue: 1,
|
||||
),
|
||||
StarterExerciseStepSeed(
|
||||
id: 'starter-v2-step-closeout-slide-left',
|
||||
name: 'Slide gauche',
|
||||
type: ExerciseStepType.reps,
|
||||
defaultTargetValue: 1,
|
||||
),
|
||||
],
|
||||
),
|
||||
StarterExerciseSeed(
|
||||
id: 'starter-v2-exercise-rebound-outlet-pass',
|
||||
name: 'Rebond + outlet pass',
|
||||
description: 'Capturer le rebond puis ressortir proprement.',
|
||||
businessTypes: [BusinessExerciseType.defense],
|
||||
tags: ['défense', 'rebond'],
|
||||
hasRepsMeasure: true,
|
||||
hasScoreMeasure: true,
|
||||
scoreLabel: 'Réussites',
|
||||
scoreUnit: 'actions',
|
||||
defaultTargetReps: 12,
|
||||
defaultTargetScore: 10,
|
||||
steps: [
|
||||
StarterExerciseStepSeed(
|
||||
id: 'starter-v2-step-rebound-outlet-rebound',
|
||||
name: 'Rebond',
|
||||
type: ExerciseStepType.reps,
|
||||
defaultTargetValue: 1,
|
||||
),
|
||||
StarterExerciseStepSeed(
|
||||
id: 'starter-v2-step-rebound-outlet-pass',
|
||||
name: 'Outlet pass',
|
||||
type: ExerciseStepType.reps,
|
||||
defaultTargetValue: 1,
|
||||
),
|
||||
],
|
||||
),
|
||||
StarterExerciseSeed(
|
||||
id: 'starter-v2-exercise-basket-hip-mobility',
|
||||
name: 'Mobilité hanches basket',
|
||||
description: 'Préparer hanches, appuis et amplitude.',
|
||||
businessTypes: [
|
||||
BusinessExerciseType.mobility,
|
||||
BusinessExerciseType.recovery,
|
||||
],
|
||||
tags: ['mobilité', 'échauffement'],
|
||||
hasTimeMeasure: true,
|
||||
defaultTargetTimeSeconds: 300,
|
||||
steps: [
|
||||
StarterExerciseStepSeed(
|
||||
id: 'starter-v2-step-hip-mobility-openers',
|
||||
name: 'Ouverture hanches',
|
||||
type: ExerciseStepType.time,
|
||||
defaultTargetValue: 60,
|
||||
),
|
||||
StarterExerciseStepSeed(
|
||||
id: 'starter-v2-step-hip-mobility-lunges',
|
||||
name: 'Fentes dynamiques',
|
||||
type: ExerciseStepType.time,
|
||||
defaultTargetValue: 60,
|
||||
),
|
||||
StarterExerciseStepSeed(
|
||||
id: 'starter-v2-step-hip-mobility-squat-hold',
|
||||
name: 'Squat hold',
|
||||
type: ExerciseStepType.time,
|
||||
defaultTargetValue: 60,
|
||||
),
|
||||
StarterExerciseStepSeed(
|
||||
id: 'starter-v2-step-hip-mobility-adductors',
|
||||
name: 'Adducteurs',
|
||||
type: ExerciseStepType.time,
|
||||
defaultTargetValue: 60,
|
||||
),
|
||||
StarterExerciseStepSeed(
|
||||
id: 'starter-v2-step-hip-mobility-breathing',
|
||||
name: 'Respiration basse',
|
||||
type: ExerciseStepType.time,
|
||||
defaultTargetValue: 60,
|
||||
),
|
||||
],
|
||||
),
|
||||
StarterExerciseSeed(
|
||||
id: 'starter-v2-exercise-ankles-calves',
|
||||
name: 'Chevilles et mollets',
|
||||
description: 'Préparer les appuis, réceptions et changements de direction.',
|
||||
businessTypes: [
|
||||
BusinessExerciseType.mobility,
|
||||
BusinessExerciseType.recovery,
|
||||
],
|
||||
tags: ['mobilité', 'appuis'],
|
||||
hasTimeMeasure: true,
|
||||
defaultTargetTimeSeconds: 180,
|
||||
steps: [
|
||||
StarterExerciseStepSeed(
|
||||
id: 'starter-v2-step-ankles-calves-right-ankle',
|
||||
name: 'Mobilité cheville droite',
|
||||
type: ExerciseStepType.time,
|
||||
defaultTargetValue: 60,
|
||||
),
|
||||
StarterExerciseStepSeed(
|
||||
id: 'starter-v2-step-ankles-calves-left-ankle',
|
||||
name: 'Mobilité cheville gauche',
|
||||
type: ExerciseStepType.time,
|
||||
defaultTargetValue: 60,
|
||||
),
|
||||
StarterExerciseStepSeed(
|
||||
id: 'starter-v2-step-ankles-calves-dynamic-calves',
|
||||
name: 'Mollets dynamiques',
|
||||
type: ExerciseStepType.time,
|
||||
defaultTargetValue: 60,
|
||||
),
|
||||
],
|
||||
),
|
||||
];
|
||||
|
||||
const basketStarterProgramSeedV2 = StarterProgramSeed(
|
||||
id: 'starter-v2-program-basket-foundations-45-min',
|
||||
name: 'Fondations basket - 45 min',
|
||||
defaultRestSeconds: 45,
|
||||
tags: ['fondations', 'basket'],
|
||||
exercises: [
|
||||
StarterProgramExerciseSeed(
|
||||
id: 'starter-v2-program-exercise-hip-mobility',
|
||||
exerciseId: 'starter-v2-exercise-basket-hip-mobility',
|
||||
setsCount: 1,
|
||||
enabledMeasures: {WorkoutMeasure.time},
|
||||
targetTimeSeconds: 300,
|
||||
restSecondsOverride: 30,
|
||||
),
|
||||
StarterProgramExerciseSeed(
|
||||
id: 'starter-v2-program-exercise-stationary-dribble',
|
||||
exerciseId: 'starter-v2-exercise-stationary-dribble-alternating',
|
||||
setsCount: 2,
|
||||
enabledMeasures: {WorkoutMeasure.time},
|
||||
targetTimeSeconds: 60,
|
||||
restSecondsOverride: 30,
|
||||
),
|
||||
StarterProgramExerciseSeed(
|
||||
id: 'starter-v2-program-exercise-combo-change-of-hand',
|
||||
exerciseId: 'starter-v2-exercise-combo-change-of-hand',
|
||||
setsCount: 2,
|
||||
enabledMeasures: {WorkoutMeasure.reps},
|
||||
targetReps: 10,
|
||||
restSecondsOverride: 45,
|
||||
),
|
||||
StarterProgramExerciseSeed(
|
||||
id: 'starter-v2-program-exercise-form-shooting',
|
||||
exerciseId: 'starter-v2-exercise-form-shooting',
|
||||
setsCount: 3,
|
||||
enabledMeasures: {WorkoutMeasure.reps, WorkoutMeasure.score},
|
||||
targetReps: 25,
|
||||
targetScore: 20,
|
||||
restSecondsOverride: 45,
|
||||
),
|
||||
StarterProgramExerciseSeed(
|
||||
id: 'starter-v2-program-exercise-spot-shooting',
|
||||
exerciseId: 'starter-v2-exercise-spot-shooting-5-positions',
|
||||
setsCount: 3,
|
||||
enabledMeasures: {WorkoutMeasure.reps, WorkoutMeasure.score},
|
||||
targetReps: 1,
|
||||
targetScore: 15,
|
||||
restSecondsOverride: 60,
|
||||
),
|
||||
StarterProgramExerciseSeed(
|
||||
id: 'starter-v2-program-exercise-alternating-layups',
|
||||
exerciseId: 'starter-v2-exercise-alternating-layups',
|
||||
setsCount: 3,
|
||||
enabledMeasures: {WorkoutMeasure.reps, WorkoutMeasure.score},
|
||||
targetReps: 20,
|
||||
targetScore: 16,
|
||||
restSecondsOverride: 45,
|
||||
),
|
||||
StarterProgramExerciseSeed(
|
||||
id: 'starter-v2-program-exercise-free-throw-routine',
|
||||
exerciseId: 'starter-v2-exercise-free-throw-routine',
|
||||
setsCount: 4,
|
||||
enabledMeasures: {WorkoutMeasure.reps, WorkoutMeasure.score},
|
||||
targetReps: 10,
|
||||
targetScore: 8,
|
||||
restSecondsOverride: 30,
|
||||
),
|
||||
StarterProgramExerciseSeed(
|
||||
id: 'starter-v2-program-exercise-defensive-slides',
|
||||
exerciseId: 'starter-v2-exercise-defensive-slides',
|
||||
setsCount: 4,
|
||||
enabledMeasures: {WorkoutMeasure.time},
|
||||
targetTimeSeconds: 45,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
const basketStarterWorkoutTemplateSeedV2 = StarterWorkoutTemplateSeed(
|
||||
id: 'starter-v2-workout-template-basket-foundations-example',
|
||||
name: 'Séance exemple - Fondations basket',
|
||||
programSnapshotId: 'starter-v2-workout-template-program-basket-foundations',
|
||||
tags: ['fondations', 'séance'],
|
||||
);
|
||||
@ -1,14 +1,15 @@
|
||||
import '../../domain/domain.dart';
|
||||
import '../ports.dart';
|
||||
|
||||
const int starterSeedVersion = 1;
|
||||
const int starterSeedVersion = 2;
|
||||
|
||||
final class StarterExerciseSeed {
|
||||
const StarterExerciseSeed({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.description,
|
||||
required this.category,
|
||||
this.category = ExerciseCategory.uncategorized,
|
||||
this.businessTypes = const [],
|
||||
this.hasTimeMeasure = false,
|
||||
this.hasRepsMeasure = false,
|
||||
this.hasScoreMeasure = false,
|
||||
@ -19,6 +20,7 @@ final class StarterExerciseSeed {
|
||||
this.defaultTargetReps,
|
||||
this.defaultTargetScore,
|
||||
this.defaultTargetScoreTimeMs,
|
||||
this.tags = const [],
|
||||
this.steps = const [],
|
||||
});
|
||||
|
||||
@ -26,6 +28,7 @@ final class StarterExerciseSeed {
|
||||
final String name;
|
||||
final String description;
|
||||
final ExerciseCategory category;
|
||||
final List<BusinessExerciseType> businessTypes;
|
||||
final bool hasTimeMeasure;
|
||||
final bool hasRepsMeasure;
|
||||
final bool hasScoreMeasure;
|
||||
@ -36,6 +39,7 @@ final class StarterExerciseSeed {
|
||||
final int? defaultTargetReps;
|
||||
final double? defaultTargetScore;
|
||||
final int? defaultTargetScoreTimeMs;
|
||||
final List<String> tags;
|
||||
final List<StarterExerciseStepSeed> steps;
|
||||
}
|
||||
|
||||
@ -59,12 +63,14 @@ final class StarterProgramSeed {
|
||||
required this.name,
|
||||
required this.defaultRestSeconds,
|
||||
required this.exercises,
|
||||
this.tags = const [],
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String name;
|
||||
final int defaultRestSeconds;
|
||||
final List<StarterProgramExerciseSeed> exercises;
|
||||
final List<String> tags;
|
||||
}
|
||||
|
||||
final class StarterProgramExerciseSeed {
|
||||
@ -96,11 +102,13 @@ final class StarterWorkoutTemplateSeed {
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.programSnapshotId,
|
||||
this.tags = const [],
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String name;
|
||||
final String programSnapshotId;
|
||||
final List<String> tags;
|
||||
}
|
||||
|
||||
StarterContent buildStarterContent({
|
||||
@ -127,7 +135,9 @@ StarterContent buildStarterContent({
|
||||
defaultTargetScore: seed.defaultTargetScore,
|
||||
defaultTargetScoreTimeMs: seed.defaultTargetScoreTimeMs,
|
||||
category: seed.category,
|
||||
businessTypes: seed.businessTypes,
|
||||
isExample: true,
|
||||
tags: seed.tags,
|
||||
steps: [
|
||||
for (var index = 0; index < seed.steps.length; index += 1)
|
||||
ExerciseStep(
|
||||
@ -170,6 +180,7 @@ StarterContent buildStarterContent({
|
||||
name: programSeed.name,
|
||||
defaultRestSeconds: programSeed.defaultRestSeconds,
|
||||
isExample: true,
|
||||
tags: programSeed.tags,
|
||||
exercises: programExercises,
|
||||
);
|
||||
final templateProgram = WorkoutTemplateProgram.snapshotFromProgram(
|
||||
@ -182,6 +193,7 @@ StarterContent buildStarterContent({
|
||||
metadata: _metadata(templateSeed.id, now, originDeviceId),
|
||||
name: templateSeed.name,
|
||||
isExample: true,
|
||||
tags: templateSeed.tags,
|
||||
programs: [templateProgram],
|
||||
);
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@ import 'package:watch_bridge_contract/watch_bridge_contract.dart';
|
||||
|
||||
import '../domain/domain.dart';
|
||||
import 'ports.dart';
|
||||
import 'starter_content/basket_starter_seed_v1.dart';
|
||||
import 'starter_content/basket_starter_seed_v2.dart';
|
||||
import 'starter_content/starter_content.dart';
|
||||
import 'watch_companion_use_cases.dart';
|
||||
|
||||
@ -277,9 +277,9 @@ final class SeedStarterContentUseCase {
|
||||
|
||||
await contentRepository.insertStarterContent(
|
||||
buildStarterContent(
|
||||
exerciseSeeds: basketStarterExerciseSeedsV1,
|
||||
programSeed: basketStarterProgramSeedV1,
|
||||
templateSeed: basketStarterWorkoutTemplateSeedV1,
|
||||
exerciseSeeds: basketStarterExerciseSeedsV2,
|
||||
programSeed: basketStarterProgramSeedV2,
|
||||
templateSeed: basketStarterWorkoutTemplateSeedV2,
|
||||
now: now,
|
||||
originDeviceId: originDeviceId,
|
||||
),
|
||||
@ -5599,6 +5599,7 @@ final class CloseWorkoutSessionUseCase {
|
||||
final class WorkoutTelemetryUseCases {
|
||||
const WorkoutTelemetryUseCases({
|
||||
required this.repository,
|
||||
this.sessionRepository,
|
||||
required this.clock,
|
||||
required this.ids,
|
||||
});
|
||||
@ -5606,6 +5607,7 @@ final class WorkoutTelemetryUseCases {
|
||||
static const persistedSampleInterval = Duration(seconds: 15);
|
||||
|
||||
final WorkoutTelemetryRepository repository;
|
||||
final ActiveSessionRepository? sessionRepository;
|
||||
final Clock clock;
|
||||
final IdGenerator ids;
|
||||
|
||||
@ -5622,6 +5624,14 @@ final class WorkoutTelemetryUseCases {
|
||||
return _telemetrySamplesFromHistorySnapshot(history);
|
||||
}
|
||||
|
||||
Future<List<ScopeInstanceDescriptor>> listScopeInstancesForHistory({
|
||||
required WorkoutHistory history,
|
||||
required WorkoutTelemetryAggregateScope scope,
|
||||
}) async {
|
||||
final samples = await listSamplesForHistory(history);
|
||||
return _scopeInstancesFromSamples(samples, scope: scope);
|
||||
}
|
||||
|
||||
Future<WorkoutTelemetryGraphSeries> readGraphSeriesForHistory({
|
||||
required WorkoutHistory history,
|
||||
required WorkoutTelemetryAggregateScope scope,
|
||||
@ -5630,6 +5640,56 @@ final class WorkoutTelemetryUseCases {
|
||||
int? setIndex,
|
||||
int? passageIndex,
|
||||
int? stepIndex,
|
||||
}) async {
|
||||
final samples = await _listSamplesForHistoryScope(
|
||||
history: history,
|
||||
scope: scope,
|
||||
programIndex: programIndex,
|
||||
exerciseIndex: exerciseIndex,
|
||||
setIndex: setIndex,
|
||||
passageIndex: passageIndex,
|
||||
stepIndex: stepIndex,
|
||||
);
|
||||
return _telemetryGraphSeriesFromSamples(
|
||||
samples,
|
||||
scope: scope,
|
||||
programIndex: programIndex,
|
||||
exerciseIndex: exerciseIndex,
|
||||
setIndex: setIndex,
|
||||
passageIndex: passageIndex,
|
||||
stepIndex: stepIndex,
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<ScopeMarker>> readScopeMarkersForHistory({
|
||||
required WorkoutHistory history,
|
||||
required WorkoutTelemetryAggregateScope scope,
|
||||
int? programIndex,
|
||||
int? exerciseIndex,
|
||||
int? setIndex,
|
||||
int? passageIndex,
|
||||
int? stepIndex,
|
||||
}) async {
|
||||
final samples = await _listSamplesForHistoryScope(
|
||||
history: history,
|
||||
scope: scope,
|
||||
programIndex: programIndex,
|
||||
exerciseIndex: exerciseIndex,
|
||||
setIndex: setIndex,
|
||||
passageIndex: passageIndex,
|
||||
stepIndex: stepIndex,
|
||||
);
|
||||
return _scopeMarkersFromSamples(samples, scope: scope);
|
||||
}
|
||||
|
||||
Future<List<WorkoutTelemetrySample>> _listSamplesForHistoryScope({
|
||||
required WorkoutHistory history,
|
||||
required WorkoutTelemetryAggregateScope scope,
|
||||
int? programIndex,
|
||||
int? exerciseIndex,
|
||||
int? setIndex,
|
||||
int? passageIndex,
|
||||
int? stepIndex,
|
||||
}) async {
|
||||
final sourceSessionId = history.sourceActiveWorkoutSessionId?.trim();
|
||||
var samples = const <WorkoutTelemetrySample>[];
|
||||
@ -5644,53 +5704,128 @@ final class WorkoutTelemetryUseCases {
|
||||
stepIndex: stepIndex,
|
||||
);
|
||||
}
|
||||
if (samples.isEmpty) {
|
||||
samples = _telemetrySamplesFromHistorySnapshot(history)
|
||||
.where(
|
||||
(sample) => _matchesTelemetryScope(
|
||||
sample,
|
||||
scope: scope,
|
||||
programIndex: programIndex,
|
||||
exerciseIndex: exerciseIndex,
|
||||
setIndex: setIndex,
|
||||
passageIndex: passageIndex,
|
||||
stepIndex: stepIndex,
|
||||
),
|
||||
)
|
||||
.toList(growable: false);
|
||||
if (samples.isNotEmpty) {
|
||||
return samples;
|
||||
}
|
||||
return _telemetryGraphSeriesFromSamples(
|
||||
samples,
|
||||
scope: scope,
|
||||
programIndex: programIndex,
|
||||
exerciseIndex: exerciseIndex,
|
||||
setIndex: setIndex,
|
||||
passageIndex: passageIndex,
|
||||
stepIndex: stepIndex,
|
||||
);
|
||||
return _telemetrySamplesFromHistorySnapshot(history)
|
||||
.where(
|
||||
(sample) => _matchesTelemetryScope(
|
||||
sample,
|
||||
scope: scope,
|
||||
programIndex: programIndex,
|
||||
exerciseIndex: exerciseIndex,
|
||||
setIndex: setIndex,
|
||||
passageIndex: passageIndex,
|
||||
stepIndex: stepIndex,
|
||||
),
|
||||
)
|
||||
.toList(growable: false);
|
||||
}
|
||||
|
||||
Future<List<WorkoutTelemetryAggregate>> recordTelemetrySample(
|
||||
WatchTelemetrySample sample,
|
||||
) async {
|
||||
final domainSample = _telemetrySampleFromWatch(sample);
|
||||
if (domainSample == null) {
|
||||
final pendingSample = _telemetryWindowStateFromWatch(sample);
|
||||
if (pendingSample == null) {
|
||||
return const [];
|
||||
}
|
||||
final inserted = await repository.saveSample(domainSample);
|
||||
final activeElapsedMs = await _activeElapsedMillisecondsForSample(
|
||||
pendingSample.sessionId,
|
||||
pendingSample.latestCapturedAt,
|
||||
);
|
||||
if (activeElapsedMs == null) {
|
||||
return const [];
|
||||
}
|
||||
final existing = await repository.findWindowState(pendingSample.sessionId);
|
||||
if (existing != null &&
|
||||
!pendingSample.latestCapturedAt.isAfter(existing.latestCapturedAt)) {
|
||||
return const [];
|
||||
}
|
||||
final nextPending = _telemetryWindowStateWithActiveAnchor(
|
||||
pendingSample,
|
||||
existing?.windowStartedActiveMs ?? activeElapsedMs,
|
||||
);
|
||||
if (existing == null) {
|
||||
await repository.saveWindowState(nextPending);
|
||||
return const [];
|
||||
}
|
||||
if (activeElapsedMs - existing.windowStartedActiveMs <
|
||||
persistedSampleInterval.inMilliseconds) {
|
||||
await repository.saveWindowState(nextPending);
|
||||
return const [];
|
||||
}
|
||||
|
||||
final consumedSample = existing.toSample(
|
||||
_telemetrySampleId(existing.sessionId, existing.windowStartedActiveMs),
|
||||
);
|
||||
final inserted = await repository.saveSample(consumedSample);
|
||||
final nextWindowStartedActiveMs =
|
||||
activeElapsedMs - existing.windowStartedActiveMs >=
|
||||
persistedSampleInterval.inMilliseconds * 2
|
||||
? activeElapsedMs
|
||||
: existing.windowStartedActiveMs +
|
||||
persistedSampleInterval.inMilliseconds;
|
||||
await repository.saveWindowState(
|
||||
_telemetryWindowStateWithActiveAnchor(
|
||||
pendingSample,
|
||||
nextWindowStartedActiveMs,
|
||||
),
|
||||
);
|
||||
if (!inserted) {
|
||||
return const [];
|
||||
}
|
||||
final samples = await repository.listSamples(domainSample.sessionId);
|
||||
final samples = await repository.listSamples(consumedSample.sessionId);
|
||||
final aggregates = _telemetryAggregatesFromSamples(samples);
|
||||
await repository.replaceAggregatesForSession(
|
||||
sessionId: domainSample.sessionId,
|
||||
sessionId: consumedSample.sessionId,
|
||||
aggregates: aggregates,
|
||||
);
|
||||
return aggregates;
|
||||
}
|
||||
|
||||
WorkoutTelemetrySample? _telemetrySampleFromWatch(
|
||||
Future<int?> _activeElapsedMillisecondsForSample(
|
||||
String sessionId,
|
||||
DateTime capturedAt,
|
||||
) async {
|
||||
final repository = sessionRepository;
|
||||
if (repository == null) {
|
||||
return capturedAt.millisecondsSinceEpoch;
|
||||
}
|
||||
final session = await repository.findById(sessionId);
|
||||
if (session == null ||
|
||||
session.status == ActiveWorkoutStatus.completed ||
|
||||
session.status == ActiveWorkoutStatus.abandoned) {
|
||||
return null;
|
||||
}
|
||||
if (session.status == ActiveWorkoutStatus.paused) {
|
||||
return null;
|
||||
}
|
||||
return session.elapsedActiveMillisecondsAt(capturedAt);
|
||||
}
|
||||
|
||||
ActiveWorkoutTelemetryWindowState _telemetryWindowStateWithActiveAnchor(
|
||||
ActiveWorkoutTelemetryWindowState state,
|
||||
int windowStartedActiveMs,
|
||||
) {
|
||||
return ActiveWorkoutTelemetryWindowState(
|
||||
sessionId: state.sessionId,
|
||||
windowStartedActiveMs: windowStartedActiveMs,
|
||||
latestCapturedAt: state.latestCapturedAt,
|
||||
programIndex: state.programIndex,
|
||||
exerciseIndex: state.exerciseIndex,
|
||||
setIndex: state.setIndex,
|
||||
passageIndex: state.passageIndex,
|
||||
stepIndex: state.stepIndex,
|
||||
programSnapshotId: state.programSnapshotId,
|
||||
exerciseSnapshotId: state.exerciseSnapshotId,
|
||||
stepSnapshotId: state.stepSnapshotId,
|
||||
heartRateBpm: state.heartRateBpm,
|
||||
distanceMeters: state.distanceMeters,
|
||||
caloriesKcal: state.caloriesKcal,
|
||||
);
|
||||
}
|
||||
|
||||
ActiveWorkoutTelemetryWindowState? _telemetryWindowStateFromWatch(
|
||||
WatchTelemetrySample sample,
|
||||
) {
|
||||
final sessionId = sample.sessionId.trim();
|
||||
@ -5709,10 +5844,10 @@ final class WorkoutTelemetryUseCases {
|
||||
isUtc: true,
|
||||
)
|
||||
: clock.now();
|
||||
return WorkoutTelemetrySample(
|
||||
id: _telemetrySampleId(sample, sessionId, capturedAt),
|
||||
return ActiveWorkoutTelemetryWindowState(
|
||||
sessionId: sessionId,
|
||||
capturedAt: capturedAt,
|
||||
windowStartedActiveMs: 0,
|
||||
latestCapturedAt: capturedAt,
|
||||
programIndex: sample.programIndex,
|
||||
exerciseIndex: sample.exerciseIndex,
|
||||
setIndex: sample.setIndex,
|
||||
@ -5727,16 +5862,8 @@ final class WorkoutTelemetryUseCases {
|
||||
);
|
||||
}
|
||||
|
||||
String _telemetrySampleId(
|
||||
WatchTelemetrySample sample,
|
||||
String sessionId,
|
||||
DateTime capturedAt,
|
||||
) {
|
||||
final bucketMs =
|
||||
capturedAt.millisecondsSinceEpoch ~/
|
||||
persistedSampleInterval.inMilliseconds *
|
||||
persistedSampleInterval.inMilliseconds;
|
||||
return 'telemetry:$sessionId:$bucketMs';
|
||||
String _telemetrySampleId(String sessionId, int windowStartedActiveMs) {
|
||||
return 'telemetry:$sessionId:$windowStartedActiveMs';
|
||||
}
|
||||
}
|
||||
|
||||
@ -5828,6 +5955,168 @@ WorkoutTelemetryGraphSeries _telemetryGraphSeriesFromSamples(
|
||||
);
|
||||
}
|
||||
|
||||
List<ScopeInstanceDescriptor> _scopeInstancesFromSamples(
|
||||
List<WorkoutTelemetrySample> samples, {
|
||||
required WorkoutTelemetryAggregateScope scope,
|
||||
}) {
|
||||
final ordered = samples.toList()
|
||||
..sort((left, right) => left.capturedAt.compareTo(right.capturedAt));
|
||||
final firstCapturedAtByKey = <_TelemetryScopeKey, DateTime>{};
|
||||
for (final sample in ordered) {
|
||||
final key = _scopeKeyForSample(sample, scope);
|
||||
if (key == null) {
|
||||
continue;
|
||||
}
|
||||
firstCapturedAtByKey.putIfAbsent(key, () => sample.capturedAt);
|
||||
}
|
||||
var ordinal = 0;
|
||||
return [
|
||||
for (final entry in firstCapturedAtByKey.entries)
|
||||
ScopeInstanceDescriptor(
|
||||
scope: scope,
|
||||
programIndex: entry.key.programIndex,
|
||||
exerciseIndex: entry.key.exerciseIndex,
|
||||
setIndex: entry.key.setIndex,
|
||||
passageIndex: entry.key.passageIndex,
|
||||
stepIndex: entry.key.stepIndex,
|
||||
ordinal: ordinal += 1,
|
||||
firstCapturedAt: entry.value,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
List<ScopeMarker> _scopeMarkersFromSamples(
|
||||
List<WorkoutTelemetrySample> samples, {
|
||||
required WorkoutTelemetryAggregateScope scope,
|
||||
}) {
|
||||
final ordered = samples.toList()
|
||||
..sort((left, right) => left.capturedAt.compareTo(right.capturedAt));
|
||||
if (ordered.isEmpty || scope == WorkoutTelemetryAggregateScope.step) {
|
||||
return const [];
|
||||
}
|
||||
|
||||
final firstCapturedAt = ordered.first.capturedAt;
|
||||
final groups = <_TelemetryScopeKey, _ScopeMarkerGroup>{};
|
||||
for (final sample in ordered) {
|
||||
final key = _markerChildKeyForSample(sample, parentScope: scope);
|
||||
if (key == null) {
|
||||
continue;
|
||||
}
|
||||
groups.putIfAbsent(key, () => _ScopeMarkerGroup()).add(sample);
|
||||
}
|
||||
|
||||
var ordinal = 0;
|
||||
final output = <ScopeMarker>[];
|
||||
for (final group in groups.values) {
|
||||
ordinal += 1;
|
||||
final label = _markerChildLabel(scope, ordinal);
|
||||
output.add(
|
||||
ScopeMarker(
|
||||
elapsedMs: group.firstCapturedAt!
|
||||
.difference(firstCapturedAt)
|
||||
.inMilliseconds
|
||||
.clamp(0, double.infinity)
|
||||
.toInt(),
|
||||
label: 'Déb. $label',
|
||||
boundary: ScopeMarkerBoundary.start,
|
||||
),
|
||||
);
|
||||
output.add(
|
||||
ScopeMarker(
|
||||
elapsedMs: group.lastCapturedAt!
|
||||
.difference(firstCapturedAt)
|
||||
.inMilliseconds
|
||||
.clamp(0, double.infinity)
|
||||
.toInt(),
|
||||
label: 'Fin $label',
|
||||
boundary: ScopeMarkerBoundary.end,
|
||||
),
|
||||
);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
_TelemetryScopeKey? _scopeKeyForSample(
|
||||
WorkoutTelemetrySample sample,
|
||||
WorkoutTelemetryAggregateScope scope,
|
||||
) {
|
||||
return switch (scope) {
|
||||
WorkoutTelemetryAggregateScope.session => _TelemetryScopeKey(
|
||||
sessionId: sample.sessionId,
|
||||
scope: WorkoutTelemetryAggregateScope.session,
|
||||
),
|
||||
WorkoutTelemetryAggregateScope.exercise =>
|
||||
sample.programIndex != null && sample.exerciseIndex != null
|
||||
? _TelemetryScopeKey(
|
||||
sessionId: sample.sessionId,
|
||||
scope: WorkoutTelemetryAggregateScope.exercise,
|
||||
programIndex: sample.programIndex,
|
||||
exerciseIndex: sample.exerciseIndex,
|
||||
)
|
||||
: null,
|
||||
WorkoutTelemetryAggregateScope.set =>
|
||||
sample.programIndex != null &&
|
||||
sample.exerciseIndex != null &&
|
||||
sample.setIndex != null
|
||||
? _TelemetryScopeKey(
|
||||
sessionId: sample.sessionId,
|
||||
scope: WorkoutTelemetryAggregateScope.set,
|
||||
programIndex: sample.programIndex,
|
||||
exerciseIndex: sample.exerciseIndex,
|
||||
setIndex: sample.setIndex,
|
||||
)
|
||||
: null,
|
||||
WorkoutTelemetryAggregateScope.step =>
|
||||
sample.programIndex != null &&
|
||||
sample.exerciseIndex != null &&
|
||||
sample.setIndex != null &&
|
||||
sample.stepIndex != null
|
||||
? _TelemetryScopeKey(
|
||||
sessionId: sample.sessionId,
|
||||
scope: WorkoutTelemetryAggregateScope.step,
|
||||
programIndex: sample.programIndex,
|
||||
exerciseIndex: sample.exerciseIndex,
|
||||
setIndex: sample.setIndex,
|
||||
passageIndex: sample.passageIndex,
|
||||
stepIndex: sample.stepIndex,
|
||||
)
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
_TelemetryScopeKey? _markerChildKeyForSample(
|
||||
WorkoutTelemetrySample sample, {
|
||||
required WorkoutTelemetryAggregateScope parentScope,
|
||||
}) {
|
||||
return switch (parentScope) {
|
||||
WorkoutTelemetryAggregateScope.session => _scopeKeyForSample(
|
||||
sample,
|
||||
WorkoutTelemetryAggregateScope.exercise,
|
||||
),
|
||||
WorkoutTelemetryAggregateScope.exercise => _scopeKeyForSample(
|
||||
sample,
|
||||
WorkoutTelemetryAggregateScope.set,
|
||||
),
|
||||
WorkoutTelemetryAggregateScope.set => _scopeKeyForSample(
|
||||
sample,
|
||||
WorkoutTelemetryAggregateScope.step,
|
||||
),
|
||||
WorkoutTelemetryAggregateScope.step => null,
|
||||
};
|
||||
}
|
||||
|
||||
String _markerChildLabel(
|
||||
WorkoutTelemetryAggregateScope parentScope,
|
||||
int ordinal,
|
||||
) {
|
||||
return switch (parentScope) {
|
||||
WorkoutTelemetryAggregateScope.session => 'ex. $ordinal',
|
||||
WorkoutTelemetryAggregateScope.exercise => 'série $ordinal',
|
||||
WorkoutTelemetryAggregateScope.set => 'ét. $ordinal',
|
||||
WorkoutTelemetryAggregateScope.step => '',
|
||||
};
|
||||
}
|
||||
|
||||
Map<String, Object?> _workoutTelemetrySampleSnapshotJson(
|
||||
WorkoutTelemetrySample sample,
|
||||
) {
|
||||
@ -6028,6 +6317,19 @@ final class _TelemetryScopeKey {
|
||||
);
|
||||
}
|
||||
|
||||
final class _ScopeMarkerGroup {
|
||||
DateTime? firstCapturedAt;
|
||||
DateTime? lastCapturedAt;
|
||||
|
||||
void add(WorkoutTelemetrySample sample) {
|
||||
final capturedAt = sample.capturedAt;
|
||||
firstCapturedAt ??= capturedAt;
|
||||
if (lastCapturedAt == null || capturedAt.isAfter(lastCapturedAt!)) {
|
||||
lastCapturedAt = capturedAt;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class _TelemetryAggregateBuilder {
|
||||
_TelemetryAggregateBuilder(this.key);
|
||||
|
||||
@ -6099,6 +6401,7 @@ final class ActiveWorkoutSensorState {
|
||||
this.averageHeartRateBpm,
|
||||
this.maxHeartRateBpm,
|
||||
this.latestDistanceMeters,
|
||||
required this.latestDistanceAvailable,
|
||||
this.latestCaloriesKcal,
|
||||
required this.estimatedCaloriesKcal,
|
||||
});
|
||||
@ -6112,6 +6415,7 @@ final class ActiveWorkoutSensorState {
|
||||
final double? averageHeartRateBpm;
|
||||
final int? maxHeartRateBpm;
|
||||
final double? latestDistanceMeters;
|
||||
final bool latestDistanceAvailable;
|
||||
final double? latestCaloriesKcal;
|
||||
final double estimatedCaloriesKcal;
|
||||
}
|
||||
@ -6191,6 +6495,7 @@ final class _ActiveWorkoutSensorAccumulator {
|
||||
int? _minHeartRateBpm;
|
||||
int? _maxHeartRateBpm;
|
||||
double? _latestDistanceMeters;
|
||||
var _latestDistanceAvailable = false;
|
||||
double? _latestCaloriesKcal;
|
||||
|
||||
ActiveWorkoutSensorState get snapshot {
|
||||
@ -6207,6 +6512,7 @@ final class _ActiveWorkoutSensorAccumulator {
|
||||
averageHeartRateBpm: averageHeartRateBpm,
|
||||
maxHeartRateBpm: _maxHeartRateBpm,
|
||||
latestDistanceMeters: _latestDistanceMeters,
|
||||
latestDistanceAvailable: _latestDistanceAvailable,
|
||||
latestCaloriesKcal: _latestCaloriesKcal,
|
||||
estimatedCaloriesKcal: _estimatedCaloriesKcal(
|
||||
averageHeartRateBpm: averageHeartRateBpm ?? 0,
|
||||
@ -6253,6 +6559,7 @@ final class _ActiveWorkoutSensorAccumulator {
|
||||
distanceMeters >= _latestDistanceMeters!)) {
|
||||
_latestDistanceMeters = distanceMeters;
|
||||
}
|
||||
_latestDistanceAvailable = distanceMeters != null;
|
||||
if (caloriesKcal != null &&
|
||||
(_latestCaloriesKcal == null || caloriesKcal >= _latestCaloriesKcal!)) {
|
||||
_latestCaloriesKcal = caloriesKcal;
|
||||
|
||||
Reference in New Issue
Block a user