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:
@ -11,17 +11,31 @@ final class WatchSessionUiState {
|
||||
required this.projection,
|
||||
this.commandPending = false,
|
||||
this.waitingForPhone = false,
|
||||
this.timerTogglePending = false,
|
||||
this.connectionLost = false,
|
||||
this.staleProjection = false,
|
||||
this.scoreCommandPending = false,
|
||||
this.scoreWaitingForPhone = false,
|
||||
this.optimisticManualScoreValue,
|
||||
this.commandFailureMessage,
|
||||
this.commandFailureSerial = 0,
|
||||
this.lastAck,
|
||||
this.sensorSample,
|
||||
});
|
||||
|
||||
final WatchSessionProjection projection;
|
||||
final bool commandPending;
|
||||
final bool waitingForPhone;
|
||||
final bool timerTogglePending;
|
||||
final bool connectionLost;
|
||||
final bool staleProjection;
|
||||
final bool scoreCommandPending;
|
||||
final bool scoreWaitingForPhone;
|
||||
final double? optimisticManualScoreValue;
|
||||
final String? commandFailureMessage;
|
||||
final int commandFailureSerial;
|
||||
final WatchCommandAckEvent? lastAck;
|
||||
final WatchSensorSample? sensorSample;
|
||||
|
||||
bool get actionsEnabled => !commandPending && !connectionLost;
|
||||
|
||||
@ -29,19 +43,50 @@ final class WatchSessionUiState {
|
||||
WatchSessionProjection? projection,
|
||||
bool? commandPending,
|
||||
bool? waitingForPhone,
|
||||
bool? timerTogglePending,
|
||||
bool? connectionLost,
|
||||
bool? staleProjection,
|
||||
bool? scoreCommandPending,
|
||||
bool? scoreWaitingForPhone,
|
||||
double? optimisticManualScoreValue,
|
||||
bool clearOptimisticManualScoreValue = false,
|
||||
String? commandFailureMessage,
|
||||
bool clearCommandFailureMessage = false,
|
||||
int? commandFailureSerial,
|
||||
WatchCommandAckEvent? lastAck,
|
||||
WatchSensorSample? sensorSample,
|
||||
bool clearSensorSample = false,
|
||||
}) {
|
||||
return WatchSessionUiState(
|
||||
projection: projection ?? this.projection,
|
||||
commandPending: commandPending ?? this.commandPending,
|
||||
waitingForPhone: waitingForPhone ?? this.waitingForPhone,
|
||||
timerTogglePending: timerTogglePending ?? this.timerTogglePending,
|
||||
connectionLost: connectionLost ?? this.connectionLost,
|
||||
staleProjection: staleProjection ?? this.staleProjection,
|
||||
scoreCommandPending: scoreCommandPending ?? this.scoreCommandPending,
|
||||
scoreWaitingForPhone: scoreWaitingForPhone ?? this.scoreWaitingForPhone,
|
||||
optimisticManualScoreValue: clearOptimisticManualScoreValue
|
||||
? null
|
||||
: optimisticManualScoreValue ?? this.optimisticManualScoreValue,
|
||||
commandFailureMessage: clearCommandFailureMessage
|
||||
? null
|
||||
: commandFailureMessage ?? this.commandFailureMessage,
|
||||
commandFailureSerial: commandFailureSerial ?? this.commandFailureSerial,
|
||||
lastAck: lastAck ?? this.lastAck,
|
||||
sensorSample: clearSensorSample
|
||||
? null
|
||||
: sensorSample ?? this.sensorSample,
|
||||
);
|
||||
}
|
||||
|
||||
bool get hasLiveSensors {
|
||||
final sample = sensorSample;
|
||||
return sample != null &&
|
||||
(sample.heartRateBpm != null ||
|
||||
sample.distanceMeters != null ||
|
||||
sample.caloriesKcal != null);
|
||||
}
|
||||
}
|
||||
|
||||
final class WatchSessionViewModel extends ValueNotifier<WatchSessionUiState> {
|
||||
@ -59,6 +104,7 @@ final class WatchSessionViewModel extends ValueNotifier<WatchSessionUiState> {
|
||||
_connectionLostThreshold = connectionLostThreshold,
|
||||
super(WatchSessionUiState(projection: _initialProjection())) {
|
||||
_subscriptions.add(_nativeClient.projections.listen(_handleProjection));
|
||||
_subscriptions.add(_nativeClient.sensorSamples.listen(_handleSensorSample));
|
||||
_subscriptions.add(_nativeClient.acks.listen(_handleAck));
|
||||
_subscriptions.add(
|
||||
_nativeClient.connectionEvents.listen(_handleConnectionEvent),
|
||||
@ -79,10 +125,16 @@ final class WatchSessionViewModel extends ValueNotifier<WatchSessionUiState> {
|
||||
|
||||
Timer? _waitingTimer;
|
||||
Timer? _commandTimeoutTimer;
|
||||
Timer? _scoreWaitingTimer;
|
||||
Timer? _scoreCommandTimeoutTimer;
|
||||
Timer? _freshnessTimer;
|
||||
Timer? _commandFailureClearTimer;
|
||||
WatchCommandEnvelope? _pendingCommand;
|
||||
final _pendingScoreCommandIds = <String>{};
|
||||
double? _optimisticManualScoreValue;
|
||||
DateTime? _lastProjectionReceivedAt;
|
||||
var _commandCounter = 0;
|
||||
var _commandFailureSerial = 0;
|
||||
|
||||
Future<void> refresh() async {
|
||||
value = value.copyWith(connectionLost: false);
|
||||
@ -119,18 +171,30 @@ final class WatchSessionViewModel extends ValueNotifier<WatchSessionUiState> {
|
||||
WatchSecondaryAction.skipCurrentStep => WatchCommandType.skipCurrentStep,
|
||||
WatchSecondaryAction.skipCurrentPassage =>
|
||||
WatchCommandType.skipCurrentPassage,
|
||||
WatchSecondaryAction.finishCurrentSet => WatchCommandType.finishCurrentSet,
|
||||
WatchSecondaryAction.finishCurrentSet =>
|
||||
WatchCommandType.finishCurrentSet,
|
||||
WatchSecondaryAction.skipCurrentSet => WatchCommandType.skipCurrentSet,
|
||||
WatchSecondaryAction.skipCurrentRest => WatchCommandType.skipCurrentRest,
|
||||
};
|
||||
return _sendCommand(command);
|
||||
}
|
||||
|
||||
Future<void> incrementScore() {
|
||||
return _sendScoreCommand(WatchCommandType.incrementScore, 1);
|
||||
}
|
||||
|
||||
Future<void> decrementScore() {
|
||||
return _sendScoreCommand(WatchCommandType.decrementScore, -1);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_waitingTimer?.cancel();
|
||||
_commandTimeoutTimer?.cancel();
|
||||
_scoreWaitingTimer?.cancel();
|
||||
_scoreCommandTimeoutTimer?.cancel();
|
||||
_freshnessTimer?.cancel();
|
||||
_commandFailureClearTimer?.cancel();
|
||||
for (final subscription in _subscriptions) {
|
||||
unawaited(subscription.cancel());
|
||||
}
|
||||
@ -138,7 +202,9 @@ final class WatchSessionViewModel extends ValueNotifier<WatchSessionUiState> {
|
||||
}
|
||||
|
||||
Future<void> _sendCommand(WatchCommandType type) async {
|
||||
if (!value.actionsEnabled || value.projection.deviceSessionId.isEmpty) {
|
||||
if (!value.actionsEnabled ||
|
||||
(_requiresActiveSession(type) &&
|
||||
value.projection.deviceSessionId.isEmpty)) {
|
||||
return;
|
||||
}
|
||||
final nowMs = DateTime.now().toUtc().millisecondsSinceEpoch;
|
||||
@ -153,6 +219,7 @@ final class WatchSessionViewModel extends ValueNotifier<WatchSessionUiState> {
|
||||
value = value.copyWith(
|
||||
commandPending: true,
|
||||
waitingForPhone: false,
|
||||
timerTogglePending: _isTimerToggleCommand(type),
|
||||
connectionLost: false,
|
||||
);
|
||||
_waitingTimer?.cancel();
|
||||
@ -165,6 +232,7 @@ final class WatchSessionViewModel extends ValueNotifier<WatchSessionUiState> {
|
||||
value = value.copyWith(
|
||||
commandPending: false,
|
||||
waitingForPhone: false,
|
||||
timerTogglePending: false,
|
||||
connectionLost: true,
|
||||
);
|
||||
unawaited(HapticFeedback.heavyImpact());
|
||||
@ -177,25 +245,114 @@ final class WatchSessionViewModel extends ValueNotifier<WatchSessionUiState> {
|
||||
value = value.copyWith(
|
||||
commandPending: false,
|
||||
waitingForPhone: false,
|
||||
timerTogglePending: false,
|
||||
connectionLost: true,
|
||||
);
|
||||
unawaited(HapticFeedback.heavyImpact());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _sendScoreCommand(WatchCommandType type, int delta) async {
|
||||
final projection = value.projection;
|
||||
if (value.connectionLost ||
|
||||
!projection.phoneReachable ||
|
||||
!projection.hasManualScore ||
|
||||
projection.deviceSessionId.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final current =
|
||||
_optimisticManualScoreValue ?? projection.currentManualScoreValue ?? 0;
|
||||
final next = (current + delta).clamp(0, double.infinity).toDouble();
|
||||
if (next == current) {
|
||||
return;
|
||||
}
|
||||
final nowMs = DateTime.now().toUtc().millisecondsSinceEpoch;
|
||||
final command = WatchCommandEnvelope(
|
||||
commandId: 'watch-$nowMs-${_commandCounter++}',
|
||||
type: type,
|
||||
sessionId: projection.deviceSessionId,
|
||||
expectedRevision: projection.revision,
|
||||
sentAtEpochMs: nowMs,
|
||||
);
|
||||
_pendingScoreCommandIds.add(command.commandId);
|
||||
_optimisticManualScoreValue = next;
|
||||
value = value.copyWith(
|
||||
scoreCommandPending: true,
|
||||
scoreWaitingForPhone: false,
|
||||
optimisticManualScoreValue: next,
|
||||
connectionLost: false,
|
||||
);
|
||||
_scoreWaitingTimer?.cancel();
|
||||
_scoreCommandTimeoutTimer?.cancel();
|
||||
_scoreWaitingTimer = Timer(_waitingThreshold, () {
|
||||
value = value.copyWith(scoreWaitingForPhone: true);
|
||||
});
|
||||
_scoreCommandTimeoutTimer = Timer(_commandTimeout, () {
|
||||
_clearScorePending(recalibrate: true);
|
||||
unawaited(HapticFeedback.heavyImpact());
|
||||
unawaited(_nativeClient.requestResync());
|
||||
});
|
||||
try {
|
||||
await _nativeClient.sendCommand(command);
|
||||
unawaited(HapticFeedback.selectionClick());
|
||||
} on PlatformException {
|
||||
_pendingScoreCommandIds.remove(command.commandId);
|
||||
_clearScorePending(recalibrate: true);
|
||||
value = value.copyWith(connectionLost: true);
|
||||
unawaited(HapticFeedback.heavyImpact());
|
||||
}
|
||||
}
|
||||
|
||||
void _handleProjection(WatchSessionProjection projection) {
|
||||
final previousProjection = value.projection;
|
||||
_lastProjectionReceivedAt = DateTime.now();
|
||||
_pendingCommand = null;
|
||||
_clearCommandTimers();
|
||||
_syncScorePendingFromProjection(projection);
|
||||
value = WatchSessionUiState(
|
||||
projection: projection,
|
||||
timerTogglePending: false,
|
||||
scoreCommandPending: _pendingScoreCommandIds.isNotEmpty,
|
||||
scoreWaitingForPhone:
|
||||
_pendingScoreCommandIds.isNotEmpty && value.scoreWaitingForPhone,
|
||||
optimisticManualScoreValue: _optimisticManualScoreValue,
|
||||
commandFailureMessage: value.commandFailureMessage,
|
||||
commandFailureSerial: value.commandFailureSerial,
|
||||
lastAck: value.lastAck,
|
||||
sensorSample: projection.deviceSessionId.isEmpty
|
||||
? null
|
||||
: value.sensorSample?.sessionId == projection.deviceSessionId
|
||||
? value.sensorSample
|
||||
: null,
|
||||
);
|
||||
_triggerProjectionHaptic(previousProjection, projection);
|
||||
}
|
||||
|
||||
void _handleSensorSample(WatchSensorSample sample) {
|
||||
final sessionId = sample.sessionId.trim();
|
||||
if (sessionId.isEmpty || sessionId != value.projection.deviceSessionId) {
|
||||
return;
|
||||
}
|
||||
final hasMetric =
|
||||
sample.heartRateBpm != null ||
|
||||
sample.distanceMeters != null ||
|
||||
sample.caloriesKcal != null;
|
||||
if (!hasMetric) {
|
||||
return;
|
||||
}
|
||||
value = value.copyWith(sensorSample: sample);
|
||||
}
|
||||
|
||||
void _handleAck(WatchCommandAckEvent ack) {
|
||||
if (_pendingScoreCommandIds.remove(ack.commandId)) {
|
||||
value = value.copyWith(lastAck: ack);
|
||||
unawaited(HapticFeedback.lightImpact());
|
||||
if (_isRejected(ack.status)) {
|
||||
_clearScorePending(recalibrate: true);
|
||||
unawaited(_nativeClient.requestResync());
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (_pendingCommand?.commandId != ack.commandId) {
|
||||
value = value.copyWith(lastAck: ack);
|
||||
return;
|
||||
@ -208,9 +365,10 @@ final class WatchSessionViewModel extends ValueNotifier<WatchSessionUiState> {
|
||||
);
|
||||
unawaited(HapticFeedback.lightImpact());
|
||||
if (_isRejected(ack.status)) {
|
||||
final failedType = _pendingCommand?.type;
|
||||
_pendingCommand = null;
|
||||
_clearCommandTimers();
|
||||
value = value.copyWith(commandPending: false);
|
||||
_publishCommandFailure(_commandFailureMessageFor(failedType));
|
||||
unawaited(_nativeClient.requestResync());
|
||||
}
|
||||
}
|
||||
@ -242,12 +400,61 @@ final class WatchSessionViewModel extends ValueNotifier<WatchSessionUiState> {
|
||||
_commandTimeoutTimer = null;
|
||||
}
|
||||
|
||||
void _publishCommandFailure(String message) {
|
||||
_commandFailureClearTimer?.cancel();
|
||||
value = value.copyWith(
|
||||
commandPending: false,
|
||||
timerTogglePending: false,
|
||||
commandFailureMessage: message,
|
||||
commandFailureSerial: ++_commandFailureSerial,
|
||||
);
|
||||
_commandFailureClearTimer = Timer(const Duration(seconds: 2), () {
|
||||
value = value.copyWith(clearCommandFailureMessage: true);
|
||||
});
|
||||
}
|
||||
|
||||
void _clearScorePending({required bool recalibrate}) {
|
||||
_pendingScoreCommandIds.clear();
|
||||
_scoreWaitingTimer?.cancel();
|
||||
_scoreWaitingTimer = null;
|
||||
_scoreCommandTimeoutTimer?.cancel();
|
||||
_scoreCommandTimeoutTimer = null;
|
||||
_optimisticManualScoreValue = null;
|
||||
value = value.copyWith(
|
||||
scoreCommandPending: false,
|
||||
scoreWaitingForPhone: false,
|
||||
clearOptimisticManualScoreValue: recalibrate,
|
||||
);
|
||||
}
|
||||
|
||||
void _syncScorePendingFromProjection(WatchSessionProjection projection) {
|
||||
if (_pendingScoreCommandIds.isEmpty) {
|
||||
_optimisticManualScoreValue = null;
|
||||
_scoreWaitingTimer?.cancel();
|
||||
_scoreWaitingTimer = null;
|
||||
_scoreCommandTimeoutTimer?.cancel();
|
||||
_scoreCommandTimeoutTimer = null;
|
||||
return;
|
||||
}
|
||||
final optimistic = _optimisticManualScoreValue;
|
||||
final confirmed = projection.currentManualScoreValue;
|
||||
if (optimistic != null && confirmed == optimistic) {
|
||||
_pendingScoreCommandIds.clear();
|
||||
_optimisticManualScoreValue = null;
|
||||
_scoreWaitingTimer?.cancel();
|
||||
_scoreWaitingTimer = null;
|
||||
_scoreCommandTimeoutTimer?.cancel();
|
||||
_scoreCommandTimeoutTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
void _triggerProjectionHaptic(
|
||||
WatchSessionProjection previous,
|
||||
WatchSessionProjection current,
|
||||
) {
|
||||
final phaseChanged = previous.phase != current.phase;
|
||||
final enteredReadyTimer = current.phase == WatchSessionPhase.nextTimerReady &&
|
||||
final enteredReadyTimer =
|
||||
current.phase == WatchSessionPhase.nextTimerReady &&
|
||||
previous.phase != WatchSessionPhase.nextTimerReady;
|
||||
final enteredRestEnd =
|
||||
previous.phase == WatchSessionPhase.restRunning &&
|
||||
@ -255,9 +462,11 @@ final class WatchSessionViewModel extends ValueNotifier<WatchSessionUiState> {
|
||||
current.phase != WatchSessionPhase.restPaused;
|
||||
if (phaseChanged && (enteredReadyTimer || enteredRestEnd)) {
|
||||
unawaited(HapticFeedback.mediumImpact());
|
||||
unawaited(Future<void>.delayed(const Duration(milliseconds: 120), () {
|
||||
return HapticFeedback.mediumImpact();
|
||||
}));
|
||||
unawaited(
|
||||
Future<void>.delayed(const Duration(milliseconds: 120), () {
|
||||
return HapticFeedback.mediumImpact();
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -269,6 +478,28 @@ bool _isRejected(WatchCommandAck ack) {
|
||||
};
|
||||
}
|
||||
|
||||
String _commandFailureMessageFor(WatchCommandType? type) {
|
||||
return switch (type) {
|
||||
WatchCommandType.finishCurrentSet ||
|
||||
WatchCommandType.skipCurrentSet => 'Série non modifiée',
|
||||
_ => 'Commande non appliquée',
|
||||
};
|
||||
}
|
||||
|
||||
bool _isTimerToggleCommand(WatchCommandType type) {
|
||||
return switch (type) {
|
||||
WatchCommandType.startCurrentExercise ||
|
||||
WatchCommandType.startPreparedTimedStep ||
|
||||
WatchCommandType.pauseSession ||
|
||||
WatchCommandType.resumeSession => true,
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
bool _requiresActiveSession(WatchCommandType type) {
|
||||
return true;
|
||||
}
|
||||
|
||||
WatchSessionProjection _initialProjection() {
|
||||
return WatchSessionProjection(
|
||||
deviceSessionId: '',
|
||||
|
||||
@ -30,6 +30,8 @@ final class WatchCommandAckEvent {
|
||||
abstract interface class NativeWatchBridgeClient {
|
||||
Stream<WatchSessionProjection> get projections;
|
||||
|
||||
Stream<WatchSensorSample> get sensorSamples;
|
||||
|
||||
Stream<WatchCommandAckEvent> get acks;
|
||||
|
||||
Stream<WatchBridgeConnectionEvent> get connectionEvents;
|
||||
@ -45,25 +47,28 @@ final class MethodChannelNativeWatchBridgeClient
|
||||
implements NativeWatchBridgeClient {
|
||||
const MethodChannelNativeWatchBridgeClient({
|
||||
MethodChannel methodChannel = const MethodChannel(_methodChannelName),
|
||||
EventChannel projectionChannel = const EventChannel(
|
||||
_projectionChannelName,
|
||||
EventChannel projectionChannel = const EventChannel(_projectionChannelName),
|
||||
EventChannel sensorSampleChannel = const EventChannel(
|
||||
_sensorSampleChannelName,
|
||||
),
|
||||
EventChannel ackChannel = const EventChannel(_ackChannelName),
|
||||
EventChannel connectionChannel = const EventChannel(
|
||||
_connectionChannelName,
|
||||
),
|
||||
EventChannel connectionChannel = const EventChannel(_connectionChannelName),
|
||||
}) : _methodChannel = methodChannel,
|
||||
_projectionChannel = projectionChannel,
|
||||
_sensorSampleChannel = sensorSampleChannel,
|
||||
_ackChannel = ackChannel,
|
||||
_connectionChannel = connectionChannel;
|
||||
|
||||
static const _methodChannelName = 'gametime.watch_bridge/methods';
|
||||
static const _projectionChannelName = 'gametime.watch_bridge/projections';
|
||||
static const _sensorSampleChannelName =
|
||||
'gametime.watch_bridge/sensor_samples';
|
||||
static const _ackChannelName = 'gametime.watch_bridge/acks';
|
||||
static const _connectionChannelName = 'gametime.watch_bridge/connection';
|
||||
|
||||
final MethodChannel _methodChannel;
|
||||
final EventChannel _projectionChannel;
|
||||
final EventChannel _sensorSampleChannel;
|
||||
final EventChannel _ackChannel;
|
||||
final EventChannel _connectionChannel;
|
||||
|
||||
@ -77,6 +82,16 @@ final class MethodChannelNativeWatchBridgeClient
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<WatchSensorSample> get sensorSamples {
|
||||
return _sensorSampleChannel
|
||||
.receiveBroadcastStream()
|
||||
.where((event) => event is Map)
|
||||
.map((event) {
|
||||
return WatchSensorSample.fromJson(_stringObjectMap(event));
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<WatchCommandAckEvent> get acks {
|
||||
return _ackChannel
|
||||
@ -144,7 +159,11 @@ String? _nullableStringFromJson(Object? value) {
|
||||
}
|
||||
|
||||
int? _nullableIntFromJson(Object? value) {
|
||||
return value is int ? value : value is num ? value.toInt() : null;
|
||||
return value is int
|
||||
? value
|
||||
: value is num
|
||||
? value.toInt()
|
||||
: null;
|
||||
}
|
||||
|
||||
T _enumFromJson<T extends Enum>(Object? value, List<T> values, T fallback) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -9,23 +9,41 @@ ThemeData watchTheme() {
|
||||
|
||||
final textTheme = Typography.whiteMountainView.copyWith(
|
||||
labelSmall: const TextStyle(
|
||||
fontFamily: 'Archivo',
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: muted,
|
||||
letterSpacing: 0,
|
||||
),
|
||||
bodySmall: const TextStyle(
|
||||
fontFamily: 'Archivo',
|
||||
fontSize: 11,
|
||||
color: muted,
|
||||
height: 1.15,
|
||||
letterSpacing: 0,
|
||||
),
|
||||
bodyMedium: const TextStyle(
|
||||
fontFamily: 'Archivo',
|
||||
fontSize: 13,
|
||||
color: text,
|
||||
height: 1.15,
|
||||
letterSpacing: 0,
|
||||
),
|
||||
bodySmall: const TextStyle(fontSize: 11, color: muted, height: 1.15),
|
||||
bodyMedium: const TextStyle(fontSize: 13, color: text, height: 1.15),
|
||||
titleSmall: const TextStyle(
|
||||
fontFamily: 'Archivo',
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: text,
|
||||
height: 1.05,
|
||||
letterSpacing: 0,
|
||||
),
|
||||
displayLarge: const TextStyle(
|
||||
fontSize: 44,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: text,
|
||||
fontFamily: 'Anton',
|
||||
fontSize: 42,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Color(0xFFC9A24A),
|
||||
height: 0.95,
|
||||
letterSpacing: 0,
|
||||
fontFeatures: [FontFeature.tabularFigures()],
|
||||
),
|
||||
);
|
||||
@ -50,8 +68,10 @@ ThemeData watchTheme() {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
textStyle: textTheme.labelLarge?.copyWith(
|
||||
fontFamily: 'Archivo',
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w800,
|
||||
letterSpacing: 0,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -62,8 +82,10 @@ ThemeData watchTheme() {
|
||||
side: const BorderSide(color: Color(0xFF303748)),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
textStyle: textTheme.labelLarge?.copyWith(
|
||||
fontFamily: 'Archivo',
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w800,
|
||||
letterSpacing: 0,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user