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:
@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user