feat(voice): unified mobile voice bar with gesture-isolated PTT row
Replace separate VoiceStatusChip + VoicePttButton with a single CompactVoiceBar widget that combines both into a two-row layout: - Control row (tap): status text, mute, deafen, settings chevron - PTT row (hold): full-width hold-to-talk, shown only in PTT mode Gesture isolation prevents mis-touch between rows: the control row uses tap-only InkWell/IconButton while the PTT row uses a raw Listener for pointer-down/up events. Key changes: - Add CompactVoiceBar widget with state-colored container (normal, muted, talk-power-blocked) - Remove mute/deafen IconButtons from AppBar headerActions - Restructure voice details sheet into primary section + collapsible ExpansionTiles (audio processing, PTT capability, debug) - Optimistic state updates for mute/deafen to eliminate tap delay - Instant PTT visual feedback (no AnimatedContainer fade) - Constant geometry across all states (no layout shift on toggle)
This commit is contained in:
@@ -1126,14 +1126,18 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
||||
|
||||
Future<void> _toggleOutputMute() async {
|
||||
final next = !_outputMuted;
|
||||
// Optimistic: flip the UI immediately.
|
||||
setState(() {
|
||||
_outputMuted = next;
|
||||
});
|
||||
try {
|
||||
await rust.setOutputMuted(muted: next);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_outputMuted = next;
|
||||
});
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
// Roll back on failure.
|
||||
setState(() {
|
||||
_outputMuted = !next;
|
||||
});
|
||||
_showUiError('output mute', e);
|
||||
}
|
||||
}
|
||||
@@ -1260,6 +1264,13 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
||||
return;
|
||||
}
|
||||
final next = !_hardMute;
|
||||
// Optimistic: flip the UI immediately so the icon responds
|
||||
// before the two bridge calls round-trip through FFI.
|
||||
setState(() {
|
||||
_inputMuted = next;
|
||||
_hardMute = next;
|
||||
_hardMuteByPermission = false;
|
||||
});
|
||||
try {
|
||||
// Hard-mute is two coordinated effects:
|
||||
// * setHardMute — local TransmitGate clamp; we stop sending
|
||||
@@ -1272,14 +1283,13 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
||||
// through. Drive them together.
|
||||
await rust.setHardMute(muted: next);
|
||||
await rust.setInputMuted(muted: next);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_inputMuted = next;
|
||||
_hardMute = next;
|
||||
_hardMuteByPermission = false;
|
||||
});
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
// Roll back on failure.
|
||||
setState(() {
|
||||
_inputMuted = !next;
|
||||
_hardMute = !next;
|
||||
});
|
||||
_showUiError('hard mute', e);
|
||||
}
|
||||
}
|
||||
@@ -2145,26 +2155,6 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
final headerActions = [
|
||||
if (_serverReachable && _inChannel) ...[
|
||||
IconButton(
|
||||
tooltip: _hardMuteByTalkPower
|
||||
? 'Insufficient talk power to speak in this channel'
|
||||
: l10n.voiceHardMuteLabel,
|
||||
icon: Icon(_hardMute ? Icons.mic_off : Icons.mic),
|
||||
isSelected: _hardMute,
|
||||
selectedIcon: const Icon(Icons.mic_off),
|
||||
color: _hardMute ? theme.colorScheme.error : null,
|
||||
onPressed: _hardMuteByTalkPower ? null : _onToggleHardMute,
|
||||
),
|
||||
IconButton(
|
||||
tooltip: l10n.voiceOutputMuteLabel,
|
||||
icon: Icon(_outputMuted ? Icons.headset_off : Icons.headset),
|
||||
isSelected: _outputMuted,
|
||||
selectedIcon: const Icon(Icons.headset_off),
|
||||
color: _outputMuted ? theme.colorScheme.error : null,
|
||||
onPressed: _toggleOutputMute,
|
||||
),
|
||||
],
|
||||
IconButton(
|
||||
tooltip: l10n.aboutAction,
|
||||
icon: const Icon(Icons.info_outline),
|
||||
@@ -2449,7 +2439,8 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
||||
Expanded(child: snapshotView),
|
||||
const SizedBox(height: 8),
|
||||
permissionBanner,
|
||||
VoiceStatusChip(
|
||||
CompactVoiceBar(
|
||||
inChannel: _inChannel,
|
||||
transmitMode: _transmitMode,
|
||||
releaseTailMs: _releaseTailMs,
|
||||
pttBoundKeyLabel: _pttBoundKeyLabel,
|
||||
@@ -2457,19 +2448,15 @@ class _BetaHomeState extends State<_BetaHome> with WidgetsBindingObserver {
|
||||
isTouchOnly: isTouchOnlyPttHost,
|
||||
inputMuted: _hardMute,
|
||||
outputMuted: _outputMuted,
|
||||
onToggleInputMute: _onToggleHardMute,
|
||||
onToggleOutputMute: _toggleOutputMute,
|
||||
onOpenDetails: () => _onOpenVoiceDetailsSheet(),
|
||||
onPttHeldChanged: _onOnscreenPttHeldChanged,
|
||||
talkPower: ownClientState?.talkPower,
|
||||
neededTalkPower: ownClientState?.neededTalkPower,
|
||||
talkPowerGranted: ownClientState?.talkPowerGranted,
|
||||
onTap: () => _onOpenVoiceDetailsSheet(),
|
||||
hardMuteByTalkPower: _hardMuteByTalkPower,
|
||||
),
|
||||
if (_inChannel &&
|
||||
_transmitMode == rust.BridgeTransmitMode.ptt) ...[
|
||||
const SizedBox(height: 8),
|
||||
VoicePttButton(
|
||||
active: _audioStats?.pttActive ?? false,
|
||||
onHeldChanged: _onOnscreenPttHeldChanged,
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user