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),
),
],
),
),
);
}
@@ -27,7 +27,6 @@ class VoiceBar extends StatelessWidget {
required this.pttBoundKeyLabel,
required this.onToggleMute,
required this.onConfigure,
required this.onBindKey,
required this.onLeave,
});
@@ -68,14 +67,12 @@ class VoiceBar extends StatelessWidget {
/// Toggle the hard-mute clamp.
final VoidCallback onToggleMute;
/// Open the voice settings dialog (mode + release tail).
/// Open the voice settings dialog. This is the SINGLE entry point
/// for transmit-mode selection, PTT key binding, and release-tail
/// configuration. The capability badge below is information-only
/// and intentionally does NOT have its own configure affordance.
final VoidCallback onConfigure;
/// Open the bind-key capture flow directly (skips the settings
/// dialog). Surfaced from the capability badge when PTT mode is
/// active.
final VoidCallback onBindKey;
/// Leave the voice channel.
final VoidCallback onLeave;
@@ -208,13 +205,15 @@ class VoiceBar extends StatelessWidget {
// PTT capability badge — only relevant when PTT mode is
// active. Hidden for Continuous / Voice Activity since
// there's no key binding to surface a capability for.
// The badge is information-only; the user reaches the
// bind-key flow through the Voice Bar's settings gear
// (single configuration entry point — see the comment
// on `onConfigure`).
if (isPtt)
PttCapabilityBadge(
level: pttLevel,
backendId: pttBackendId,
boundInputClass: pttBoundInputClass,
boundKeyLabel: pttBoundKeyLabel,
onConfigure: onBindKey,
),
if (inChannel) ...[
const SizedBox(height: 6),