feat(watch): clôture lot #91 - fréquence cardiaque live, notifications de séance et finitions montre

Consolide le lot applicatif watch companion validé :
- télémétrie fréquence cardiaque live remontée montre -> téléphone
  (collecteur watch, adapter Wear Data Layer, persistance Drift,
  propagation aux écrans historique/programme/profil/exécution)
- notifications de séance en arrière-plan côté téléphone (service
  foreground de statut + passerelle applicative)
- finitions montre : chrono d'étape, score d'étape, retrait du bouton
  "lancer une séance", thème, icônes et polices watch_app

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-28 05:56:12 +02:00
parent c65a5a76a9
commit 65d43b9768
80 changed files with 12292 additions and 892 deletions

View File

@ -1,4 +1,4 @@
const int watchBridgeSchemaVersion = 1;
const int watchBridgeSchemaVersion = 4;
enum WatchCommandType {
startCurrentExercise,
@ -10,6 +10,8 @@ enum WatchCommandType {
finishCurrentSet,
skipCurrentSet,
skipCurrentRest,
incrementScore,
decrementScore,
}
enum WatchCommandAck {
@ -56,6 +58,8 @@ enum WatchTimerDisplayMode { countdown, elapsed }
enum WatchTimerRunState { stopped, running, paused }
enum WatchManualScoreScope { series, step }
final class WatchCommandEnvelope {
const WatchCommandEnvelope({
this.schemaVersion = watchBridgeSchemaVersion,
@ -138,6 +142,9 @@ final class WatchSessionProjection {
required this.seriesIndex,
required this.seriesTotal,
required this.exerciseName,
this.programIndex,
this.exerciseIndex,
this.setIndex,
this.passageIndex,
this.passageTotal,
this.stepIndex,
@ -149,6 +156,12 @@ final class WatchSessionProjection {
this.secondaryActions = const [],
this.nextExerciseName,
this.statusLabel,
this.hasManualScore = false,
this.currentManualScoreValue,
this.canDecrementScore = false,
this.manualScoreTargetValue,
this.manualScoreTargetLabel,
this.manualScoreScope,
});
factory WatchSessionProjection.fromJson(Map<String, Object?> json) {
@ -169,6 +182,9 @@ final class WatchSessionProjection {
seriesIndex: _intFromJson(json['seriesIndex'], 0),
seriesTotal: _intFromJson(json['seriesTotal'], 0),
exerciseName: _stringFromJson(json['exerciseName']),
programIndex: _nullableIntFromJson(json['programIndex']),
exerciseIndex: _nullableIntFromJson(json['exerciseIndex']),
setIndex: _nullableIntFromJson(json['setIndex']),
passageIndex: _nullableIntFromJson(json['passageIndex']),
passageTotal: _nullableIntFromJson(json['passageTotal']),
stepIndex: _nullableIntFromJson(json['stepIndex']),
@ -187,6 +203,21 @@ final class WatchSessionProjection {
),
nextExerciseName: _nullableStringFromJson(json['nextExerciseName']),
statusLabel: _nullableStringFromJson(json['statusLabel']),
hasManualScore: _boolFromJson(json['hasManualScore'], false),
currentManualScoreValue: _nullableDoubleFromJson(
json['currentManualScoreValue'],
),
canDecrementScore: _boolFromJson(json['canDecrementScore'], false),
manualScoreTargetValue: _nullableDoubleFromJson(
json['manualScoreTargetValue'],
),
manualScoreTargetLabel: _nullableStringFromJson(
json['manualScoreTargetLabel'],
),
manualScoreScope: _nullableEnumFromJson(
json['manualScoreScope'],
WatchManualScoreScope.values,
),
);
}
@ -199,6 +230,9 @@ final class WatchSessionProjection {
final int seriesIndex;
final int seriesTotal;
final String exerciseName;
final int? programIndex;
final int? exerciseIndex;
final int? setIndex;
final int? passageIndex;
final int? passageTotal;
final int? stepIndex;
@ -210,6 +244,12 @@ final class WatchSessionProjection {
final List<WatchSecondaryAction> secondaryActions;
final String? nextExerciseName;
final String? statusLabel;
final bool hasManualScore;
final double? currentManualScoreValue;
final bool canDecrementScore;
final double? manualScoreTargetValue;
final String? manualScoreTargetLabel;
final WatchManualScoreScope? manualScoreScope;
Map<String, Object?> toJson() {
return {
@ -222,6 +262,9 @@ final class WatchSessionProjection {
'seriesIndex': seriesIndex,
'seriesTotal': seriesTotal,
'exerciseName': exerciseName,
'programIndex': programIndex,
'exerciseIndex': exerciseIndex,
'setIndex': setIndex,
'passageIndex': passageIndex,
'passageTotal': passageTotal,
'stepIndex': stepIndex,
@ -237,6 +280,12 @@ final class WatchSessionProjection {
.toList(),
'nextExerciseName': nextExerciseName,
'statusLabel': statusLabel,
'hasManualScore': hasManualScore,
'currentManualScoreValue': currentManualScoreValue,
'canDecrementScore': canDecrementScore,
'manualScoreTargetValue': manualScoreTargetValue,
'manualScoreTargetLabel': manualScoreTargetLabel,
'manualScoreScope': manualScoreScope?.name,
};
}
@ -253,6 +302,9 @@ final class WatchSessionProjection {
seriesIndex == other.seriesIndex &&
seriesTotal == other.seriesTotal &&
exerciseName == other.exerciseName &&
programIndex == other.programIndex &&
exerciseIndex == other.exerciseIndex &&
setIndex == other.setIndex &&
passageIndex == other.passageIndex &&
passageTotal == other.passageTotal &&
stepIndex == other.stepIndex &&
@ -263,12 +315,18 @@ final class WatchSessionProjection {
primaryAction == other.primaryAction &&
_listEquals(secondaryActions, other.secondaryActions) &&
nextExerciseName == other.nextExerciseName &&
statusLabel == other.statusLabel;
statusLabel == other.statusLabel &&
hasManualScore == other.hasManualScore &&
currentManualScoreValue == other.currentManualScoreValue &&
canDecrementScore == other.canDecrementScore &&
manualScoreTargetValue == other.manualScoreTargetValue &&
manualScoreTargetLabel == other.manualScoreTargetLabel &&
manualScoreScope == other.manualScoreScope;
}
@override
int get hashCode {
return Object.hash(
return Object.hashAll([
schemaVersion,
deviceSessionId,
revision,
@ -278,6 +336,9 @@ final class WatchSessionProjection {
seriesIndex,
seriesTotal,
exerciseName,
programIndex,
exerciseIndex,
setIndex,
passageIndex,
passageTotal,
stepIndex,
@ -289,10 +350,226 @@ final class WatchSessionProjection {
Object.hashAll(secondaryActions),
nextExerciseName,
statusLabel,
hasManualScore,
currentManualScoreValue,
canDecrementScore,
manualScoreTargetValue,
manualScoreTargetLabel,
manualScoreScope,
]);
}
}
final class WatchSensorSummary {
const WatchSensorSummary({
this.schemaVersion = watchBridgeSchemaVersion,
required this.sessionId,
required this.sampleCount,
this.minHeartRateBpm,
this.averageHeartRateBpm,
this.maxHeartRateBpm,
this.totalDistanceMeters,
this.totalCaloriesKcal,
});
factory WatchSensorSummary.fromJson(Map<String, Object?> json) {
return WatchSensorSummary(
schemaVersion: _intFromJson(
json['schemaVersion'],
watchBridgeSchemaVersion,
),
sessionId: _stringFromJson(json['sessionId']),
sampleCount: _intFromJson(json['sampleCount'], 0),
minHeartRateBpm: _nullableIntFromJson(json['minHeartRateBpm']),
averageHeartRateBpm: _nullableDoubleFromJson(json['averageHeartRateBpm']),
maxHeartRateBpm: _nullableIntFromJson(json['maxHeartRateBpm']),
totalDistanceMeters: _nullableDoubleFromJson(json['totalDistanceMeters']),
totalCaloriesKcal: _nullableDoubleFromJson(json['totalCaloriesKcal']),
);
}
final int schemaVersion;
final String sessionId;
final int sampleCount;
final int? minHeartRateBpm;
final double? averageHeartRateBpm;
final int? maxHeartRateBpm;
final double? totalDistanceMeters;
final double? totalCaloriesKcal;
Map<String, Object?> toJson() {
return {
'schemaVersion': schemaVersion,
'sessionId': sessionId,
'sampleCount': sampleCount,
'minHeartRateBpm': minHeartRateBpm,
'averageHeartRateBpm': averageHeartRateBpm,
'maxHeartRateBpm': maxHeartRateBpm,
'totalDistanceMeters': totalDistanceMeters,
'totalCaloriesKcal': totalCaloriesKcal,
};
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
other is WatchSensorSummary &&
schemaVersion == other.schemaVersion &&
sessionId == other.sessionId &&
sampleCount == other.sampleCount &&
minHeartRateBpm == other.minHeartRateBpm &&
averageHeartRateBpm == other.averageHeartRateBpm &&
maxHeartRateBpm == other.maxHeartRateBpm &&
totalDistanceMeters == other.totalDistanceMeters &&
totalCaloriesKcal == other.totalCaloriesKcal;
}
@override
int get hashCode {
return Object.hash(
schemaVersion,
sessionId,
sampleCount,
minHeartRateBpm,
averageHeartRateBpm,
maxHeartRateBpm,
totalDistanceMeters,
totalCaloriesKcal,
);
}
}
final class WatchSensorSample {
const WatchSensorSample({
this.schemaVersion = watchBridgeSchemaVersion,
this.sampleId,
required this.sessionId,
int? capturedAtEpochMs,
int? recordedAtEpochMs,
this.programIndex,
this.exerciseIndex,
this.setIndex,
this.passageIndex,
this.stepIndex,
this.programSnapshotId,
this.exerciseSnapshotId,
this.stepSnapshotId,
this.heartRateBpm,
this.distanceMeters,
this.caloriesKcal,
}) : capturedAtEpochMs = capturedAtEpochMs ?? recordedAtEpochMs ?? 0;
factory WatchSensorSample.fromJson(Map<String, Object?> json) {
return WatchSensorSample(
schemaVersion: _intFromJson(
json['schemaVersion'],
watchBridgeSchemaVersion,
),
sampleId: _nullableStringFromJson(json['sampleId']),
sessionId: _stringFromJson(json['sessionId']),
capturedAtEpochMs: _intFromJson(
json['capturedAtEpochMs'],
_intFromJson(json['recordedAtEpochMs'], 0),
),
programIndex: _nullableIntFromJson(json['programIndex']),
exerciseIndex: _nullableIntFromJson(json['exerciseIndex']),
setIndex: _nullableIntFromJson(json['setIndex']),
passageIndex: _nullableIntFromJson(json['passageIndex']),
stepIndex: _nullableIntFromJson(json['stepIndex']),
programSnapshotId: _nullableStringFromJson(json['programSnapshotId']),
exerciseSnapshotId: _nullableStringFromJson(json['exerciseSnapshotId']),
stepSnapshotId: _nullableStringFromJson(json['stepSnapshotId']),
heartRateBpm: _nullableIntFromJson(json['heartRateBpm']),
distanceMeters: _nullableDoubleFromJson(json['distanceMeters']),
caloriesKcal: _nullableDoubleFromJson(json['caloriesKcal']),
);
}
final int schemaVersion;
final String? sampleId;
final String sessionId;
final int capturedAtEpochMs;
final int? programIndex;
final int? exerciseIndex;
final int? setIndex;
final int? passageIndex;
final int? stepIndex;
final String? programSnapshotId;
final String? exerciseSnapshotId;
final String? stepSnapshotId;
final int? heartRateBpm;
final double? distanceMeters;
final double? caloriesKcal;
int get recordedAtEpochMs => capturedAtEpochMs;
Map<String, Object?> toJson() {
return {
'schemaVersion': schemaVersion,
'sampleId': sampleId,
'sessionId': sessionId,
'capturedAtEpochMs': capturedAtEpochMs,
'recordedAtEpochMs': capturedAtEpochMs,
'programIndex': programIndex,
'exerciseIndex': exerciseIndex,
'setIndex': setIndex,
'passageIndex': passageIndex,
'stepIndex': stepIndex,
'programSnapshotId': programSnapshotId,
'exerciseSnapshotId': exerciseSnapshotId,
'stepSnapshotId': stepSnapshotId,
'heartRateBpm': heartRateBpm,
'distanceMeters': distanceMeters,
'caloriesKcal': caloriesKcal,
};
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
other is WatchSensorSample &&
schemaVersion == other.schemaVersion &&
sampleId == other.sampleId &&
sessionId == other.sessionId &&
capturedAtEpochMs == other.capturedAtEpochMs &&
programIndex == other.programIndex &&
exerciseIndex == other.exerciseIndex &&
setIndex == other.setIndex &&
passageIndex == other.passageIndex &&
stepIndex == other.stepIndex &&
programSnapshotId == other.programSnapshotId &&
exerciseSnapshotId == other.exerciseSnapshotId &&
stepSnapshotId == other.stepSnapshotId &&
heartRateBpm == other.heartRateBpm &&
distanceMeters == other.distanceMeters &&
caloriesKcal == other.caloriesKcal;
}
@override
int get hashCode {
return Object.hash(
schemaVersion,
sampleId,
sessionId,
capturedAtEpochMs,
programIndex,
exerciseIndex,
setIndex,
passageIndex,
stepIndex,
programSnapshotId,
exerciseSnapshotId,
stepSnapshotId,
heartRateBpm,
distanceMeters,
caloriesKcal,
);
}
}
typedef WatchTelemetrySummary = WatchSensorSummary;
typedef WatchTelemetrySample = WatchSensorSample;
final class WatchTimerProjection {
const WatchTimerProjection({
required this.kind,
@ -392,6 +669,17 @@ T _enumFromJson<T extends Enum>(Object? value, List<T> values, T fallback) {
return fallback;
}
T? _nullableEnumFromJson<T extends Enum>(Object? value, List<T> values) {
if (value is String) {
for (final enumValue in values) {
if (enumValue.name == value) {
return enumValue;
}
}
}
return null;
}
List<T> _enumListFromJson<T extends Enum>(Object? value, List<T> values) {
if (value is! List) {
return const [];
@ -438,6 +726,14 @@ int? _nullableIntFromJson(Object? value) {
return value is int ? value : null;
}
double? _nullableDoubleFromJson(Object? value) {
return switch (value) {
double() => value,
int() => value.toDouble(),
_ => null,
};
}
bool _boolFromJson(Object? value, bool fallback) {
return value is bool ? value : fallback;
}