feat(watch): phone session projection to watch (#91-B)
This commit is contained in:
@ -31,6 +31,7 @@ final class AppBootstrap implements AppDependencies {
|
||||
required this.workoutTemplateUseCases,
|
||||
required this.activeWorkoutSessionUseCases,
|
||||
required this.activeExerciseStepUseCases,
|
||||
required this.watchCompanionProjectionUseCases,
|
||||
required this.closeWorkoutSessionUseCase,
|
||||
required this.workoutHistoryUseCases,
|
||||
required this.progressionStatsUseCase,
|
||||
@ -57,6 +58,7 @@ final class AppBootstrap implements AppDependencies {
|
||||
final ActiveWorkoutSessionUseCases activeWorkoutSessionUseCases;
|
||||
@override
|
||||
final ActiveExerciseStepUseCases activeExerciseStepUseCases;
|
||||
final WatchCompanionProjectionUseCases watchCompanionProjectionUseCases;
|
||||
@override
|
||||
final CloseWorkoutSessionUseCase closeWorkoutSessionUseCase;
|
||||
@override
|
||||
@ -171,6 +173,12 @@ final class AppBootstrap implements AppDependencies {
|
||||
ids: ids,
|
||||
originDeviceId: originDeviceId,
|
||||
),
|
||||
watchCompanionProjectionUseCases: WatchCompanionProjectionUseCases(
|
||||
sessionRepository: activeSessionRepository,
|
||||
clock: clock,
|
||||
ids: ids,
|
||||
originDeviceId: originDeviceId,
|
||||
),
|
||||
closeWorkoutSessionUseCase: CloseWorkoutSessionUseCase(
|
||||
sessionRepository: activeSessionRepository,
|
||||
historyRepository: historyRepository,
|
||||
|
||||
@ -8,3 +8,4 @@ export 'ports.dart';
|
||||
export 'starter_content/basket_starter_seed_v1.dart';
|
||||
export 'starter_content/starter_content.dart';
|
||||
export 'use_cases.dart';
|
||||
export 'watch_companion_use_cases.dart';
|
||||
|
||||
@ -1,10 +1,14 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
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/starter_content.dart';
|
||||
import 'watch_companion_use_cases.dart';
|
||||
|
||||
const Object _useCaseUnchanged = Object();
|
||||
|
||||
@ -3084,6 +3088,513 @@ final class ActiveExerciseStepProgressView {
|
||||
: steps[state.currentStepIndex];
|
||||
}
|
||||
|
||||
final class WatchCompanionProjectionUseCases implements WatchProjectionSource {
|
||||
WatchCompanionProjectionUseCases({
|
||||
required ActiveSessionRepository sessionRepository,
|
||||
required Clock clock,
|
||||
required IdGenerator ids,
|
||||
required String originDeviceId,
|
||||
WatchProjectionPublisher? publisher,
|
||||
}) : _projector = WatchSessionProjectionProjector(
|
||||
sessionRepository: sessionRepository,
|
||||
clock: clock,
|
||||
ids: ids,
|
||||
originDeviceId: originDeviceId,
|
||||
),
|
||||
_publisher = publisher;
|
||||
|
||||
final WatchSessionProjectionProjector _projector;
|
||||
final WatchProjectionPublisher? _publisher;
|
||||
final _controller = StreamController<WatchSessionProjection>.broadcast();
|
||||
WatchSessionProjection? _latestProjection;
|
||||
int _revision = 0;
|
||||
|
||||
@override
|
||||
Stream<WatchSessionProjection> get projections => _controller.stream;
|
||||
|
||||
@override
|
||||
Future<WatchSessionProjection> currentProjection() async {
|
||||
return _latestProjection ?? _projectWithCurrentRevision();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<WatchSessionProjection> emitCurrentProjection() async {
|
||||
_revision += 1;
|
||||
final projection = await _projector.project(revision: _revision);
|
||||
_latestProjection = projection;
|
||||
_controller.add(projection);
|
||||
await _publisher?.publish(projection);
|
||||
return projection;
|
||||
}
|
||||
|
||||
Future<void> dispose() => _controller.close();
|
||||
|
||||
Future<WatchSessionProjection> _projectWithCurrentRevision() {
|
||||
return _projector.project(revision: _revision);
|
||||
}
|
||||
}
|
||||
|
||||
final class WatchSessionProjectionProjector {
|
||||
const WatchSessionProjectionProjector({
|
||||
required this.sessionRepository,
|
||||
required this.clock,
|
||||
required this.ids,
|
||||
required this.originDeviceId,
|
||||
});
|
||||
|
||||
final ActiveSessionRepository sessionRepository;
|
||||
final Clock clock;
|
||||
final IdGenerator ids;
|
||||
final String originDeviceId;
|
||||
|
||||
Future<WatchSessionProjection> project({required int revision}) async {
|
||||
final now = clock.now();
|
||||
final session = await sessionRepository.findOpen();
|
||||
if (session == null ||
|
||||
session.status == ActiveWorkoutStatus.completed ||
|
||||
session.status == ActiveWorkoutStatus.abandoned ||
|
||||
session.status == ActiveWorkoutStatus.savedExit) {
|
||||
return WatchSessionProjection(
|
||||
deviceSessionId: '',
|
||||
revision: revision,
|
||||
projectedAtEpochMs: _epochMs(now),
|
||||
phase: WatchSessionPhase.noActiveSession,
|
||||
phoneReachable: true,
|
||||
seriesIndex: 0,
|
||||
seriesTotal: 0,
|
||||
exerciseName: '',
|
||||
primaryAction: WatchPrimaryAction.none,
|
||||
statusLabel: 'Aucune séance en cours',
|
||||
);
|
||||
}
|
||||
|
||||
final snapshot = _findExerciseSnapshot(
|
||||
resolvedTemplateSnapshotJson: session.resolvedTemplateSnapshotJson,
|
||||
programIndex: session.currentProgramIndex,
|
||||
exerciseIndex: session.currentExerciseIndex,
|
||||
);
|
||||
if (snapshot == null) {
|
||||
return WatchSessionProjection(
|
||||
deviceSessionId: session.metadata.id,
|
||||
revision: revision,
|
||||
projectedAtEpochMs: _epochMs(now),
|
||||
phase: WatchSessionPhase.noActiveSession,
|
||||
phoneReachable: true,
|
||||
seriesIndex: session.currentSetIndex + 1,
|
||||
seriesTotal: 0,
|
||||
exerciseName: '',
|
||||
primaryAction: WatchPrimaryAction.none,
|
||||
statusLabel: 'Séance indisponible',
|
||||
);
|
||||
}
|
||||
|
||||
final activeRest = await _findActiveRest(session.metadata.id);
|
||||
final setTimer = await sessionRepository.findSetTimerState(
|
||||
sessionId: session.metadata.id,
|
||||
programIndex: session.currentProgramIndex,
|
||||
exerciseIndex: session.currentExerciseIndex,
|
||||
setIndex: session.currentSetIndex,
|
||||
);
|
||||
final scoreStopwatch = await sessionRepository.findScoreStopwatchState(
|
||||
sessionId: session.metadata.id,
|
||||
programIndex: session.currentProgramIndex,
|
||||
exerciseIndex: session.currentExerciseIndex,
|
||||
setIndex: session.currentSetIndex,
|
||||
);
|
||||
final stepView = await _readStepViewIfStarted(session, snapshot);
|
||||
final stepState = stepView?.state;
|
||||
final currentStep = stepView?.currentStep ?? _initialStep(snapshot);
|
||||
final expectedPassages = _expectedPassages(snapshot);
|
||||
final projectedAtEpochMs = _epochMs(now);
|
||||
|
||||
final timers = <WatchTimerProjection>[
|
||||
if (activeRest != null) _restTimerProjection(activeRest, now),
|
||||
if (currentStep != null && currentStep.type == ExerciseStepType.time)
|
||||
_stepTimerProjection(stepState, currentStep, now),
|
||||
if (scoreStopwatch != null)
|
||||
?_scoreStopwatchTimerProjection(scoreStopwatch, now),
|
||||
if (setTimer != null) ?_setTimerProjection(setTimer, now),
|
||||
];
|
||||
final dominantTimer = timers.isEmpty ? null : timers.first;
|
||||
final secondaryTimers = dominantTimer == null
|
||||
? const <WatchTimerProjection>[]
|
||||
: timers.skip(1).toList(growable: false);
|
||||
final phase = _phase(
|
||||
session: session,
|
||||
snapshot: snapshot,
|
||||
activeRest: activeRest,
|
||||
stepState: stepState,
|
||||
currentStep: currentStep,
|
||||
timers: timers,
|
||||
);
|
||||
|
||||
return WatchSessionProjection(
|
||||
deviceSessionId: session.metadata.id,
|
||||
revision: revision,
|
||||
projectedAtEpochMs: projectedAtEpochMs,
|
||||
phase: phase,
|
||||
phoneReachable: true,
|
||||
seriesIndex: session.currentSetIndex + 1,
|
||||
seriesTotal: snapshot.setsCount,
|
||||
exerciseName: snapshot.exerciseNameSnapshot,
|
||||
passageIndex: expectedPassages > 1 && stepState != null
|
||||
? stepState.currentPassageIndex + 1
|
||||
: null,
|
||||
passageTotal: expectedPassages > 1 ? expectedPassages : null,
|
||||
stepIndex: currentStep == null
|
||||
? null
|
||||
: (stepState?.currentStepIndex ?? 0) + 1,
|
||||
stepTotal: snapshot.steps.isEmpty ? null : snapshot.steps.length,
|
||||
stepName: currentStep?.name,
|
||||
dominantTimer: dominantTimer,
|
||||
secondaryTimers: secondaryTimers,
|
||||
primaryAction: _primaryAction(phase),
|
||||
secondaryActions: _secondaryActions(
|
||||
phase: phase,
|
||||
snapshot: snapshot,
|
||||
stepState: stepState,
|
||||
expectedPassages: expectedPassages,
|
||||
),
|
||||
nextExerciseName: activeRest != null
|
||||
? _restNextExerciseName(
|
||||
session.resolvedTemplateSnapshotJson,
|
||||
session,
|
||||
activeRest,
|
||||
snapshot,
|
||||
)
|
||||
: phase == WatchSessionPhase.betweenSetsReady
|
||||
? _betweenSetsNextExerciseName(
|
||||
session.resolvedTemplateSnapshotJson,
|
||||
session,
|
||||
snapshot,
|
||||
)
|
||||
: null,
|
||||
statusLabel: _statusLabel(phase, dominantTimer),
|
||||
);
|
||||
}
|
||||
|
||||
Future<ActiveExerciseStepProgressView?> _readStepViewIfStarted(
|
||||
ActiveWorkoutSession session,
|
||||
_ResolvedExerciseSnapshot snapshot,
|
||||
) async {
|
||||
if (snapshot.steps.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
final state = await sessionRepository.findExerciseStepProgressState(
|
||||
sessionId: session.metadata.id,
|
||||
programIndex: session.currentProgramIndex,
|
||||
exerciseIndex: session.currentExerciseIndex,
|
||||
setIndex: session.currentSetIndex,
|
||||
);
|
||||
if (state == null) {
|
||||
return null;
|
||||
}
|
||||
if (state.status == ActiveExerciseStepProgressStatus.runningTimer) {
|
||||
return ActiveExerciseStepUseCases(
|
||||
sessionRepository: sessionRepository,
|
||||
clock: clock,
|
||||
ids: ids,
|
||||
originDeviceId: originDeviceId,
|
||||
).readProgress(
|
||||
sessionId: session.metadata.id,
|
||||
programIndex: session.currentProgramIndex,
|
||||
exerciseIndex: session.currentExerciseIndex,
|
||||
setIndex: session.currentSetIndex,
|
||||
);
|
||||
}
|
||||
return ActiveExerciseStepProgressView(
|
||||
state: state,
|
||||
steps: snapshot.steps,
|
||||
expectedPassages: _expectedPassages(snapshot),
|
||||
results: const [],
|
||||
);
|
||||
}
|
||||
|
||||
Future<ActiveRestState?> _findActiveRest(String sessionId) async {
|
||||
final active =
|
||||
(await sessionRepository.listRestStates(sessionId))
|
||||
.where((rest) => rest.endedAt == null && rest.skippedAt == null)
|
||||
.toList()
|
||||
..sort((left, right) => right.startedAt.compareTo(left.startedAt));
|
||||
return active.isEmpty ? null : active.first;
|
||||
}
|
||||
}
|
||||
|
||||
WatchSessionPhase _phase({
|
||||
required ActiveWorkoutSession session,
|
||||
required _ResolvedExerciseSnapshot snapshot,
|
||||
required ActiveRestState? activeRest,
|
||||
required ActiveExerciseStepProgressState? stepState,
|
||||
required ExerciseStep? currentStep,
|
||||
required List<WatchTimerProjection> timers,
|
||||
}) {
|
||||
if (activeRest != null) {
|
||||
return activeRest.pausedAt == null
|
||||
? WatchSessionPhase.restRunning
|
||||
: WatchSessionPhase.restPaused;
|
||||
}
|
||||
if (session.status == ActiveWorkoutStatus.paused) {
|
||||
return WatchSessionPhase.paused;
|
||||
}
|
||||
if (_isNextTimerReady(snapshot, stepState, currentStep)) {
|
||||
return WatchSessionPhase.nextTimerReady;
|
||||
}
|
||||
if (timers.any((timer) => timer.runState == WatchTimerRunState.running)) {
|
||||
return WatchSessionPhase.running;
|
||||
}
|
||||
return session.currentProgramIndex == 0 &&
|
||||
session.currentExerciseIndex == 0 &&
|
||||
session.currentSetIndex == 0
|
||||
? WatchSessionPhase.ready
|
||||
: WatchSessionPhase.betweenSetsReady;
|
||||
}
|
||||
|
||||
WatchPrimaryAction _primaryAction(WatchSessionPhase phase) {
|
||||
return switch (phase) {
|
||||
WatchSessionPhase.noActiveSession => WatchPrimaryAction.none,
|
||||
WatchSessionPhase.ready => WatchPrimaryAction.startCurrentExercise,
|
||||
WatchSessionPhase.running => WatchPrimaryAction.pauseSession,
|
||||
WatchSessionPhase.paused => WatchPrimaryAction.resumeSession,
|
||||
WatchSessionPhase.nextTimerReady =>
|
||||
WatchPrimaryAction.startPreparedTimedStep,
|
||||
WatchSessionPhase.restRunning => WatchPrimaryAction.pauseSession,
|
||||
WatchSessionPhase.restPaused => WatchPrimaryAction.resumeSession,
|
||||
WatchSessionPhase.betweenSetsReady =>
|
||||
WatchPrimaryAction.startCurrentExercise,
|
||||
};
|
||||
}
|
||||
|
||||
List<WatchSecondaryAction> _secondaryActions({
|
||||
required WatchSessionPhase phase,
|
||||
required _ResolvedExerciseSnapshot snapshot,
|
||||
required ActiveExerciseStepProgressState? stepState,
|
||||
required int expectedPassages,
|
||||
}) {
|
||||
if (phase == WatchSessionPhase.noActiveSession) {
|
||||
return const [];
|
||||
}
|
||||
if (phase == WatchSessionPhase.restRunning ||
|
||||
phase == WatchSessionPhase.restPaused) {
|
||||
return const [WatchSecondaryAction.skipCurrentRest];
|
||||
}
|
||||
final hasCurrentStep =
|
||||
snapshot.steps.isNotEmpty &&
|
||||
stepState?.status != ActiveExerciseStepProgressStatus.sequenceComplete;
|
||||
final hasPassageToSkip =
|
||||
hasCurrentStep &&
|
||||
expectedPassages > 1 &&
|
||||
stepState != null &&
|
||||
stepState.currentPassageIndex < expectedPassages - 1;
|
||||
return [
|
||||
if (hasCurrentStep) WatchSecondaryAction.skipCurrentStep,
|
||||
if (hasPassageToSkip) WatchSecondaryAction.skipCurrentPassage,
|
||||
WatchSecondaryAction.finishCurrentSet,
|
||||
WatchSecondaryAction.skipCurrentSet,
|
||||
];
|
||||
}
|
||||
|
||||
String _statusLabel(
|
||||
WatchSessionPhase phase,
|
||||
WatchTimerProjection? dominantTimer,
|
||||
) {
|
||||
return switch (phase) {
|
||||
WatchSessionPhase.noActiveSession => 'Aucune séance en cours',
|
||||
WatchSessionPhase.ready => 'Prêt à démarrer',
|
||||
WatchSessionPhase.running => dominantTimer?.label ?? 'En cours',
|
||||
WatchSessionPhase.paused => 'Séance en pause',
|
||||
WatchSessionPhase.nextTimerReady => 'Chrono suivant prêt',
|
||||
WatchSessionPhase.restRunning => 'Repos en cours',
|
||||
WatchSessionPhase.restPaused => 'Repos en pause',
|
||||
WatchSessionPhase.betweenSetsReady => 'Prêt pour la série suivante',
|
||||
};
|
||||
}
|
||||
|
||||
WatchTimerProjection _restTimerProjection(ActiveRestState rest, DateTime now) {
|
||||
final paused = rest.pausedAt != null;
|
||||
return WatchTimerProjection(
|
||||
kind: WatchTimerKind.rest,
|
||||
label: 'Repos',
|
||||
displayMode: WatchTimerDisplayMode.countdown,
|
||||
runState: paused ? WatchTimerRunState.paused : WatchTimerRunState.running,
|
||||
referenceEpochMs: _epochMs(now),
|
||||
accumulatedMs: rest.elapsedMillisecondsAt(now),
|
||||
startedAtEpochMs: paused ? null : _epochMs(now),
|
||||
targetMs: rest.adjustedRestSeconds * 1000,
|
||||
);
|
||||
}
|
||||
|
||||
WatchTimerProjection _stepTimerProjection(
|
||||
ActiveExerciseStepProgressState? state,
|
||||
ExerciseStep step,
|
||||
DateTime now,
|
||||
) {
|
||||
return WatchTimerProjection(
|
||||
kind: WatchTimerKind.step,
|
||||
label: 'Chrono étape',
|
||||
displayMode: WatchTimerDisplayMode.countdown,
|
||||
runState: switch (state?.status) {
|
||||
ActiveExerciseStepProgressStatus.runningTimer =>
|
||||
WatchTimerRunState.running,
|
||||
ActiveExerciseStepProgressStatus.pausedTimer => WatchTimerRunState.paused,
|
||||
_ => WatchTimerRunState.stopped,
|
||||
},
|
||||
referenceEpochMs: _epochMs(now),
|
||||
accumulatedMs: state?.accumulatedMs ?? 0,
|
||||
startedAtEpochMs:
|
||||
state?.status == ActiveExerciseStepProgressStatus.runningTimer
|
||||
? _epochMs(state!.startedAt!)
|
||||
: null,
|
||||
targetMs: step.defaultTargetValue * 1000,
|
||||
);
|
||||
}
|
||||
|
||||
WatchTimerProjection? _scoreStopwatchTimerProjection(
|
||||
ActiveScoreStopwatchState state,
|
||||
DateTime now,
|
||||
) {
|
||||
if (state.status == ActiveScoreStopwatchStatus.stopped) {
|
||||
return null;
|
||||
}
|
||||
return WatchTimerProjection(
|
||||
kind: WatchTimerKind.scoreStopwatch,
|
||||
label: 'Score chrono',
|
||||
displayMode: WatchTimerDisplayMode.elapsed,
|
||||
runState: state.status == ActiveScoreStopwatchStatus.running
|
||||
? WatchTimerRunState.running
|
||||
: WatchTimerRunState.paused,
|
||||
referenceEpochMs: _epochMs(now),
|
||||
accumulatedMs: state.accumulatedMs,
|
||||
startedAtEpochMs: state.status == ActiveScoreStopwatchStatus.running
|
||||
? _epochMs(state.startedAt)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
WatchTimerProjection? _setTimerProjection(
|
||||
ActiveSetTimerState state,
|
||||
DateTime now,
|
||||
) {
|
||||
if (state.status == ActiveSetTimerStatus.stopped ||
|
||||
state.status == ActiveSetTimerStatus.skipped) {
|
||||
return null;
|
||||
}
|
||||
return WatchTimerProjection(
|
||||
kind: WatchTimerKind.setTimer,
|
||||
label: 'Temps de série',
|
||||
displayMode: WatchTimerDisplayMode.elapsed,
|
||||
runState: state.status == ActiveSetTimerStatus.running
|
||||
? WatchTimerRunState.running
|
||||
: WatchTimerRunState.paused,
|
||||
referenceEpochMs: _epochMs(now),
|
||||
accumulatedMs: state.accumulatedMs,
|
||||
startedAtEpochMs:
|
||||
state.status == ActiveSetTimerStatus.running && state.startedAt != null
|
||||
? _epochMs(state.startedAt!)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
ExerciseStep? _initialStep(_ResolvedExerciseSnapshot snapshot) {
|
||||
return snapshot.steps.isEmpty ? null : snapshot.steps.first;
|
||||
}
|
||||
|
||||
bool _isNextTimerReady(
|
||||
_ResolvedExerciseSnapshot snapshot,
|
||||
ActiveExerciseStepProgressState? state,
|
||||
ExerciseStep? currentStep,
|
||||
) {
|
||||
if (state == null ||
|
||||
currentStep == null ||
|
||||
state.status != ActiveExerciseStepProgressStatus.stoppedTimer ||
|
||||
currentStep.type != ExerciseStepType.time ||
|
||||
snapshot.autoStartNextTimedStepEffective) {
|
||||
return false;
|
||||
}
|
||||
final previous = _previousStep(snapshot, state);
|
||||
return previous?.type == ExerciseStepType.time;
|
||||
}
|
||||
|
||||
ExerciseStep? _previousStep(
|
||||
_ResolvedExerciseSnapshot snapshot,
|
||||
ActiveExerciseStepProgressState state,
|
||||
) {
|
||||
if (snapshot.steps.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
if (state.currentStepIndex > 0) {
|
||||
return snapshot.steps[state.currentStepIndex - 1];
|
||||
}
|
||||
if (state.currentPassageIndex > 0) {
|
||||
return snapshot.steps.last;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
int _expectedPassages(_ResolvedExerciseSnapshot snapshot) {
|
||||
return snapshot.repsEnabled
|
||||
? (snapshot.targetReps ?? 1).clamp(1, 1 << 31)
|
||||
: 1;
|
||||
}
|
||||
|
||||
String? _restNextExerciseName(
|
||||
String resolvedTemplateSnapshotJson,
|
||||
ActiveWorkoutSession session,
|
||||
ActiveRestState rest,
|
||||
_ResolvedExerciseSnapshot currentSnapshot,
|
||||
) {
|
||||
final sessionIsAfterRestSource =
|
||||
_comparePositions(
|
||||
session.currentProgramIndex,
|
||||
session.currentExerciseIndex,
|
||||
session.currentSetIndex,
|
||||
rest.afterProgramIndex,
|
||||
rest.afterExerciseIndex,
|
||||
rest.afterSetIndex,
|
||||
) >
|
||||
0;
|
||||
if (sessionIsAfterRestSource) {
|
||||
return currentSnapshot.exerciseNameSnapshot;
|
||||
}
|
||||
final snapshots = _exerciseSnapshotsById(resolvedTemplateSnapshotJson);
|
||||
final setSnapshots = _listSetSnapshots(resolvedTemplateSnapshotJson);
|
||||
final currentIndex = setSnapshots.indexWhere(
|
||||
(snapshot) =>
|
||||
snapshot.programIndex == rest.afterProgramIndex &&
|
||||
snapshot.exerciseIndex == rest.afterExerciseIndex &&
|
||||
snapshot.setIndex == rest.afterSetIndex,
|
||||
);
|
||||
if (currentIndex == -1 || currentIndex + 1 >= setSnapshots.length) {
|
||||
return null;
|
||||
}
|
||||
final next = setSnapshots[currentIndex + 1];
|
||||
return snapshots[next.exerciseSnapshotId]?.exerciseNameSnapshot;
|
||||
}
|
||||
|
||||
String? _betweenSetsNextExerciseName(
|
||||
String resolvedTemplateSnapshotJson,
|
||||
ActiveWorkoutSession session,
|
||||
_ResolvedExerciseSnapshot currentSnapshot,
|
||||
) {
|
||||
final setSnapshots = _listSetSnapshots(resolvedTemplateSnapshotJson);
|
||||
final currentIndex = setSnapshots.indexWhere(
|
||||
(snapshot) =>
|
||||
snapshot.programIndex == session.currentProgramIndex &&
|
||||
snapshot.exerciseIndex == session.currentExerciseIndex &&
|
||||
snapshot.setIndex == session.currentSetIndex,
|
||||
);
|
||||
if (currentIndex <= 0) {
|
||||
return null;
|
||||
}
|
||||
final previous = setSnapshots[currentIndex - 1];
|
||||
final current = setSnapshots[currentIndex];
|
||||
if (previous.exerciseSnapshotId == current.exerciseSnapshotId) {
|
||||
return null;
|
||||
}
|
||||
return currentSnapshot.exerciseNameSnapshot;
|
||||
}
|
||||
|
||||
int _epochMs(DateTime value) => value.toUtc().millisecondsSinceEpoch;
|
||||
|
||||
final class ActiveExerciseStepUseCases {
|
||||
const ActiveExerciseStepUseCases({
|
||||
required this.sessionRepository,
|
||||
@ -4519,6 +5030,7 @@ _ResolvedExerciseSnapshot? _findExerciseSnapshot({
|
||||
),
|
||||
scoreLabelSnapshot: exercise['scoreLabelSnapshot'] as String?,
|
||||
scoreUnitSnapshot: exercise['scoreUnitSnapshot'] as String?,
|
||||
setsCount: exercise['setsCount'] as int? ?? 0,
|
||||
steps: _exerciseStepsFromSnapshot(exercise['exerciseStepsSnapshot']),
|
||||
autoStartNextTimedStepEffective: autoStartNextTimedStepEffective,
|
||||
);
|
||||
@ -4730,6 +5242,7 @@ Map<String, _ResolvedExerciseSnapshot> _exerciseSnapshotsById(
|
||||
),
|
||||
scoreLabelSnapshot: exercise['scoreLabelSnapshot'] as String?,
|
||||
scoreUnitSnapshot: exercise['scoreUnitSnapshot'] as String?,
|
||||
setsCount: exercise['setsCount'] as int? ?? 0,
|
||||
steps: _exerciseStepsFromSnapshot(exercise['exerciseStepsSnapshot']),
|
||||
autoStartNextTimedStepEffective:
|
||||
(exercise['autoStartNextTimedStepOverride'] as bool?) ??
|
||||
@ -4758,6 +5271,7 @@ final class _ResolvedExerciseSnapshot {
|
||||
required this.scoreInputModeSnapshot,
|
||||
this.scoreLabelSnapshot,
|
||||
this.scoreUnitSnapshot,
|
||||
required this.setsCount,
|
||||
this.steps = const [],
|
||||
this.autoStartNextTimedStepEffective = true,
|
||||
});
|
||||
@ -4777,6 +5291,7 @@ final class _ResolvedExerciseSnapshot {
|
||||
final ScoreInputMode scoreInputModeSnapshot;
|
||||
final String? scoreLabelSnapshot;
|
||||
final String? scoreUnitSnapshot;
|
||||
final int setsCount;
|
||||
final List<ExerciseStep> steps;
|
||||
final bool autoStartNextTimedStepEffective;
|
||||
}
|
||||
|
||||
20
lib/application/watch_companion_use_cases.dart
Normal file
20
lib/application/watch_companion_use_cases.dart
Normal file
@ -0,0 +1,20 @@
|
||||
import 'package:watch_bridge_contract/watch_bridge_contract.dart';
|
||||
|
||||
abstract interface class WatchCommandIngress {
|
||||
Future<WatchCommandAck> dispatch(WatchCommandEnvelope command);
|
||||
}
|
||||
|
||||
abstract interface class WatchProjectionPublisher {
|
||||
Future<void> publish(WatchSessionProjection projection);
|
||||
}
|
||||
|
||||
abstract interface class WatchProjectionSource {
|
||||
Stream<WatchSessionProjection> get projections;
|
||||
|
||||
Future<WatchSessionProjection> currentProjection();
|
||||
|
||||
Future<WatchSessionProjection> emitCurrentProjection();
|
||||
}
|
||||
|
||||
abstract interface class WatchCompanionUseCases
|
||||
implements WatchCommandIngress, WatchProjectionSource {}
|
||||
Reference in New Issue
Block a user