This commit is contained in:
@ -19,6 +19,7 @@ abstract interface class AppDependencies {
|
||||
WorkoutHistoryUseCases get workoutHistoryUseCases;
|
||||
ProgressionStatsUseCase get progressionStatsUseCase;
|
||||
ExercisePerformanceReferenceUseCase get exercisePerformanceReferenceUseCase;
|
||||
WatchAlertPublisher get watchAlertPublisher;
|
||||
DataExportUseCase get dataExportUseCase;
|
||||
DataImportUseCase get dataImportUseCase;
|
||||
SyncUseCases get syncUseCases;
|
||||
@ -74,6 +75,8 @@ final class AppBootstrap implements AppDependencies {
|
||||
final WatchCompanionProjectionUseCases watchCompanionProjectionUseCases;
|
||||
final WatchCompanionCommandHandler watchCompanionCommandHandler;
|
||||
final WatchWearDataLayerAdapter watchWearDataLayerAdapter;
|
||||
@override
|
||||
WatchAlertPublisher get watchAlertPublisher => watchWearDataLayerAdapter;
|
||||
final SessionNotificationCoordinator sessionNotificationCoordinator;
|
||||
@override
|
||||
final CloseWorkoutSessionUseCase closeWorkoutSessionUseCase;
|
||||
|
||||
@ -3507,7 +3507,6 @@ bool _hasSameWatchCommandRevisionState(
|
||||
left.deviceSessionId == right.deviceSessionId &&
|
||||
left.phase == right.phase &&
|
||||
left.phoneReachable == right.phoneReachable &&
|
||||
left.expiresAtEpochMs == right.expiresAtEpochMs &&
|
||||
left.seriesIndex == right.seriesIndex &&
|
||||
left.seriesTotal == right.seriesTotal &&
|
||||
left.exerciseName == right.exerciseName &&
|
||||
|
||||
@ -8,6 +8,10 @@ abstract interface class WatchProjectionPublisher {
|
||||
Future<void> publish(WatchSessionProjection projection);
|
||||
}
|
||||
|
||||
abstract interface class WatchAlertPublisher {
|
||||
Future<void> publishAlert(WatchAlertEnvelope alert);
|
||||
}
|
||||
|
||||
abstract interface class WatchProjectionSource {
|
||||
Stream<WatchSessionProjection> get projections;
|
||||
|
||||
|
||||
@ -24,6 +24,8 @@ abstract interface class WatchBridgeNativeChannel {
|
||||
|
||||
Future<void> publishProjection(WatchSessionProjection projection);
|
||||
|
||||
Future<void> publishAlert(WatchAlertEnvelope alert);
|
||||
|
||||
Future<void> sendCommandAck(
|
||||
WatchCommandEnvelope command,
|
||||
WatchCommandAck ack, {
|
||||
@ -129,6 +131,11 @@ final class MethodChannelWatchBridgeNativeChannel
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> publishAlert(WatchAlertEnvelope alert) {
|
||||
return _invokeIgnoringMissingPlugin('publishAlert', alert.toJson());
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> requestCapabilityRefresh() {
|
||||
return _invokeIgnoringMissingPlugin('requestCapabilityRefresh');
|
||||
|
||||
@ -6,7 +6,8 @@ import '../../application/use_cases.dart';
|
||||
import '../../application/watch_companion_use_cases.dart';
|
||||
import 'native_watch_bridge_channel.dart';
|
||||
|
||||
final class WatchWearDataLayerAdapter implements WatchProjectionPublisher {
|
||||
final class WatchWearDataLayerAdapter
|
||||
implements WatchProjectionPublisher, WatchAlertPublisher {
|
||||
WatchWearDataLayerAdapter({
|
||||
required WatchBridgeNativeChannel nativeChannel,
|
||||
required WatchCommandIngress commandIngress,
|
||||
@ -108,6 +109,11 @@ final class WatchWearDataLayerAdapter implements WatchProjectionPublisher {
|
||||
await _syncForegroundService(projection);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> publishAlert(WatchAlertEnvelope alert) {
|
||||
return _nativeChannel.publishAlert(alert);
|
||||
}
|
||||
|
||||
Future<void> _enqueueCommand(WatchCommandEnvelope command) {
|
||||
final run = _commandTail.then(
|
||||
(_) => _handleCommand(command),
|
||||
|
||||
@ -35,6 +35,9 @@ final class AudioplayersExerciseStepAudioCuePlayer
|
||||
Future<void> _configureAudioContext() async {
|
||||
try {
|
||||
await _player.setAudioContext(_stepCueAudioContext);
|
||||
await _player.setPlayerMode(PlayerMode.lowLatency);
|
||||
await _player.setReleaseMode(ReleaseMode.stop);
|
||||
await _player.setVolume(1);
|
||||
} on Object catch (error) {
|
||||
debugPrint('Audio cue context ignored: $error');
|
||||
}
|
||||
|
||||
@ -18,6 +18,7 @@ final class HistoryListScreen extends StatefulWidget {
|
||||
this.sensorUseCases,
|
||||
this.performanceReferenceUseCase,
|
||||
this.onOpenProgression,
|
||||
this.watchAlertPublisher,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@ -30,6 +31,7 @@ final class HistoryListScreen extends StatefulWidget {
|
||||
final CloseWorkoutSessionUseCase closeUseCase;
|
||||
final MediaUseCases? mediaUseCases;
|
||||
final VoidCallback? onOpenProgression;
|
||||
final WatchAlertPublisher? watchAlertPublisher;
|
||||
|
||||
@override
|
||||
State<HistoryListScreen> createState() => _HistoryListScreenState();
|
||||
@ -116,6 +118,7 @@ final class _HistoryListScreenState extends State<HistoryListScreen> {
|
||||
performanceReferenceUseCase: widget.performanceReferenceUseCase,
|
||||
closeUseCase: widget.closeUseCase,
|
||||
mediaUseCases: widget.mediaUseCases,
|
||||
watchAlertPublisher: widget.watchAlertPublisher,
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -138,6 +141,7 @@ final class HistoryDetailScreen extends StatelessWidget {
|
||||
this.stepUseCases,
|
||||
this.sensorUseCases,
|
||||
this.performanceReferenceUseCase,
|
||||
this.watchAlertPublisher,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@ -150,6 +154,7 @@ final class HistoryDetailScreen extends StatelessWidget {
|
||||
final ExercisePerformanceReferenceUseCase? performanceReferenceUseCase;
|
||||
final CloseWorkoutSessionUseCase closeUseCase;
|
||||
final MediaUseCases? mediaUseCases;
|
||||
final WatchAlertPublisher? watchAlertPublisher;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -265,6 +270,7 @@ final class HistoryDetailScreen extends StatelessWidget {
|
||||
workoutTemplateUseCases: workoutTemplateUseCases,
|
||||
performanceReferenceUseCase: performanceReferenceUseCase,
|
||||
mediaUseCases: mediaUseCases,
|
||||
watchAlertPublisher: watchAlertPublisher,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@ -170,6 +170,7 @@ final class _HomeScreenState extends State<HomeScreen> with RouteAware {
|
||||
shareUseCases: widget.bootstrap.shareUseCases,
|
||||
authUseCases: widget.bootstrap.authUseCases,
|
||||
syncUseCases: widget.bootstrap.syncUseCases,
|
||||
watchAlertPublisher: widget.bootstrap.watchAlertPublisher,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -191,6 +192,7 @@ final class _HomeScreenState extends State<HomeScreen> with RouteAware {
|
||||
widget.bootstrap.exercisePerformanceReferenceUseCase,
|
||||
closeUseCase: widget.bootstrap.closeWorkoutSessionUseCase,
|
||||
mediaUseCases: widget.bootstrap.mediaUseCases,
|
||||
watchAlertPublisher: widget.bootstrap.watchAlertPublisher,
|
||||
onOpenProgression: () => _openProgression(context),
|
||||
),
|
||||
),
|
||||
@ -256,6 +258,7 @@ final class _HomeScreenState extends State<HomeScreen> with RouteAware {
|
||||
widget.bootstrap.exercisePerformanceReferenceUseCase,
|
||||
closeUseCase: widget.bootstrap.closeWorkoutSessionUseCase,
|
||||
mediaUseCases: widget.bootstrap.mediaUseCases,
|
||||
watchAlertPublisher: widget.bootstrap.watchAlertPublisher,
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -276,6 +279,7 @@ final class _HomeScreenState extends State<HomeScreen> with RouteAware {
|
||||
performanceReferenceUseCase:
|
||||
widget.bootstrap.exercisePerformanceReferenceUseCase,
|
||||
mediaUseCases: widget.bootstrap.mediaUseCases,
|
||||
watchAlertPublisher: widget.bootstrap.watchAlertPublisher,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@ -16,6 +16,7 @@ final class ProgressionScreen extends StatefulWidget {
|
||||
this.mediaUseCases,
|
||||
this.stepUseCases,
|
||||
this.performanceReferenceUseCase,
|
||||
this.watchAlertPublisher,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@ -27,6 +28,7 @@ final class ProgressionScreen extends StatefulWidget {
|
||||
final ExercisePerformanceReferenceUseCase? performanceReferenceUseCase;
|
||||
final CloseWorkoutSessionUseCase closeUseCase;
|
||||
final MediaUseCases? mediaUseCases;
|
||||
final WatchAlertPublisher? watchAlertPublisher;
|
||||
|
||||
@override
|
||||
State<ProgressionScreen> createState() => _ProgressionScreenState();
|
||||
@ -247,6 +249,7 @@ final class _ProgressionScreenState extends State<ProgressionScreen> {
|
||||
performanceReferenceUseCase: widget.performanceReferenceUseCase,
|
||||
closeUseCase: widget.closeUseCase,
|
||||
mediaUseCases: widget.mediaUseCases,
|
||||
watchAlertPublisher: widget.watchAlertPublisher,
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -278,6 +281,7 @@ final class _ProgressionScreenState extends State<ProgressionScreen> {
|
||||
performanceReferenceUseCase: widget.performanceReferenceUseCase,
|
||||
closeUseCase: widget.closeUseCase,
|
||||
mediaUseCases: widget.mediaUseCases,
|
||||
watchAlertPublisher: widget.watchAlertPublisher,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@ -4,6 +4,7 @@ import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
import 'package:watch_bridge_contract/watch_bridge_contract.dart';
|
||||
|
||||
import '../application/application.dart';
|
||||
import '../domain/domain.dart';
|
||||
@ -30,6 +31,7 @@ final class WorkoutExecutionScreen extends StatefulWidget {
|
||||
this.stepUseCases,
|
||||
this.sensorUseCases,
|
||||
this.stepAudioCuePlayer,
|
||||
this.watchAlertPublisher,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@ -45,6 +47,7 @@ final class WorkoutExecutionScreen extends StatefulWidget {
|
||||
final VideoMediaBuilder? videoMediaBuilder;
|
||||
final ActiveWorkoutSensorUseCases? sensorUseCases;
|
||||
final ExerciseStepAudioCuePlayer? stepAudioCuePlayer;
|
||||
final WatchAlertPublisher? watchAlertPublisher;
|
||||
|
||||
@override
|
||||
State<WorkoutExecutionScreen> createState() => _WorkoutExecutionScreenState();
|
||||
@ -1077,6 +1080,7 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
||||
}
|
||||
|
||||
Future<void> _playStepAudioCue({required bool short}) async {
|
||||
unawaited(_publishStepAlert(short: short));
|
||||
try {
|
||||
if (short) {
|
||||
await _stepAudioCuePlayer.playShortCountdownBeep();
|
||||
@ -1088,6 +1092,52 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _publishStepAlert({required bool short}) async {
|
||||
final publisher = widget.watchAlertPublisher;
|
||||
if (publisher == null) {
|
||||
return;
|
||||
}
|
||||
final nowMs = DateTime.now().toUtc().millisecondsSinceEpoch;
|
||||
final position = _position;
|
||||
final stepState = _stepProgress?.state;
|
||||
final passageIndex = stepState?.currentPassageIndex;
|
||||
final stepIndex = stepState?.currentStepIndex;
|
||||
final ttlMs = short ? 700 : 2000;
|
||||
final alert = WatchAlertEnvelope(
|
||||
alertId: [
|
||||
_session.metadata.id,
|
||||
position.programIndex,
|
||||
position.exerciseIndex,
|
||||
position.setIndex,
|
||||
passageIndex,
|
||||
stepIndex,
|
||||
short ? 'tick' : 'finish',
|
||||
nowMs,
|
||||
].join('-'),
|
||||
sessionId: _session.metadata.id,
|
||||
revision: 0,
|
||||
kind: short
|
||||
? WatchAlertKind.countdownTick
|
||||
: WatchAlertKind.countdownFinished,
|
||||
timerKind: WatchTimerKind.step,
|
||||
programIndex: position.programIndex,
|
||||
exerciseIndex: position.exerciseIndex,
|
||||
setIndex: position.setIndex,
|
||||
passageIndex: passageIndex,
|
||||
stepIndex: stepIndex,
|
||||
scheduledForEpochMs: nowMs,
|
||||
expiresAtEpochMs: nowMs + ttlMs,
|
||||
pattern: short
|
||||
? WatchAlertPattern.countdownTick
|
||||
: WatchAlertPattern.timerFinished,
|
||||
);
|
||||
try {
|
||||
await publisher.publishAlert(alert);
|
||||
} on Object catch (error) {
|
||||
debugPrint('Watch alert ignored: $error');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _startCurrentStepTimer() async {
|
||||
if (_stepProgress?.currentStep?.type != ExerciseStepType.time) return;
|
||||
try {
|
||||
@ -1818,6 +1868,7 @@ final class _WorkoutExecutionScreenState extends State<WorkoutExecutionScreen> {
|
||||
mediaAssetLoader: widget.mediaAssetLoader,
|
||||
videoMediaBuilder: widget.videoMediaBuilder,
|
||||
sensorUseCases: widget.sensorUseCases,
|
||||
watchAlertPublisher: widget.watchAlertPublisher,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@ -24,6 +24,7 @@ final class WorkoutTemplateListScreen extends StatefulWidget {
|
||||
this.shareUseCases,
|
||||
this.authUseCases,
|
||||
this.syncUseCases,
|
||||
this.watchAlertPublisher,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@ -39,6 +40,7 @@ final class WorkoutTemplateListScreen extends StatefulWidget {
|
||||
final ShareUseCases? shareUseCases;
|
||||
final AuthUseCases? authUseCases;
|
||||
final SyncUseCases? syncUseCases;
|
||||
final WatchAlertPublisher? watchAlertPublisher;
|
||||
|
||||
@override
|
||||
State<WorkoutTemplateListScreen> createState() =>
|
||||
@ -311,6 +313,7 @@ final class _WorkoutTemplateListScreenState
|
||||
performanceReferenceUseCase: widget.performanceReferenceUseCase,
|
||||
mediaUseCases: widget.mediaUseCases,
|
||||
sensorUseCases: widget.sensorUseCases,
|
||||
watchAlertPublisher: widget.watchAlertPublisher,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user