feat(ui,ios): inline mode+tail into voice sheet, fix Unknown audio route, tap-outside unfocus
Three user-reported issues addressed at once.
1. Audio output displaying as Unknown on iOS
The route tile only set _device from currentDeviceStream events,
which fire on route *changes*. On first sheet open with no route
change yet, _device was null \u2192 _deviceLabel fell through to
audioRouteUnknown.
Fix: query AudioRouterPlatform.instance.getCurrentDevice() in
initState before attaching the stream listener. Plugin returns the
current AVAudioSession route synchronously (well, via Future) so
the tile renders Speaker / iPhone receiver / AirPods / etc.
immediately on first open. Errors swallowed \u2014 the stream remains
authoritative for subsequent updates.
2. 'Adjust mode & release tail' too deep (chip \u2192 modal \u2192 button \u2192 dialog)
Inlined the mode radio buttons and release-tail slider directly
into the voice modal sheet. Dropped the OutlinedButton 'Adjust'
trigger and the nested VoiceSettingsDialog dispatch entirely on
mobile.
Modal sheet is now a single-screen control panel:
Title 'Voice'
--------
Audio output: <current route> > (iOS/Android only)
--------
Transmit mode
\u25c9 PTT
\u25cb Continuous
\u25cb Voice activity (Coming soon) (disabled)
--------
Release tail 200 ms
[\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u25cf\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501] (0\u20131000 ms, step 50)
Bound key: F (desktop only)
--------
Level meter
TX/RX frame counts
PTT capability badge (desktop only)
VoiceSettingsDialog is retained for the wide-mode VoiceBar
'configure' button (desktop entrypoint) and the PTT-bind flow, so
desktop UX is unaffected.
New widgets: _VoiceSheetBody (StatefulWidget with local _mode +
_tail), _ModeRow (RadioListTile-shaped row with optional disabled
state for VoiceActivity). New API on showVoiceDetailsSheet:
onModeChanged + onReleaseTailChanged callbacks (replace
onAdjustVoiceSettings). Wiring in main.dart writes through to
rust.setTransmitMode / rust.setReleaseTailMs and mirrors _state.
l10n: dropped voiceAdjustSettings (en + zh). Added voiceBoundKeyLabel
(en + zh) for the desktop-only bound-key row.
3. iOS first-tap-keyboard regression (flutter/flutter#181474)
The 79f8360 _kickFocus workaround (unfocus + microtask refocus on
every TextField.onTap) was kept, but extended with a tap-outside-
to-unfocus GestureDetector wrapping the connect form Column. This
guarantees the FocusNode is in the unfocused state when the next
field tap arrives, so the focus transition is always false\u2192true
on first tap.
GestureDetector(HitTestBehavior.translucent, onTap: unfocus) is the
canonical pattern recommended in the flutter/flutter#181474 thread
+ several older iOS keyboard issues. Translucent behaviour means it
catches taps on the column padding / empty regions without
swallowing taps on the TextFields themselves (those have
onTap: _kickFocus already).
flutter analyze: 6 pre-existing Radio.groupValue deprecation infos
in voice_settings.dart (unchanged). flutter build ios --release
--no-codesign: 27.9 s, Runner.app 30.2 MB (unchanged).
Awaiting iPhone retest to confirm all three fixes.
This commit is contained in:
@@ -504,11 +504,9 @@ class _BetaHomeState extends State<_BetaHome> {
|
||||
|
||||
/// 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.
|
||||
/// surface: route picker + inline mode radio + inline release-tail
|
||||
/// slider + level meter + stats + (desktop-only) capability badge.
|
||||
/// Zero navigation depth \u2014 no nested dialog.
|
||||
Future<void> _onOpenVoiceDetailsSheet() async {
|
||||
await showVoiceDetailsSheet(
|
||||
context,
|
||||
@@ -520,7 +518,26 @@ class _BetaHomeState extends State<_BetaHome> {
|
||||
pttBackendId: _pttBackendId,
|
||||
pttBoundInputClass: _pttBoundInputClass,
|
||||
isTouchOnly: _isTouchOnlyPttHost,
|
||||
onAdjustVoiceSettings: _onOpenVoiceSettings,
|
||||
onModeChanged: (mode) async {
|
||||
try {
|
||||
await rust.setTransmitMode(mode: mode);
|
||||
if (!mounted) return;
|
||||
setState(() => _transmitMode = mode);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
setState(() => _error = e.toString());
|
||||
}
|
||||
},
|
||||
onReleaseTailChanged: (ms) async {
|
||||
try {
|
||||
await rust.setReleaseTailMs(ms: ms);
|
||||
if (!mounted) return;
|
||||
setState(() => _releaseTailMs = ms);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
setState(() => _error = e.toString());
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1309,9 +1326,24 @@ class _ConnectFormState extends State<_ConnectForm> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppL10n.of(context);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// Wrap the form in a GestureDetector that unfocuses any focused
|
||||
// TextField when the user taps an empty area of the form. This is
|
||||
// the second half of the flutter/flutter#181474 workaround:
|
||||
// _kickFocus handles the case where the user re-taps the same
|
||||
// field after the keyboard auto-dismissed, and this outer
|
||||
// gesture handler handles the more common case where the user
|
||||
// taps outside (e.g. on the column padding) to dismiss the
|
||||
// keyboard. By forcing an unfocus on tap-outside we guarantee
|
||||
// the FocusNode is in the unfocused state when the next field
|
||||
// tap arrives, so the focus transition is always false\u2192true
|
||||
// (which is what iOS needs to re-open TextInput on the very
|
||||
// first tap).
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () => FocusScope.of(context).unfocus(),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
TextField(
|
||||
controller: widget.hostCtl,
|
||||
focusNode: _hostFocus,
|
||||
@@ -1402,6 +1434,7 @@ class _ConnectFormState extends State<_ConnectForm> {
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user