Skip to content

Commit f1bfa21

Browse files
committed
[green] Keep board identity visible when connected
Signed-off-by: Viwat Vchirawongkwin <viwat.v@chula.ac.th>
1 parent ac9efa5 commit f1bfa21

16 files changed

Lines changed: 135 additions & 23 deletions

‎app/lib/app/shell.dart‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -596,8 +596,14 @@ class _Toolbar extends ConsumerWidget {
596596
),
597597
const SizedBox(width: SignalSpacing.lg),
598598
],
599-
const ConnectionStatusPill(),
600-
const Spacer(),
599+
// The pill owns the remaining width. Its state word is fixed;
600+
// only its appended board identity may ellipsize (§7.6).
601+
const Expanded(
602+
child: Align(
603+
alignment: Alignment.centerLeft,
604+
child: ConnectionStatusPill(),
605+
),
606+
),
601607
// Connection action: Disconnect when a board is connected —
602608
// returns to the full-screen Connect flow (ADR-0011); otherwise
603609
// an inert Connect slot (the live scan/connect flow is the

‎app/lib/app/widgets/connection_status_pill.dart‎

Lines changed: 62 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@
66
/// [connStateProvider] through the [Connection] seam (CON-8 — no `lib/ble`
77
/// import) and renders `disconnected` / `connecting` / `ready` / `running` with
88
/// a distinct per-state glyph + localized label + the frozen `SignalStateColors`
9-
/// (glyph / border / tint). State is legible by shape + text, with color as
10-
/// reinforcement — never color alone, never text alone.
9+
/// (glyph / border / tint). Once connected it retains the display-only PBLE/1
10+
/// board ID and PyBLE firmware from [connectControllerProvider], because the
11+
/// pre-connection Connect surface is no longer mounted (ADR-0011). State is
12+
/// legible by shape + text, with color as reinforcement — never color alone,
13+
/// never text alone.
1114
///
1215
/// **Motion (deferred, harness-gated):** §7.6/§6 call for a `connecting`/
1316
/// `running` opacity breathe under the reduced-motion gate. A perpetual
@@ -24,6 +27,7 @@ import 'package:flutter/material.dart';
2427
import 'package:flutter/foundation.dart' show ValueListenable;
2528
import 'package:flutter_riverpod/flutter_riverpod.dart';
2629

30+
import 'package:pyble/connect/connect_controller.dart';
2731
import 'package:pyble/localization/localization.dart';
2832
import 'package:pyble/pble/pble.dart';
2933
import 'package:pyble/theme/theme.dart';
@@ -67,6 +71,11 @@ class ConnectionStatusPill extends ConsumerWidget {
6771
@override
6872
Widget build(BuildContext context, WidgetRef ref) {
6973
final ValueListenable<ConnState> listenable = ref.watch(connStateProvider);
74+
final DeviceInfo? deviceInfo = ref.watch(
75+
connectControllerProvider.select(
76+
(ConnectController controller) => controller.state.deviceInfo,
77+
),
78+
);
7079
final AppLocalizations l10n = AppLocalizations.of(context);
7180
return ValueListenableBuilder<ConnState>(
7281
valueListenable: listenable,
@@ -75,6 +84,20 @@ class ConnectionStatusPill extends ConsumerWidget {
7584
final ColorScheme scheme = theme.colorScheme;
7685
final Color stateColor = SignalStateColors.of(context).colorFor(state);
7786
final bool isDark = scheme.brightness == Brightness.dark;
87+
final bool connected =
88+
state == ConnState.ready || state == ConnState.running;
89+
final DeviceInfo? visibleInfo = connected ? deviceInfo : null;
90+
final String stateWord = _wordFor(l10n, state);
91+
final String? deviceId = visibleInfo == null
92+
? null
93+
: visibleInfo.deviceId.isEmpty
94+
? l10n.connectDeviceNotReported
95+
: visibleInfo.deviceId;
96+
final String? agentVersion = visibleInfo == null
97+
? null
98+
: visibleInfo.agentVersion.isEmpty
99+
? l10n.connectDeviceNotReported
100+
: visibleInfo.agentVersion;
78101

79102
// Tint fill = state color at 16 % (light) / 24 % (dark), composited to an
80103
// OPAQUE color over the pill's actual host surface (level 4) so contrast
@@ -101,31 +124,52 @@ class ConnectionStatusPill extends ConsumerWidget {
101124
// Glyph = state color at full strength (≥ 3:1 graphic).
102125
Icon(_glyphFor(state), size: 18, color: stateColor),
103126
const SizedBox(width: SignalSpacing.sm),
104-
// Label = onSurface (AA-safe at any hue, §1.3); never ellipsizes
105-
// to nothing — the glyph always remains.
106-
Flexible(
107-
child: Text(
108-
_wordFor(l10n, state),
109-
maxLines: 1,
110-
overflow: TextOverflow.ellipsis,
111-
// Cosmetic chrome text clamps its scaler (§5) so the top
112-
// toolbar stays bounded while body/content text scales fully.
113-
textScaler: MediaQuery.textScalerOf(
114-
context,
115-
).clamp(maxScaleFactor: 1.3),
116-
style: theme.textTheme.labelMedium?.copyWith(
117-
color: scheme.onSurface,
118-
),
127+
// The full localized state word is the minimum signal and never
128+
// ellipsizes. Only the appended identity summary may truncate.
129+
Text(
130+
stateWord,
131+
maxLines: 1,
132+
textScaler: MediaQuery.textScalerOf(
133+
context,
134+
).clamp(maxScaleFactor: 1.3),
135+
style: theme.textTheme.labelMedium?.copyWith(
136+
color: scheme.onSurface,
119137
),
120138
),
139+
if (deviceId != null && agentVersion != null) ...<Widget>[
140+
const SizedBox(width: SignalSpacing.xs),
141+
Flexible(
142+
child: Text(
143+
l10n.connStatusBoardInfoSummary(deviceId, agentVersion),
144+
maxLines: 1,
145+
overflow: TextOverflow.ellipsis,
146+
textScaler: MediaQuery.textScalerOf(
147+
context,
148+
).clamp(maxScaleFactor: 1.3),
149+
style: theme.textTheme.labelMedium?.copyWith(
150+
color: scheme.onSurface,
151+
fontFeatures: const <FontFeature>[
152+
FontFeature.tabularFigures(),
153+
],
154+
),
155+
),
156+
),
157+
],
121158
],
122159
),
123160
),
124161
);
125162

126163
return Semantics(
127164
container: true,
128-
label: l10n.connStatusSemanticLabel(_wordFor(l10n, state)),
165+
excludeSemantics: true,
166+
label: deviceId == null || agentVersion == null
167+
? l10n.connStatusSemanticLabel(stateWord)
168+
: l10n.connStatusBoardInfoSemanticLabel(
169+
stateWord,
170+
deviceId,
171+
agentVersion,
172+
),
129173
child: ConstrainedBox(
130174
// ≥ 48 dp tall hit area (FR-UI-6); the visual pill is centered in it.
131175
constraints: const BoxConstraints(minHeight: 48),

‎app/lib/localization/arb/app_en.arb‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,39 @@
215215
}
216216
},
217217

218+
"connStatusBoardInfoSummary": "· {deviceId} · {agentVersion}",
219+
"@connStatusBoardInfoSummary": {
220+
"description": "Compact identity suffix appended after the full connection-state word in the always-visible status pill. {deviceId} is the PBLE/1 display-only board ID and {agentVersion} is the PyBLE agent firmware version; both are rendered verbatim.",
221+
"placeholders": {
222+
"deviceId": {
223+
"type": "String",
224+
"example": "5646"
225+
},
226+
"agentVersion": {
227+
"type": "String",
228+
"example": "0.6.0"
229+
}
230+
}
231+
},
232+
"connStatusBoardInfoSemanticLabel": "Connection status: {status}. Board ID: {deviceId}. PyBLE firmware: {agentVersion}.",
233+
"@connStatusBoardInfoSemanticLabel": {
234+
"description": "Screen-reader label for the connected status pill, preserving the complete state and the two labeled DeviceInfo values even when the visual summary is ellipsized.",
235+
"placeholders": {
236+
"status": {
237+
"type": "String",
238+
"example": "Ready"
239+
},
240+
"deviceId": {
241+
"type": "String",
242+
"example": "5646"
243+
},
244+
"agentVersion": {
245+
"type": "String",
246+
"example": "0.6.0"
247+
}
248+
}
249+
},
250+
218251
"connectEmptyTitle": "No board connected",
219252
"@connectEmptyTitle": {
220253
"description": "Title of the Connect surface empty state, shown when no board is connected (first-run / disconnected)."

‎app/lib/localization/gen/app_localizations.dart‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,22 @@ abstract class AppLocalizations {
361361
/// **'Connection status: {status}'**
362362
String connStatusSemanticLabel(String status);
363363

364+
/// Compact identity suffix appended after the full connection-state word in the always-visible status pill. {deviceId} is the PBLE/1 display-only board ID and {agentVersion} is the PyBLE agent firmware version; both are rendered verbatim.
365+
///
366+
/// In en, this message translates to:
367+
/// **'· {deviceId} · {agentVersion}'**
368+
String connStatusBoardInfoSummary(String deviceId, String agentVersion);
369+
370+
/// Screen-reader label for the connected status pill, preserving the complete state and the two labeled DeviceInfo values even when the visual summary is ellipsized.
371+
///
372+
/// In en, this message translates to:
373+
/// **'Connection status: {status}. Board ID: {deviceId}. PyBLE firmware: {agentVersion}.'**
374+
String connStatusBoardInfoSemanticLabel(
375+
String status,
376+
String deviceId,
377+
String agentVersion,
378+
);
379+
364380
/// Title of the Connect surface empty state, shown when no board is connected (first-run / disconnected).
365381
///
366382
/// In en, this message translates to:

‎app/lib/localization/gen/app_localizations_en.dart‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,20 @@ class AppLocalizationsEn extends AppLocalizations {
161161
return 'Connection status: $status';
162162
}
163163

164+
@override
165+
String connStatusBoardInfoSummary(String deviceId, String agentVersion) {
166+
return '· $deviceId · $agentVersion';
167+
}
168+
169+
@override
170+
String connStatusBoardInfoSemanticLabel(
171+
String status,
172+
String deviceId,
173+
String agentVersion,
174+
) {
175+
return 'Connection status: $status. Board ID: $deviceId. PyBLE firmware: $agentVersion.';
176+
}
177+
164178
@override
165179
String get connectEmptyTitle => 'No board connected';
166180

71 Bytes
Loading
77 Bytes
Loading
60 Bytes
Loading
71 Bytes
Loading
72 Bytes
Loading

0 commit comments

Comments
 (0)