feat(ui,voice): add speaker mute button to VoiceBar

Speaker (output) mute existed in the legacy _AudioControls widget
and the rust.setOutputMuted bridge call but was lost when SDD-097
replaced _AudioControls with VoiceBar. Mic mute carried over;
speaker mute did not.

Wire it back: VoiceBar gains an outputMuted prop + onToggleOutputMute
callback and renders a headset/headset_off icon next to the
existing mic mute. main.dart wires the existing _toggleOutputMute
handler (previously dead-code with // ignore: unused_element). The
bridge call setOutputMuted already does both effects together:
local engine silencer + server-broadcast ClientOutputMuted flag.

l10n: rename voiceHardMuteLabel to 'Mute microphone'/'麦克风静音'
to distinguish from the new voiceOutputMuteLabel 'Mute speakers'/
'扬声器静音'.

flutter analyze: clean (6 pre-existing Radio.groupValue infos).
This commit is contained in:
EdisonJwa
2026-05-16 00:18:18 +08:00
parent 33d80894d0
commit 82b99ebcff
7 changed files with 42 additions and 6 deletions
+2 -1
View File
@@ -123,7 +123,8 @@
"voiceReleaseTailLabel": "Release tail", "voiceReleaseTailLabel": "Release tail",
"voiceReleaseTailHint": "ms", "voiceReleaseTailHint": "ms",
"voiceLeaveAction": "Leave voice", "voiceLeaveAction": "Leave voice",
"voiceHardMuteLabel": "Mute", "voiceHardMuteLabel": "Mute microphone",
"voiceOutputMuteLabel": "Mute speakers",
"voiceSettingsTitle": "Voice settings", "voiceSettingsTitle": "Voice settings",
"voiceBindKeyAction": "Bind PTT key" "voiceBindKeyAction": "Bind PTT key"
} }
+2 -1
View File
@@ -80,7 +80,8 @@
"voiceReleaseTailLabel": "释放延迟", "voiceReleaseTailLabel": "释放延迟",
"voiceReleaseTailHint": "毫秒", "voiceReleaseTailHint": "毫秒",
"voiceLeaveAction": "离开语音", "voiceLeaveAction": "离开语音",
"voiceHardMuteLabel": "静音", "voiceHardMuteLabel": "麦克风静音",
"voiceOutputMuteLabel": "扬声器静音",
"voiceSettingsTitle": "语音设置", "voiceSettingsTitle": "语音设置",
"voiceBindKeyAction": "绑定 PTT 按键" "voiceBindKeyAction": "绑定 PTT 按键"
} }
@@ -526,9 +526,15 @@ abstract class AppL10n {
/// No description provided for @voiceHardMuteLabel. /// No description provided for @voiceHardMuteLabel.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Mute'** /// **'Mute microphone'**
String get voiceHardMuteLabel; String get voiceHardMuteLabel;
/// No description provided for @voiceOutputMuteLabel.
///
/// In en, this message translates to:
/// **'Mute speakers'**
String get voiceOutputMuteLabel;
/// No description provided for @voiceSettingsTitle. /// No description provided for @voiceSettingsTitle.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -255,7 +255,10 @@ class AppL10nEn extends AppL10n {
String get voiceLeaveAction => 'Leave voice'; String get voiceLeaveAction => 'Leave voice';
@override @override
String get voiceHardMuteLabel => 'Mute'; String get voiceHardMuteLabel => 'Mute microphone';
@override
String get voiceOutputMuteLabel => 'Mute speakers';
@override @override
String get voiceSettingsTitle => 'Voice settings'; String get voiceSettingsTitle => 'Voice settings';
@@ -249,7 +249,10 @@ class AppL10nZh extends AppL10n {
String get voiceLeaveAction => '离开语音'; String get voiceLeaveAction => '离开语音';
@override @override
String get voiceHardMuteLabel => '静音'; String get voiceHardMuteLabel => '麦克风静音';
@override
String get voiceOutputMuteLabel => '扬声器静音';
@override @override
String get voiceSettingsTitle => '语音设置'; String get voiceSettingsTitle => '语音设置';
+2 -1
View File
@@ -318,7 +318,6 @@ class _BetaHomeState extends State<_BetaHome> {
} }
} }
// ignore: unused_element
Future<void> _toggleOutputMute() async { Future<void> _toggleOutputMute() async {
final next = !_outputMuted; final next = !_outputMuted;
try { try {
@@ -846,6 +845,7 @@ class _BetaHomeState extends State<_BetaHome> {
inChannel: _inChannel, inChannel: _inChannel,
transmitMode: _transmitMode, transmitMode: _transmitMode,
hardMute: _hardMute, hardMute: _hardMute,
outputMuted: _outputMuted,
releaseTailMs: _releaseTailMs, releaseTailMs: _releaseTailMs,
channelName: _currentVoiceChannelName(), channelName: _currentVoiceChannelName(),
audioStats: _audioStats, audioStats: _audioStats,
@@ -854,6 +854,7 @@ class _BetaHomeState extends State<_BetaHome> {
pttBoundInputClass: _pttBoundInputClass, pttBoundInputClass: _pttBoundInputClass,
pttBoundKeyLabel: _pttBoundKeyLabel, pttBoundKeyLabel: _pttBoundKeyLabel,
onToggleMute: _onToggleHardMute, onToggleMute: _onToggleHardMute,
onToggleOutputMute: _toggleOutputMute,
onConfigure: _onOpenVoiceSettings, onConfigure: _onOpenVoiceSettings,
onLeave: _onLeaveVoice, onLeave: _onLeaveVoice,
), ),
@@ -18,6 +18,7 @@ class VoiceBar extends StatelessWidget {
required this.inChannel, required this.inChannel,
required this.transmitMode, required this.transmitMode,
required this.hardMute, required this.hardMute,
required this.outputMuted,
required this.releaseTailMs, required this.releaseTailMs,
required this.channelName, required this.channelName,
required this.audioStats, required this.audioStats,
@@ -26,6 +27,7 @@ class VoiceBar extends StatelessWidget {
required this.pttBoundInputClass, required this.pttBoundInputClass,
required this.pttBoundKeyLabel, required this.pttBoundKeyLabel,
required this.onToggleMute, required this.onToggleMute,
required this.onToggleOutputMute,
required this.onConfigure, required this.onConfigure,
required this.onLeave, required this.onLeave,
}); });
@@ -39,6 +41,13 @@ class VoiceBar extends StatelessWidget {
/// Hard-mute clamp state. /// Hard-mute clamp state.
final bool hardMute; final bool hardMute;
/// Speaker (output) mute state. Mirrors the server-broadcast
/// `ClientOutputMuted` flag plus the engine's local output
/// silencer — toggling this hushes incoming voice immediately
/// AND tells the server so other clients see the headphone-off
/// icon next to our name.
final bool outputMuted;
/// Configured release-tail in milliseconds (0..=500). Surfaced as /// Configured release-tail in milliseconds (0..=500). Surfaced as
/// a hint underneath the mode badge. /// a hint underneath the mode badge.
final int releaseTailMs; final int releaseTailMs;
@@ -67,6 +76,9 @@ class VoiceBar extends StatelessWidget {
/// Toggle the hard-mute clamp. /// Toggle the hard-mute clamp.
final VoidCallback onToggleMute; final VoidCallback onToggleMute;
/// Toggle speaker (output) mute.
final VoidCallback onToggleOutputMute;
/// Open the voice settings dialog. This is the SINGLE entry point /// Open the voice settings dialog. This is the SINGLE entry point
/// for transmit-mode selection, PTT key binding, and release-tail /// for transmit-mode selection, PTT key binding, and release-tail
/// configuration. The capability badge below is information-only /// configuration. The capability badge below is information-only
@@ -136,6 +148,15 @@ class VoiceBar extends StatelessWidget {
), ),
], ],
const Spacer(), const Spacer(),
IconButton(
tooltip: l10n.voiceOutputMuteLabel,
icon: Icon(
outputMuted ? Icons.headset_off : Icons.headset,
),
isSelected: outputMuted,
selectedIcon: const Icon(Icons.headset_off),
onPressed: onToggleOutputMute,
),
IconButton( IconButton(
tooltip: l10n.voiceHardMuteLabel, tooltip: l10n.voiceHardMuteLabel,
icon: Icon(hardMute ? Icons.mic_off : Icons.mic), icon: Icon(hardMute ? Icons.mic_off : Icons.mic),