feat(ui,ios,android): audio output route picker + collapse voice controls into single modal

Two user-reported issues addressed:

  1. 'on supported devices such as iphone or android user should be
     able to select audio device such as speaker or airpods or phone'
  2. 'the bottom folder is duplicated with the app bar settings'

Issue 1 \u2014 audio output route picker:

Added the audio_router 1.1.1 plugin (MIT, supports iOS + Android)
which renders the platform-native picker:

  * iOS: Apple AVRoutePickerView system sheet \u2014 the same UI as
    Control Center's audio chooser. Lists Speaker / iPhone receiver /
    AirPods / connected Bluetooth devices / AirPlay / CarPlay.
    System manages the device list; we don't have to track route
    changes manually.

  * Android (post-rc.8 when we wire the platform): Material Design 3
    dialog backed by AudioManager.setCommunicationDevice() with
    SCO Bluetooth + USB headsets filtered for VoIP.

The picker prerequisite documented by the plugin (audio session
must be playAndRecord/voiceChat before the picker fires) is already
satisfied by our AppDelegate.swift configuration from commit 0466000.

The new _AudioOutputTile widget in voice_compact.dart subscribes to
AudioRouter.currentDeviceStream so the row label + icon auto-update
when the user plugs in headphones, connects AirPods, etc. \u2014 no
manual KVO observation needed.

Tile is mobile-only (Platform.isIOS || Platform.isAndroid). Desktop
hosts continue to use the system mixer; the tile is hidden.

Plugin caveat: the published enum AudioSourceType has no .usb
variant (despite README mentioning USB support). We map only the
seven real enum cases: builtinSpeaker / builtinReceiver / bluetooth /
wiredHeadset / carAudio / airplay / unknown.

Issue 2 \u2014 collapse voice controls into the single modal sheet:

The AppBar gear icon (Icons.tune) that opened VoiceSettingsDialog
was removed. It duplicated the configuration entry point that the
status chip \u2192 modal-sheet path already provides, and the user found
that duplication confusing on a phone-narrow screen where AppBar
real estate is precious.

The voice modal sheet (showVoiceDetailsSheet) is now the **single**
voice-controls surface on mobile, with layout (top to bottom):

  1. Audio output route picker tile (iOS / Android only) \u2014 new.
  2. Mode + bind / release-tail recap (display only).
  3. 'Adjust mode & release tail' OutlinedButton that closes the
     sheet and opens the same VoiceSettingsDialog the gear icon
     used to open. One config form, not two.
  4. Mic level meter.
  5. TX / RX frame counts + mic state.
  6. PTT capability badge (desktop only).

Sheet title renamed from 'Voice settings' (which collided with the
gear-icon tooltip) to 'Voice'. New l10n keys: voiceSheetTitle,
voiceAdjustSettings, audioOutputLabel, audioRoute{Speaker,Receiver,
Bluetooth,WiredHeadset,CarAudio,Airplay,Unknown}. en + zh translated.

Build: flutter build ios --release --no-codesign clean, 50.5 s,
Runner.app 30.2 MB (+200 KB from audio_router). flutter analyze
clean (6 pre-existing Radio.groupValue deprecation infos in
voice_settings.dart, unchanged).
This commit is contained in:
EdisonJwa
2026-05-16 18:18:43 +08:00
parent fa94b9438f
commit b8e9c8549e
7 changed files with 258 additions and 95 deletions
+21 -18
View File
@@ -502,12 +502,13 @@ class _BetaHomeState extends State<_BetaHome> {
}
}
/// Narrow-mode voice details modal sheet (Plan E status chip
/// trigger). Surfaces level meter + TX/RX counts + capability
/// badge in a Material 3 bottom sheet. Mode + bind-key + release-
/// tail configuration still live in [VoiceSettingsDialog] reached
/// via the AppBar gear icon — there is exactly one configuration
/// surface.
/// Narrow-mode voice controls modal sheet (Plan E status chip
/// trigger). On mobile this is the **single** voice-controls
/// surface: route picker + mode/tail recap + "Adjust" button to
/// [VoiceSettingsDialog] + level meter + stats + (desktop-only)
/// capability badge. The AppBar gear icon was removed in this
/// rc.8 follow-up so there is exactly one entry point to voice
/// controls on mobile.
Future<void> _onOpenVoiceDetailsSheet() async {
await showVoiceDetailsSheet(
context,
@@ -519,6 +520,7 @@ class _BetaHomeState extends State<_BetaHome> {
pttBackendId: _pttBackendId,
pttBoundInputClass: _pttBoundInputClass,
isTouchOnly: _isTouchOnlyPttHost,
onAdjustVoiceSettings: _onOpenVoiceSettings,
);
}
@@ -862,13 +864,19 @@ class _BetaHomeState extends State<_BetaHome> {
channelName: _currentVoiceChannelName(),
),
actions: [
// Narrow-mode AppBar gains mic-mute + headset-mute + voice
// settings icons when the user is in a voice channel, so
// the on-screen body can dedicate ~85% of its height to
// the channel tree + the bottom-anchored PTT button. Wide
// mode keeps these controls inside the VoiceBar widget
// (left column) so the signed-off rc.8 wide-layout
// doesn't shift.
// Narrow-mode AppBar gains mic-mute + headset-mute icons
// when the user is in a voice channel, so the on-screen
// body can dedicate ~85% of its height to the channel tree
// + the bottom-anchored PTT button. Wide mode keeps these
// controls inside the VoiceBar widget (left column) so the
// signed-off rc.8 wide-layout doesn't shift.
//
// The gear icon (Icons.tune) that used to live here was
// **removed** in the rc.8 follow-up: it duplicated the
// "Adjust mode & release tail" button now inside the modal
// sheet, and the user reported the duplication. The modal
// is reached by tapping the status chip above the PTT
// button — there is now exactly one entry point.
if (_phase == _Phase.connected &&
_inChannel &&
MediaQuery.of(context).size.width < 840.0) ...[
@@ -886,11 +894,6 @@ class _BetaHomeState extends State<_BetaHome> {
selectedIcon: const Icon(Icons.headset_off),
onPressed: _toggleOutputMute,
),
IconButton(
tooltip: l10n.voiceSettingsTitle,
icon: const Icon(Icons.tune),
onPressed: _onOpenVoiceSettings,
),
],
IconButton(
tooltip: l10n.aboutAction,