fix(ui,voice): consolidate to single Voice settings entry point

The capability badge had its own 'Configure' TextButton that opened
the bind-key flow, while the Voice Bar's settings gear also reached
bind-key through the settings dialog. Two paths, same destination —
confusing and pointless duplication that the user flagged.

Resolution: the gear is the only configuration entry point. The
capability badge becomes information-only — it still shows the
detected PTT level + backend and (for L0Focused) the info-icon
explanation sheet, but no Configure button. The badge no longer
takes  or  props. The Voice Bar drops
the  callback added in 6a41a0b.

Also removed the dead  legacy widget class (lines
1001-1181) — it had no callers since the VoiceBar refactor in
ba444d9 but was still cluttering the file and even held a stale
reference to PttCapabilityBadge's old constructor signature.

The bound-key string is no longer duplicated either: the Voice Bar's
PTT-only secondary line ('PTT: Space   ·   Release tail: 200ms')
remains the only place that shows the bound key, since it's also
the only PTT-mode-gated surface.

flutter analyze: clean (6 pre-existing Radio.groupValue infos).
This commit is contained in:
EdisonJwa
2026-05-15 23:52:23 +08:00
parent 8cd919cffc
commit 64878e3a7d
2 changed files with 35 additions and 250 deletions
+27 -241
View File
@@ -855,7 +855,6 @@ class _BetaHomeState extends State<_BetaHome> {
pttBoundKeyLabel: _pttBoundKeyLabel,
onToggleMute: _onToggleHardMute,
onConfigure: _onOpenVoiceSettings,
onBindKey: () => _onConfigurePtt(context),
onLeave: _onLeaveVoice,
),
const SizedBox(height: 12),
@@ -999,187 +998,6 @@ class _BookmarkList extends StatelessWidget {
}
}
class _AudioControls extends StatefulWidget {
const _AudioControls({
required this.stats,
required this.inputMuted,
required this.outputMuted,
required this.outputGain,
required this.pttLevel,
required this.pttBackendId,
required this.pttBoundInputClass,
required this.pttBoundKeyLabel,
required this.onPttDown,
required this.onPttUp,
required this.onToggleInputMute,
required this.onToggleOutputMute,
required this.onGainChanged,
required this.onConfigurePtt,
});
final rust.BridgeAudioStats? stats;
final bool inputMuted;
final bool outputMuted;
final double outputGain;
final String pttLevel;
final String pttBackendId;
final String pttBoundInputClass;
final String pttBoundKeyLabel;
final VoidCallback onPttDown;
final VoidCallback onPttUp;
final VoidCallback onToggleInputMute;
final VoidCallback onToggleOutputMute;
final ValueChanged<double> onGainChanged;
final VoidCallback onConfigurePtt;
@override
State<_AudioControls> createState() => _AudioControlsState();
}
class _AudioControlsState extends State<_AudioControls> {
bool _pressed = false;
@override
Widget build(BuildContext context) {
final l10n = AppL10n.of(context);
final theme = Theme.of(context);
final stats = widget.stats;
final statsText = stats == null
? ''
: l10n.audioStatsLine(
stats.framesSent,
stats.framesReceived,
stats.pttActive ? 'on' : 'off',
);
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Capability badge (gen2 v0.9.3 / SDD-091). Renders the
// active PTT level + backend so the user understands when
// Global PTT has fallen back to Focused PTT, and surfaces
// a per-platform explanation sheet when the resolved
// capability is `L0Focused`.
PttCapabilityBadge(
level: widget.pttLevel,
backendId: widget.pttBackendId,
boundInputClass: widget.pttBoundInputClass,
boundKeyLabel: widget.pttBoundKeyLabel,
onConfigure: widget.onConfigurePtt,
),
// Accessibility (SysRS-262 + SysRS-282 + SysRS-263):
// wrap the custom Listener-based PTT control in a
// `Semantics` node so screen readers announce its role
// ("button") and its current state ("Transmitting" /
// "Hold to talk"). The icon + text inside already
// communicate the state without relying on colour
// alone.
Semantics(
button: true,
enabled: true,
toggled: _pressed,
label: _pressed ? l10n.pttTransmitting : l10n.pttHoldToTalk,
hint: l10n.pttHoldToTalkSemanticsHint,
excludeSemantics: true,
child: Listener(
onPointerDown: (_) {
setState(() => _pressed = true);
widget.onPttDown();
},
onPointerUp: (_) {
setState(() => _pressed = false);
widget.onPttUp();
},
onPointerCancel: (_) {
setState(() => _pressed = false);
widget.onPttUp();
},
child: Container(
padding: const EdgeInsets.symmetric(vertical: 16),
decoration: BoxDecoration(
color: _pressed
? theme.colorScheme.primary
: theme.colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
_pressed ? Icons.mic : Icons.mic_off,
color: _pressed
? theme.colorScheme.onPrimary
: theme.colorScheme.onPrimaryContainer,
),
const SizedBox(width: 8),
Text(
_pressed ? l10n.pttTransmitting : l10n.pttHoldToTalk,
style: TextStyle(
color: _pressed
? theme.colorScheme.onPrimary
: theme.colorScheme.onPrimaryContainer,
fontWeight: FontWeight.w600,
),
),
],
),
),
),
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: FilterChip(
avatar: Icon(
widget.inputMuted ? Icons.mic_off : Icons.mic,
size: 18,
),
label: Text(
widget.inputMuted ? l10n.inputUnmuteAction : l10n.inputMuteAction,
),
selected: widget.inputMuted,
onSelected: (_) => widget.onToggleInputMute(),
),
),
const SizedBox(width: 8),
Expanded(
child: FilterChip(
avatar: Icon(
widget.outputMuted ? Icons.volume_off : Icons.volume_up,
size: 18,
),
label: Text(
widget.outputMuted ? l10n.outputUnmuteAction : l10n.outputMuteAction,
),
selected: widget.outputMuted,
onSelected: (_) => widget.onToggleOutputMute(),
),
),
],
),
const SizedBox(height: 4),
Row(
children: [
const Icon(Icons.volume_down, size: 18),
Expanded(
child: Slider(
value: widget.outputGain.clamp(0.0, 2.0),
min: 0.0,
max: 2.0,
divisions: 40,
label: '${(widget.outputGain * 100).round()}%',
onChanged: widget.onGainChanged,
),
),
const Icon(Icons.volume_up, size: 18),
],
),
Text(statsText, style: theme.textTheme.bodySmall),
],
);
}
}
/// PTT capability badge (gen2 v0.9.3 / SDD-091).
///
@@ -1199,8 +1017,6 @@ class PttCapabilityBadge extends StatelessWidget {
required this.level,
required this.backendId,
required this.boundInputClass,
required this.boundKeyLabel,
required this.onConfigure,
});
/// Resolved capability level as the bridge emits it
@@ -1215,17 +1031,6 @@ class PttCapabilityBadge extends StatelessWidget {
/// or empty when no binding is set).
final String boundInputClass;
/// Platform-neutral key label captured by the binding dialog
/// (e.g. `"Space"`, `"F10"`, `"mouse-side-button:8"`). Empty
/// when the user has not saved a binding in the current
/// process. Display-only; the bridge holds the authoritative
/// binding. Per DEC-027 this string is the same one already
/// crossed into the Rust side — no new privacy surface.
final String boundKeyLabel;
/// Open the configure-binding dialog. Wired by the caller.
final VoidCallback onConfigure;
bool get _isFocused => level == 'L0Focused';
String _explainBodyForPlatform(AppL10n l10n) {
@@ -1304,59 +1109,40 @@ class PttCapabilityBadge extends StatelessWidget {
: '$badgeLabel\n($boundInputClass)';
return Padding(
padding: const EdgeInsets.only(bottom: 6),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Tooltip(
message: tooltipMessage,
child: Row(
children: [
Icon(
_isFocused ? Icons.crop_free : Icons.public,
size: 14,
color: theme.colorScheme.onSurfaceVariant,
),
const SizedBox(width: 4),
Expanded(
child: Text(
badgeLabel,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
),
if (_isFocused)
IconButton(
icon: const Icon(Icons.info_outline, size: 16),
tooltip: l10n.pttCapabilityExplainTitle,
visualDensity: VisualDensity.compact,
onPressed: () => _openExplanationSheet(context),
),
TextButton.icon(
icon: const Icon(Icons.tune, size: 14),
label: Text(l10n.pttConfigureAction),
onPressed: onConfigure,
),
],
child: Tooltip(
message: tooltipMessage,
child: Row(
children: [
Icon(
_isFocused ? Icons.crop_free : Icons.public,
size: 14,
color: theme.colorScheme.onSurfaceVariant,
),
),
// Second line: show which key the user just bound, so
// they can remember what to press. Only rendered when a
// binding has been saved in the current process (the
// bridge holds the authoritative binding across restarts;
// this label is display-only).
if (boundKeyLabel.isNotEmpty)
Padding(
padding: const EdgeInsets.only(left: 18, top: 2),
const SizedBox(width: 4),
Expanded(
child: Text(
l10n.pttCapabilityBoundKey(boundKeyLabel),
badgeLabel,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
fontFamily: 'monospace',
),
),
),
],
// Info icon only for L0Focused — the explanation sheet
// tells the user why their PTT may not work outside the
// app window and how to grant the permission. There is
// intentionally NO 'Configure' button here: the single
// configuration entry point is the Voice Bar's
// settings gear (onConfigure on `VoiceBar`). Having two
// identical bind-key entry points just confuses users.
if (_isFocused)
IconButton(
icon: const Icon(Icons.info_outline, size: 16),
tooltip: l10n.pttCapabilityExplainTitle,
visualDensity: VisualDensity.compact,
onPressed: () => _openExplanationSheet(context),
),
],
),
),
);
}