feat(ios,p0): iOS P0 platform, audio fixes, channel UX
This commit is contained in:
@@ -256,7 +256,7 @@ class _VoicePttButtonState extends State<VoicePttButton> {
|
||||
/// 3. Release-tail slider (PTT-only).
|
||||
/// 4. Mic level meter.
|
||||
/// 5. TX / RX frame counts.
|
||||
/// 6. PTT capability badge (desktop only).
|
||||
/// 6. PTT capability badge.
|
||||
///
|
||||
/// Mode + release-tail are inlined directly here instead of being
|
||||
/// hidden behind an "Adjust" button → nested dialog. Single-screen
|
||||
@@ -355,8 +355,7 @@ class _VoiceSheetBodyState extends State<_VoiceSheetBody> {
|
||||
|
||||
// Route picker only meaningful on iOS + Android where the OS
|
||||
// owns audio routing. Desktop hosts skip the tile entirely.
|
||||
final showRoutePicker =
|
||||
!kIsWeb && (Platform.isIOS || Platform.isAndroid);
|
||||
final showRoutePicker = !kIsWeb && (Platform.isIOS || Platform.isAndroid);
|
||||
|
||||
return SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
@@ -365,10 +364,7 @@ class _VoiceSheetBodyState extends State<_VoiceSheetBody> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
l10n.voiceSheetTitle,
|
||||
style: theme.textTheme.titleLarge,
|
||||
),
|
||||
Text(l10n.voiceSheetTitle, style: theme.textTheme.titleLarge),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// 1) Audio output route picker tile (mobile only).
|
||||
@@ -428,9 +424,9 @@ class _VoiceSheetBodyState extends State<_VoiceSheetBody> {
|
||||
],
|
||||
),
|
||||
Slider(
|
||||
value: _tail.toDouble().clamp(0, 1000),
|
||||
value: _tail.toDouble().clamp(0, 500),
|
||||
min: 0,
|
||||
max: 1000,
|
||||
max: 500,
|
||||
divisions: 20,
|
||||
label: '$_tail ms',
|
||||
onChanged: _setTail,
|
||||
@@ -470,8 +466,12 @@ class _VoiceSheetBodyState extends State<_VoiceSheetBody> {
|
||||
style: theme.textTheme.bodySmall,
|
||||
),
|
||||
|
||||
// 5) PTT capability badge \u2014 desktop-only.
|
||||
if (isPtt && !widget.isTouchOnly) ...[
|
||||
// 5) PTT capability badge. On iOS this must remain
|
||||
// visible even though the resolved level is always
|
||||
// `L0Focused`, because the P0 acceptance flow requires
|
||||
// honest capability advertising with an explanation of
|
||||
// the sandbox limitation.
|
||||
if (isPtt) ...[
|
||||
const SizedBox(height: 12),
|
||||
PttCapabilityBadge(
|
||||
level: widget.pttLevel,
|
||||
@@ -506,8 +506,8 @@ class _ModeRow extends StatelessWidget {
|
||||
final color = disabled
|
||||
? theme.colorScheme.onSurfaceVariant.withAlpha(120)
|
||||
: selected
|
||||
? theme.colorScheme.primary
|
||||
: theme.colorScheme.onSurface;
|
||||
? theme.colorScheme.primary
|
||||
: theme.colorScheme.onSurface;
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
@@ -604,8 +604,11 @@ class _AudioOutputTileState extends State<_AudioOutputTile> {
|
||||
}
|
||||
}
|
||||
|
||||
static String _portLabel(AVAudioSessionPort? type, String fallback,
|
||||
AppL10n l10n) {
|
||||
static String _portLabel(
|
||||
AVAudioSessionPort? type,
|
||||
String fallback,
|
||||
AppL10n l10n,
|
||||
) {
|
||||
switch (type) {
|
||||
case AVAudioSessionPort.builtInSpeaker:
|
||||
return l10n.audioRouteSpeaker;
|
||||
@@ -783,8 +786,9 @@ class _AudioOutputPickerSheetState extends State<_AudioOutputPickerSheet> {
|
||||
// mic by default, or BT/wired if connected and selected
|
||||
// elsewhere). For consistency, only switch input when the
|
||||
// user explicitly picks a non-speaker input row.
|
||||
await AVAudioSession()
|
||||
.overrideOutputAudioPort(AVAudioSessionPortOverride.speaker);
|
||||
await AVAudioSession().overrideOutputAudioPort(
|
||||
AVAudioSessionPortOverride.speaker,
|
||||
);
|
||||
debugPrint('chanora: audio output -> speakerphone (override applied)');
|
||||
} catch (e, st) {
|
||||
debugPrint('chanora: _selectSpeaker FAILED: $e\n$st');
|
||||
@@ -800,8 +804,9 @@ class _AudioOutputPickerSheetState extends State<_AudioOutputPickerSheet> {
|
||||
// which is the built-in receiver. Calling setPreferredInput
|
||||
// explicitly here is redundant and risks the same recalc
|
||||
// race that broke _selectSpeaker before.
|
||||
await AVAudioSession()
|
||||
.overrideOutputAudioPort(AVAudioSessionPortOverride.none);
|
||||
await AVAudioSession().overrideOutputAudioPort(
|
||||
AVAudioSessionPortOverride.none,
|
||||
);
|
||||
debugPrint('chanora: audio output -> receiver (override cleared)');
|
||||
} catch (e, st) {
|
||||
debugPrint('chanora: _selectReceiver FAILED: $e\n$st');
|
||||
@@ -817,11 +822,13 @@ class _AudioOutputPickerSheetState extends State<_AudioOutputPickerSheet> {
|
||||
// OWN output (the user hears audio through the same device
|
||||
// they speak into), so .none + setPreferredInput is the
|
||||
// correct combo here.
|
||||
await AVAudioSession()
|
||||
.overrideOutputAudioPort(AVAudioSessionPortOverride.none);
|
||||
await AVAudioSession().overrideOutputAudioPort(
|
||||
AVAudioSessionPortOverride.none,
|
||||
);
|
||||
await AVAudioSession().setPreferredInput(port);
|
||||
debugPrint(
|
||||
'chanora: audio output -> ${port.portName} (${port.portType})');
|
||||
'chanora: audio output -> ${port.portName} (${port.portType})',
|
||||
);
|
||||
} catch (e, st) {
|
||||
debugPrint('chanora: _selectInput FAILED: $e\n$st');
|
||||
}
|
||||
@@ -843,10 +850,12 @@ class _AudioOutputPickerSheetState extends State<_AudioOutputPickerSheet> {
|
||||
);
|
||||
}
|
||||
|
||||
final currentOutputType =
|
||||
_route?.outputs.isNotEmpty == true ? _route!.outputs.first.portType : null;
|
||||
final currentInputUid =
|
||||
_route?.inputs.isNotEmpty == true ? _route!.inputs.first.uid : null;
|
||||
final currentOutputType = _route?.outputs.isNotEmpty == true
|
||||
? _route!.outputs.first.portType
|
||||
: null;
|
||||
final currentInputUid = _route?.inputs.isNotEmpty == true
|
||||
? _route!.inputs.first.uid
|
||||
: null;
|
||||
|
||||
// Whether the active route is the speakerphone override (built-in
|
||||
// speaker is the output but the actual session category isn't
|
||||
@@ -868,10 +877,7 @@ class _AudioOutputPickerSheetState extends State<_AudioOutputPickerSheet> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
l10n.audioOutputLabel,
|
||||
style: theme.textTheme.titleLarge,
|
||||
),
|
||||
Text(l10n.audioOutputLabel, style: theme.textTheme.titleLarge),
|
||||
const SizedBox(height: 16),
|
||||
_PickerRow(
|
||||
icon: Icons.volume_up,
|
||||
@@ -889,7 +895,10 @@ class _AudioOutputPickerSheetState extends State<_AudioOutputPickerSheet> {
|
||||
_PickerRow(
|
||||
icon: _AudioOutputTileState._portIcon(port.portType),
|
||||
label: _AudioOutputTileState._portLabel(
|
||||
port.portType, port.portName, l10n),
|
||||
port.portType,
|
||||
port.portName,
|
||||
l10n,
|
||||
),
|
||||
selected: !isSpeaker && port.uid == currentInputUid,
|
||||
onTap: () => _selectInput(port),
|
||||
),
|
||||
@@ -916,8 +925,9 @@ class _PickerRow extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final color =
|
||||
selected ? theme.colorScheme.primary : theme.colorScheme.onSurface;
|
||||
final color = selected
|
||||
? theme.colorScheme.primary
|
||||
: theme.colorScheme.onSurface;
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
@@ -933,8 +943,7 @@ class _PickerRow extends StatelessWidget {
|
||||
style: theme.textTheme.bodyLarge?.copyWith(color: color),
|
||||
),
|
||||
),
|
||||
if (selected)
|
||||
Icon(Icons.check, color: theme.colorScheme.primary),
|
||||
if (selected) Icon(Icons.check, color: theme.colorScheme.primary),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user