1. Hard-mute now informs the server (setInputMuted) in addition to clamping the local TransmitGate. Without the server-side flag, other clients keep seeing us un-muted; without the local clamp a beat of in-flight audio leaks through. Drive both together so the mic icon and the actual silence land at the same time. 2. Split the badge's Configure affordance from the Voice Bar's 'Voice settings' gear. The gear opens the mode + release-tail dialog (onConfigure); the badge's configure opens the bind-key capture flow directly (new onBindKey). Previously both routed to the settings dialog, so 'Voice settings' and the badge's 'Configure' were the same screen — useless duplication. 3. Bind-key label is now PTT-only. The mode-badge row no longer prints 'PTT: Space' when Continuous / Voice Activity is selected. A new PTT-only secondary line carries the bound key plus the release-tail value together, hidden entirely for non-PTT modes. 4. Release-tail row is now PTT-only in BOTH the Voice Bar and the Voice settings dialog. The dialog previously kept the slider visible across all modes; switching to Continuous left the user staring at a control that did nothing. 5. PTT capability badge is now PTT-only. In Continuous and Voice Activity modes there is no key binding to surface a capability for, so the 'L0Focused (focused)' line + its info sheet and the Configure button disappear from the Voice Bar when the user isn't in PTT mode. All five fixes are pure UI; no Rust changes needed. flutter analyze remains clean (6 pre-existing Radio.groupValue deprecation infos).
175 lines
5.6 KiB
Dart
175 lines
5.6 KiB
Dart
// Voice settings dialog (SDD-097). Surfaces a TransmitMode radio
|
|
// group, a bind-key button, and a release-tail slider.
|
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import '../l10n/generated/app_localizations.dart';
|
|
import '../src/rust/api.dart' as rust;
|
|
|
|
/// Result returned by [`VoiceSettingsDialog`]. `null` indicates a
|
|
/// cancelled dialog.
|
|
class VoiceSettingsResult {
|
|
/// Construct a result snapshot.
|
|
const VoiceSettingsResult({
|
|
required this.mode,
|
|
required this.releaseTailMs,
|
|
required this.bindKeyRequested,
|
|
});
|
|
|
|
/// Selected transmit mode.
|
|
final rust.BridgeTransmitMode mode;
|
|
|
|
/// Chosen release-tail in milliseconds (0..=500, step 25).
|
|
final int releaseTailMs;
|
|
|
|
/// True when the user tapped the "bind key" button. The caller
|
|
/// is expected to open the focus-scoped capture dialog
|
|
/// afterwards.
|
|
final bool bindKeyRequested;
|
|
}
|
|
|
|
/// Voice settings dialog widget.
|
|
class VoiceSettingsDialog extends StatefulWidget {
|
|
/// Construct a dialog seeded with the current settings.
|
|
const VoiceSettingsDialog({
|
|
super.key,
|
|
required this.initialMode,
|
|
required this.initialReleaseTailMs,
|
|
});
|
|
|
|
/// Currently active transmit mode.
|
|
final rust.BridgeTransmitMode initialMode;
|
|
|
|
/// Currently configured release tail in milliseconds.
|
|
final int initialReleaseTailMs;
|
|
|
|
@override
|
|
State<VoiceSettingsDialog> createState() => _VoiceSettingsDialogState();
|
|
}
|
|
|
|
class _VoiceSettingsDialogState extends State<VoiceSettingsDialog> {
|
|
late rust.BridgeTransmitMode _mode;
|
|
late double _releaseTail;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_mode = widget.initialMode;
|
|
_releaseTail = widget.initialReleaseTailMs.clamp(0, 500).toDouble();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final l10n = AppL10n.of(context);
|
|
final theme = Theme.of(context);
|
|
return AlertDialog(
|
|
title: Text(l10n.voiceSettingsTitle),
|
|
content: SizedBox(
|
|
width: 360,
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
l10n.voiceModeLabel,
|
|
style: theme.textTheme.titleSmall,
|
|
),
|
|
const SizedBox(height: 4),
|
|
RadioListTile<rust.BridgeTransmitMode>(
|
|
dense: true,
|
|
value: rust.BridgeTransmitMode.ptt,
|
|
groupValue: _mode,
|
|
title: Text(l10n.voiceModePtt),
|
|
onChanged: (v) => setState(() => _mode = v!),
|
|
),
|
|
RadioListTile<rust.BridgeTransmitMode>(
|
|
dense: true,
|
|
value: rust.BridgeTransmitMode.continuous,
|
|
groupValue: _mode,
|
|
title: Text(l10n.voiceModeContinuous),
|
|
onChanged: (v) => setState(() => _mode = v!),
|
|
),
|
|
RadioListTile<rust.BridgeTransmitMode>(
|
|
dense: true,
|
|
value: rust.BridgeTransmitMode.voiceActivity,
|
|
groupValue: _mode,
|
|
title: Text(l10n.voiceModeVoiceActivity),
|
|
secondary: Text(
|
|
l10n.voiceModeComingSoon,
|
|
style: theme.textTheme.bodySmall,
|
|
),
|
|
// VoiceActivity is reserved per DEC-030 — keep the
|
|
// tile visible but disabled per SDD-095.
|
|
onChanged: null,
|
|
),
|
|
const Divider(),
|
|
// Bind-key + release-tail are PTT-only concepts. Hide
|
|
// them entirely when the user has switched to a
|
|
// non-PTT mode so the dialog stays focused on what's
|
|
// actually configurable for that mode.
|
|
if (_mode == rust.BridgeTransmitMode.ptt) ...[
|
|
OutlinedButton.icon(
|
|
icon: const Icon(Icons.keyboard),
|
|
label: Text(l10n.voiceBindKeyAction),
|
|
onPressed: () {
|
|
Navigator.of(context).pop(
|
|
VoiceSettingsResult(
|
|
mode: _mode,
|
|
releaseTailMs: _releaseTail.round(),
|
|
bindKeyRequested: true,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
l10n.voiceReleaseTailLabel,
|
|
style: theme.textTheme.titleSmall,
|
|
),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: Slider(
|
|
value: _releaseTail,
|
|
min: 0,
|
|
max: 500,
|
|
divisions: 20, // step 25 ms
|
|
label:
|
|
'${_releaseTail.round()}${l10n.voiceReleaseTailHint}',
|
|
onChanged: (v) => setState(() => _releaseTail = v),
|
|
),
|
|
),
|
|
SizedBox(
|
|
width: 64,
|
|
child: Text(
|
|
'${_releaseTail.round()}${l10n.voiceReleaseTailHint}',
|
|
style: theme.textTheme.bodySmall,
|
|
textAlign: TextAlign.end,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(context).pop(),
|
|
child: Text(l10n.closeAction),
|
|
),
|
|
FilledButton(
|
|
onPressed: () => Navigator.of(context).pop(
|
|
VoiceSettingsResult(
|
|
mode: _mode,
|
|
releaseTailMs: _releaseTail.round(),
|
|
bindKeyRequested: false,
|
|
),
|
|
),
|
|
child: Text(l10n.pttConfigureSaveAction),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|