diff --git a/apps/chanora_flutter/lib/main.dart b/apps/chanora_flutter/lib/main.dart index 4ed1b1b..b592931 100644 --- a/apps/chanora_flutter/lib/main.dart +++ b/apps/chanora_flutter/lib/main.dart @@ -855,7 +855,6 @@ class _BetaHomeState extends State<_BetaHome> { pttBoundKeyLabel: _pttBoundKeyLabel, onToggleMute: _onToggleHardMute, onConfigure: _onOpenVoiceSettings, - onBindKey: () => _onConfigurePtt(context), onLeave: _onLeaveVoice, ), const SizedBox(height: 12), @@ -999,187 +998,6 @@ class _BookmarkList extends StatelessWidget { } } -class _AudioControls extends StatefulWidget { - const _AudioControls({ - required this.stats, - required this.inputMuted, - required this.outputMuted, - required this.outputGain, - required this.pttLevel, - required this.pttBackendId, - required this.pttBoundInputClass, - required this.pttBoundKeyLabel, - required this.onPttDown, - required this.onPttUp, - required this.onToggleInputMute, - required this.onToggleOutputMute, - required this.onGainChanged, - required this.onConfigurePtt, - }); - - final rust.BridgeAudioStats? stats; - final bool inputMuted; - final bool outputMuted; - final double outputGain; - final String pttLevel; - final String pttBackendId; - final String pttBoundInputClass; - final String pttBoundKeyLabel; - final VoidCallback onPttDown; - final VoidCallback onPttUp; - final VoidCallback onToggleInputMute; - final VoidCallback onToggleOutputMute; - final ValueChanged onGainChanged; - final VoidCallback onConfigurePtt; - - @override - State<_AudioControls> createState() => _AudioControlsState(); -} - -class _AudioControlsState extends State<_AudioControls> { - bool _pressed = false; - - @override - Widget build(BuildContext context) { - final l10n = AppL10n.of(context); - final theme = Theme.of(context); - final stats = widget.stats; - final statsText = stats == null - ? '—' - : l10n.audioStatsLine( - stats.framesSent, - stats.framesReceived, - stats.pttActive ? 'on' : 'off', - ); - - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - // Capability badge (gen2 v0.9.3 / SDD-091). Renders the - // active PTT level + backend so the user understands when - // Global PTT has fallen back to Focused PTT, and surfaces - // a per-platform explanation sheet when the resolved - // capability is `L0Focused`. - PttCapabilityBadge( - level: widget.pttLevel, - backendId: widget.pttBackendId, - boundInputClass: widget.pttBoundInputClass, - boundKeyLabel: widget.pttBoundKeyLabel, - onConfigure: widget.onConfigurePtt, - ), - // Accessibility (SysRS-262 + SysRS-282 + SysRS-263): - // wrap the custom Listener-based PTT control in a - // `Semantics` node so screen readers announce its role - // ("button") and its current state ("Transmitting" / - // "Hold to talk"). The icon + text inside already - // communicate the state without relying on colour - // alone. - Semantics( - button: true, - enabled: true, - toggled: _pressed, - label: _pressed ? l10n.pttTransmitting : l10n.pttHoldToTalk, - hint: l10n.pttHoldToTalkSemanticsHint, - excludeSemantics: true, - child: Listener( - onPointerDown: (_) { - setState(() => _pressed = true); - widget.onPttDown(); - }, - onPointerUp: (_) { - setState(() => _pressed = false); - widget.onPttUp(); - }, - onPointerCancel: (_) { - setState(() => _pressed = false); - widget.onPttUp(); - }, - child: Container( - padding: const EdgeInsets.symmetric(vertical: 16), - decoration: BoxDecoration( - color: _pressed - ? theme.colorScheme.primary - : theme.colorScheme.primaryContainer, - borderRadius: BorderRadius.circular(12), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - _pressed ? Icons.mic : Icons.mic_off, - color: _pressed - ? theme.colorScheme.onPrimary - : theme.colorScheme.onPrimaryContainer, - ), - const SizedBox(width: 8), - Text( - _pressed ? l10n.pttTransmitting : l10n.pttHoldToTalk, - style: TextStyle( - color: _pressed - ? theme.colorScheme.onPrimary - : theme.colorScheme.onPrimaryContainer, - fontWeight: FontWeight.w600, - ), - ), - ], - ), - ), - ), - ), - const SizedBox(height: 8), - Row( - children: [ - Expanded( - child: FilterChip( - avatar: Icon( - widget.inputMuted ? Icons.mic_off : Icons.mic, - size: 18, - ), - label: Text( - widget.inputMuted ? l10n.inputUnmuteAction : l10n.inputMuteAction, - ), - selected: widget.inputMuted, - onSelected: (_) => widget.onToggleInputMute(), - ), - ), - const SizedBox(width: 8), - Expanded( - child: FilterChip( - avatar: Icon( - widget.outputMuted ? Icons.volume_off : Icons.volume_up, - size: 18, - ), - label: Text( - widget.outputMuted ? l10n.outputUnmuteAction : l10n.outputMuteAction, - ), - selected: widget.outputMuted, - onSelected: (_) => widget.onToggleOutputMute(), - ), - ), - ], - ), - const SizedBox(height: 4), - Row( - children: [ - const Icon(Icons.volume_down, size: 18), - Expanded( - child: Slider( - value: widget.outputGain.clamp(0.0, 2.0), - min: 0.0, - max: 2.0, - divisions: 40, - label: '${(widget.outputGain * 100).round()}%', - onChanged: widget.onGainChanged, - ), - ), - const Icon(Icons.volume_up, size: 18), - ], - ), - Text(statsText, style: theme.textTheme.bodySmall), - ], - ); - } -} /// PTT capability badge (gen2 v0.9.3 / SDD-091). /// @@ -1199,8 +1017,6 @@ class PttCapabilityBadge extends StatelessWidget { required this.level, required this.backendId, required this.boundInputClass, - required this.boundKeyLabel, - required this.onConfigure, }); /// Resolved capability level as the bridge emits it @@ -1215,17 +1031,6 @@ class PttCapabilityBadge extends StatelessWidget { /// or empty when no binding is set). final String boundInputClass; - /// Platform-neutral key label captured by the binding dialog - /// (e.g. `"Space"`, `"F10"`, `"mouse-side-button:8"`). Empty - /// when the user has not saved a binding in the current - /// process. Display-only; the bridge holds the authoritative - /// binding. Per DEC-027 this string is the same one already - /// crossed into the Rust side — no new privacy surface. - final String boundKeyLabel; - - /// Open the configure-binding dialog. Wired by the caller. - final VoidCallback onConfigure; - bool get _isFocused => level == 'L0Focused'; String _explainBodyForPlatform(AppL10n l10n) { @@ -1304,59 +1109,40 @@ class PttCapabilityBadge extends StatelessWidget { : '$badgeLabel\n($boundInputClass)'; return Padding( padding: const EdgeInsets.only(bottom: 6), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Tooltip( - message: tooltipMessage, - child: Row( - children: [ - Icon( - _isFocused ? Icons.crop_free : Icons.public, - size: 14, - color: theme.colorScheme.onSurfaceVariant, - ), - const SizedBox(width: 4), - Expanded( - child: Text( - badgeLabel, - style: theme.textTheme.bodySmall?.copyWith( - color: theme.colorScheme.onSurfaceVariant, - ), - ), - ), - if (_isFocused) - IconButton( - icon: const Icon(Icons.info_outline, size: 16), - tooltip: l10n.pttCapabilityExplainTitle, - visualDensity: VisualDensity.compact, - onPressed: () => _openExplanationSheet(context), - ), - TextButton.icon( - icon: const Icon(Icons.tune, size: 14), - label: Text(l10n.pttConfigureAction), - onPressed: onConfigure, - ), - ], + child: Tooltip( + message: tooltipMessage, + child: Row( + children: [ + Icon( + _isFocused ? Icons.crop_free : Icons.public, + size: 14, + color: theme.colorScheme.onSurfaceVariant, ), - ), - // Second line: show which key the user just bound, so - // they can remember what to press. Only rendered when a - // binding has been saved in the current process (the - // bridge holds the authoritative binding across restarts; - // this label is display-only). - if (boundKeyLabel.isNotEmpty) - Padding( - padding: const EdgeInsets.only(left: 18, top: 2), + const SizedBox(width: 4), + Expanded( child: Text( - l10n.pttCapabilityBoundKey(boundKeyLabel), + badgeLabel, style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, - fontFamily: 'monospace', ), ), ), - ], + // Info icon only for L0Focused — the explanation sheet + // tells the user why their PTT may not work outside the + // app window and how to grant the permission. There is + // intentionally NO 'Configure' button here: the single + // configuration entry point is the Voice Bar's + // settings gear (onConfigure on `VoiceBar`). Having two + // identical bind-key entry points just confuses users. + if (_isFocused) + IconButton( + icon: const Icon(Icons.info_outline, size: 16), + tooltip: l10n.pttCapabilityExplainTitle, + visualDensity: VisualDensity.compact, + onPressed: () => _openExplanationSheet(context), + ), + ], + ), ), ); } diff --git a/apps/chanora_flutter/lib/widgets/voice_bar.dart b/apps/chanora_flutter/lib/widgets/voice_bar.dart index 7941035..414f70e 100644 --- a/apps/chanora_flutter/lib/widgets/voice_bar.dart +++ b/apps/chanora_flutter/lib/widgets/voice_bar.dart @@ -27,7 +27,6 @@ class VoiceBar extends StatelessWidget { required this.pttBoundKeyLabel, required this.onToggleMute, required this.onConfigure, - required this.onBindKey, required this.onLeave, }); @@ -68,14 +67,12 @@ class VoiceBar extends StatelessWidget { /// Toggle the hard-mute clamp. final VoidCallback onToggleMute; - /// Open the voice settings dialog (mode + release tail). + /// Open the voice settings dialog. This is the SINGLE entry point + /// for transmit-mode selection, PTT key binding, and release-tail + /// configuration. The capability badge below is information-only + /// and intentionally does NOT have its own configure affordance. final VoidCallback onConfigure; - /// Open the bind-key capture flow directly (skips the settings - /// dialog). Surfaced from the capability badge when PTT mode is - /// active. - final VoidCallback onBindKey; - /// Leave the voice channel. final VoidCallback onLeave; @@ -208,13 +205,15 @@ class VoiceBar extends StatelessWidget { // PTT capability badge — only relevant when PTT mode is // active. Hidden for Continuous / Voice Activity since // there's no key binding to surface a capability for. + // The badge is information-only; the user reaches the + // bind-key flow through the Voice Bar's settings gear + // (single configuration entry point — see the comment + // on `onConfigure`). if (isPtt) PttCapabilityBadge( level: pttLevel, backendId: pttBackendId, boundInputClass: pttBoundInputClass, - boundKeyLabel: pttBoundKeyLabel, - onConfigure: onBindKey, ), if (inChannel) ...[ const SizedBox(height: 6),