// Voice settings dialog (SDD-097). Surfaces a TransmitMode radio // group, a bind-key button, and a release-tail slider. import 'dart:io' show Platform; import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; import '../l10n/generated/app_localizations.dart'; import '../src/rust/api.dart' as rust; /// True when the host is a touch-only mobile platform without a /// hardware keyboard the user would bind a PTT key on. Mirrors the /// helper in `voice_bar.dart`. bool get _isTouchOnlyPttHost { if (kIsWeb) return false; return Platform.isIOS || Platform.isAndroid; } /// 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 createState() => _VoiceSettingsDialogState(); } class _VoiceSettingsDialogState extends State { 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( dense: true, value: rust.BridgeTransmitMode.ptt, groupValue: _mode, title: Text(l10n.voiceModePtt), onChanged: (v) => setState(() => _mode = v!), ), RadioListTile( dense: true, value: rust.BridgeTransmitMode.continuous, groupValue: _mode, title: Text(l10n.voiceModeContinuous), onChanged: (v) => setState(() => _mode = v!), ), RadioListTile( 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. // // Additionally on touch-only mobile hosts (iOS / iPadOS // / Android) there is no hardware keyboard to bind a // key on — the VoiceBar renders an on-screen Push to // Talk button instead. Hide the Bind Key affordance // there but keep the release-tail slider since it // still applies to the on-screen button's behaviour. if (_mode == rust.BridgeTransmitMode.ptt) ...[ if (!_isTouchOnlyPttHost) ...[ 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), ), ], ); } }