fix(ui,voice): five Voice Bar / settings UX bugs
1. Hard-mute now informs the server (setInputMuted) in addition to clamping the local TransmitGate. Without the server-side flag, other clients keep seeing us un-muted; without the local clamp a beat of in-flight audio leaks through. Drive both together so the mic icon and the actual silence land at the same time. 2. Split the badge's Configure affordance from the Voice Bar's 'Voice settings' gear. The gear opens the mode + release-tail dialog (onConfigure); the badge's configure opens the bind-key capture flow directly (new onBindKey). Previously both routed to the settings dialog, so 'Voice settings' and the badge's 'Configure' were the same screen — useless duplication. 3. Bind-key label is now PTT-only. The mode-badge row no longer prints 'PTT: Space' when Continuous / Voice Activity is selected. A new PTT-only secondary line carries the bound key plus the release-tail value together, hidden entirely for non-PTT modes. 4. Release-tail row is now PTT-only in BOTH the Voice Bar and the Voice settings dialog. The dialog previously kept the slider visible across all modes; switching to Continuous left the user staring at a control that did nothing. 5. PTT capability badge is now PTT-only. In Continuous and Voice Activity modes there is no key binding to surface a capability for, so the 'L0Focused (focused)' line + its info sheet and the Configure button disappear from the Voice Bar when the user isn't in PTT mode. All five fixes are pure UI; no Rust changes needed. flutter analyze remains clean (6 pre-existing Radio.groupValue deprecation infos).
This commit is contained in:
@@ -356,7 +356,19 @@ class _BetaHomeState extends State<_BetaHome> {
|
|||||||
Future<void> _onToggleHardMute() async {
|
Future<void> _onToggleHardMute() async {
|
||||||
final next = !_hardMute;
|
final next = !_hardMute;
|
||||||
try {
|
try {
|
||||||
|
// Hard-mute is two coordinated effects:
|
||||||
|
// * setHardMute — local TransmitGate clamp; we stop sending
|
||||||
|
// Opus frames the instant this returns.
|
||||||
|
// * setInputMuted — server-side ClientMuted flag so other
|
||||||
|
// clients see the mic-off icon next to our name and the
|
||||||
|
// server stops relaying any in-flight frames.
|
||||||
|
// Sending only one of them is user-confusing; clients see
|
||||||
|
// silence but no icon, or icon but a beat of audio leaks
|
||||||
|
// through. Drive them together.
|
||||||
await rust.setHardMute(muted: next);
|
await rust.setHardMute(muted: next);
|
||||||
|
await rust.setInputMuted(muted: next);
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() => _inputMuted = next);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() => _error = e.toString());
|
setState(() => _error = e.toString());
|
||||||
@@ -822,6 +834,7 @@ class _BetaHomeState extends State<_BetaHome> {
|
|||||||
pttBoundKeyLabel: _pttBoundKeyLabel,
|
pttBoundKeyLabel: _pttBoundKeyLabel,
|
||||||
onToggleMute: _onToggleHardMute,
|
onToggleMute: _onToggleHardMute,
|
||||||
onConfigure: _onOpenVoiceSettings,
|
onConfigure: _onOpenVoiceSettings,
|
||||||
|
onBindKey: () => _onConfigurePtt(context),
|
||||||
onLeave: _onLeaveVoice,
|
onLeave: _onLeaveVoice,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ class VoiceBar extends StatelessWidget {
|
|||||||
required this.pttBoundKeyLabel,
|
required this.pttBoundKeyLabel,
|
||||||
required this.onToggleMute,
|
required this.onToggleMute,
|
||||||
required this.onConfigure,
|
required this.onConfigure,
|
||||||
|
required this.onBindKey,
|
||||||
required this.onLeave,
|
required this.onLeave,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -67,17 +68,21 @@ class VoiceBar extends StatelessWidget {
|
|||||||
/// Toggle the hard-mute clamp.
|
/// Toggle the hard-mute clamp.
|
||||||
final VoidCallback onToggleMute;
|
final VoidCallback onToggleMute;
|
||||||
|
|
||||||
/// Open the voice settings dialog.
|
/// Open the voice settings dialog (mode + release tail).
|
||||||
final VoidCallback onConfigure;
|
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.
|
/// Leave the voice channel.
|
||||||
final VoidCallback onLeave;
|
final VoidCallback onLeave;
|
||||||
|
|
||||||
String _modeLabel(AppL10n l10n) {
|
String _modeLabel(AppL10n l10n) {
|
||||||
switch (transmitMode) {
|
switch (transmitMode) {
|
||||||
case rust.BridgeTransmitMode.ptt:
|
case rust.BridgeTransmitMode.ptt:
|
||||||
final key = pttBoundKeyLabel.isEmpty ? '—' : pttBoundKeyLabel;
|
return l10n.voiceModePtt;
|
||||||
return '${l10n.voiceModePtt}: $key';
|
|
||||||
case rust.BridgeTransmitMode.continuous:
|
case rust.BridgeTransmitMode.continuous:
|
||||||
return l10n.voiceModeContinuous;
|
return l10n.voiceModeContinuous;
|
||||||
case rust.BridgeTransmitMode.voiceActivity:
|
case rust.BridgeTransmitMode.voiceActivity:
|
||||||
@@ -91,6 +96,7 @@ class VoiceBar extends StatelessWidget {
|
|||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final stats = audioStats;
|
final stats = audioStats;
|
||||||
final levelActive = stats?.pttActive ?? false;
|
final levelActive = stats?.pttActive ?? false;
|
||||||
|
final isPtt = transmitMode == rust.BridgeTransmitMode.ptt;
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
margin: EdgeInsets.zero,
|
margin: EdgeInsets.zero,
|
||||||
@@ -169,11 +175,16 @@ class VoiceBar extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
// Row 3: release tail hint (only meaningful for PTT mode)
|
// Row 3: PTT-only secondary line — bound key + release
|
||||||
if (transmitMode == rust.BridgeTransmitMode.ptt)
|
// tail. Hidden entirely for Continuous / Voice Activity
|
||||||
|
// so the bar stays focused on what's actually in use.
|
||||||
|
if (isPtt)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(left: 22, top: 2),
|
padding: const EdgeInsets.only(left: 22, top: 2),
|
||||||
child: Text(
|
child: Text(
|
||||||
|
'${l10n.voiceModePtt}: '
|
||||||
|
'${pttBoundKeyLabel.isEmpty ? "—" : pttBoundKeyLabel}'
|
||||||
|
' · '
|
||||||
'${l10n.voiceReleaseTailLabel}: $releaseTailMs${l10n.voiceReleaseTailHint}',
|
'${l10n.voiceReleaseTailLabel}: $releaseTailMs${l10n.voiceReleaseTailHint}',
|
||||||
style: theme.textTheme.bodySmall?.copyWith(
|
style: theme.textTheme.bodySmall?.copyWith(
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
@@ -194,14 +205,17 @@ class VoiceBar extends StatelessWidget {
|
|||||||
style: theme.textTheme.bodySmall,
|
style: theme.textTheme.bodySmall,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
// PTT capability badge
|
// PTT capability badge — only relevant when PTT mode is
|
||||||
PttCapabilityBadge(
|
// active. Hidden for Continuous / Voice Activity since
|
||||||
level: pttLevel,
|
// there's no key binding to surface a capability for.
|
||||||
backendId: pttBackendId,
|
if (isPtt)
|
||||||
boundInputClass: pttBoundInputClass,
|
PttCapabilityBadge(
|
||||||
boundKeyLabel: pttBoundKeyLabel,
|
level: pttLevel,
|
||||||
onConfigure: onConfigure,
|
backendId: pttBackendId,
|
||||||
),
|
boundInputClass: pttBoundInputClass,
|
||||||
|
boundKeyLabel: pttBoundKeyLabel,
|
||||||
|
onConfigure: onBindKey,
|
||||||
|
),
|
||||||
if (inChannel) ...[
|
if (inChannel) ...[
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
Align(
|
Align(
|
||||||
|
|||||||
@@ -103,46 +103,53 @@ class _VoiceSettingsDialogState extends State<VoiceSettingsDialog> {
|
|||||||
onChanged: null,
|
onChanged: null,
|
||||||
),
|
),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
OutlinedButton.icon(
|
// Bind-key + release-tail are PTT-only concepts. Hide
|
||||||
icon: const Icon(Icons.keyboard),
|
// them entirely when the user has switched to a
|
||||||
label: Text(l10n.voiceBindKeyAction),
|
// non-PTT mode so the dialog stays focused on what's
|
||||||
onPressed: () {
|
// actually configurable for that mode.
|
||||||
Navigator.of(context).pop(
|
if (_mode == rust.BridgeTransmitMode.ptt) ...[
|
||||||
VoiceSettingsResult(
|
OutlinedButton.icon(
|
||||||
mode: _mode,
|
icon: const Icon(Icons.keyboard),
|
||||||
releaseTailMs: _releaseTail.round(),
|
label: Text(l10n.voiceBindKeyAction),
|
||||||
bindKeyRequested: true,
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop(
|
||||||
|
VoiceSettingsResult(
|
||||||
|
mode: _mode,
|
||||||
|
releaseTailMs: _releaseTail.round(),
|
||||||
|
bindKeyRequested: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
l10n.voiceReleaseTailLabel,
|
||||||
|
style: theme.textTheme.titleSmall,
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Slider(
|
||||||
|
value: _releaseTail,
|
||||||
|
min: 0,
|
||||||
|
max: 500,
|
||||||
|
divisions: 20, // step 25 ms
|
||||||
|
label:
|
||||||
|
'${_releaseTail.round()}${l10n.voiceReleaseTailHint}',
|
||||||
|
onChanged: (v) => setState(() => _releaseTail = v),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
SizedBox(
|
||||||
},
|
width: 64,
|
||||||
),
|
child: Text(
|
||||||
const SizedBox(height: 8),
|
'${_releaseTail.round()}${l10n.voiceReleaseTailHint}',
|
||||||
Text(
|
style: theme.textTheme.bodySmall,
|
||||||
l10n.voiceReleaseTailLabel,
|
textAlign: TextAlign.end,
|
||||||
style: theme.textTheme.titleSmall,
|
),
|
||||||
),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Slider(
|
|
||||||
value: _releaseTail,
|
|
||||||
min: 0,
|
|
||||||
max: 500,
|
|
||||||
divisions: 20, // step 25 ms
|
|
||||||
label: '${_releaseTail.round()}${l10n.voiceReleaseTailHint}',
|
|
||||||
onChanged: (v) => setState(() => _releaseTail = v),
|
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
SizedBox(
|
),
|
||||||
width: 64,
|
],
|
||||||
child: Text(
|
|
||||||
'${_releaseTail.round()}${l10n.voiceReleaseTailHint}',
|
|
||||||
style: theme.textTheme.bodySmall,
|
|
||||||
textAlign: TextAlign.end,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user