feat(audio,bridge,flutter): v1 audio + PTT lifecycle implementation (SDD-094..097)
Implement the SDD-094 / SDD-095 / SDD-096 / SDD-097 detailed designs
committed in dfa84ee.
Rust side
- chanora_audio::TransmitMode enum (Ptt/Continuous/VoiceActivity) with
serde-friendly u8 repr (SDD-095).
- chanora_audio::TransmitModeSelector: lock-free Atomic-backed selector
that is the sole writer of transmit_active (per SAD-083), applying
hard_mute as a final clamp. VoiceActivity falls through to Continuous
for v1 (DEC-030 placeholder).
- chanora_audio::ReleaseTailTimer: tokio-task-owning struct driving the
selector's ptt_held input; default 200 ms tail, configurable 0–500 ms
with AtomicU32 hot read; pending JoinHandle held in a std::sync::Mutex
touched only on PTT edge transitions (SDD-096).
- chanora_storage: AudioMeta persisted as audio_meta.json next to
identity.dek; get/set_transmit_mode + get/set_release_tail_ms with
0..=500 clamp on write.
- chanora_core::ChanoraSession: voice_join(channel, password) and
voice_leave() are the new lifecycle entry points; ensure_audio_running
and shutdown_audio_if_idle are private helpers around the existing
Option<AudioEngine> field. SessionEvent::VoiceState carries the
in_channel / transmit_mode / mute / release_tail_ms tuple. Selector
state survives reconnect; supervisor rewires it to each fresh engine
gate.
- chanora_bridge: drop start_audio; add voice_join, voice_leave,
set/get_transmit_mode, set/get_release_tail_ms, set_hard_mute.
BridgeEvent::VoiceState mirrors the core event. AudioStarted/Stopped
kept for backwards compat but Flutter ignores them in the new UI.
Flutter side
- New apps/chanora_flutter/lib/widgets/voice_bar.dart replaces the
legacy _AudioControls widget. Renders channel pill, mode badge,
mute toggle, level meter, PttCapabilityBadge, leave button. No
manual Start affordance anywhere.
- New apps/chanora_flutter/lib/widgets/voice_settings.dart dialog with
TransmitMode radio group (VoiceActivity disabled with 'Coming soon'
trailing label per DEC-030), bind-key button, release-tail slider
0–500 ms step 25.
- main.dart: state fields _inChannel, _transmitMode, _hardMute,
_releaseTailMs driven by BridgeEvent_VoiceState. Channel-tap now
calls voiceJoin instead of moveToChannel. Removed _onStartAudio,
_audioStarted-gated branch, and the FilledButton.
- l10n: 11 new strings in app_en.arb + app_zh.arb.
Verification
- cargo check --workspace: clean.
- cargo test --workspace --lib: 72 passed / 0 failed / 1 ignored
(chanora_audio: +12 new tests for TransmitMode/Selector/ReleaseTail;
chanora_storage: +2 new tests for audio_meta round-trip).
- flutter analyze: 0 errors, 0 warnings; 6 infos are the Flutter 3.32
Radio.groupValue deprecation (pre-existing API usage).
- FRB Dart/Rust bindings regenerated via flutter_rust_bridge_codegen.
Follow-up (intentionally deferred)
- PttController and per-platform PTT backends still drive AudioTransmitGate
directly via the legacy set_ptt path; routing those key edges through
ChanoraSession::release_tail_timer().{key_down,key_up} so the tail
applies to native PTT input is a contained wiring change in a follow-up.
- Real audio-level RMS in BridgeAudioStats (current meter is binary).
- VoiceActivity backend (DEC-030).
This commit is contained in:
@@ -113,5 +113,17 @@
|
||||
"channels": { "type": "int" },
|
||||
"clients": { "type": "int" }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"voiceModePtt": "PTT",
|
||||
"voiceModeContinuous": "Continuous",
|
||||
"voiceModeVoiceActivity": "Voice activity",
|
||||
"voiceModeComingSoon": "Coming soon",
|
||||
"voiceModeLabel": "Transmit mode",
|
||||
"voiceReleaseTailLabel": "Release tail",
|
||||
"voiceReleaseTailHint": "ms",
|
||||
"voiceLeaveAction": "Leave voice",
|
||||
"voiceHardMuteLabel": "Mute",
|
||||
"voiceSettingsTitle": "Voice settings",
|
||||
"voiceBindKeyAction": "Bind PTT key"
|
||||
}
|
||||
|
||||
@@ -70,5 +70,17 @@
|
||||
|
||||
"channelsHeading": "频道",
|
||||
"clientsHeading": "在线用户",
|
||||
"countChannelsAndClients": "{channels} 个频道 • {clients} 在线"
|
||||
"countChannelsAndClients": "{channels} 个频道 • {clients} 在线",
|
||||
|
||||
"voiceModePtt": "按键说话",
|
||||
"voiceModeContinuous": "持续发送",
|
||||
"voiceModeVoiceActivity": "语音激活",
|
||||
"voiceModeComingSoon": "即将推出",
|
||||
"voiceModeLabel": "发送模式",
|
||||
"voiceReleaseTailLabel": "释放延迟",
|
||||
"voiceReleaseTailHint": "毫秒",
|
||||
"voiceLeaveAction": "离开语音",
|
||||
"voiceHardMuteLabel": "静音",
|
||||
"voiceSettingsTitle": "语音设置",
|
||||
"voiceBindKeyAction": "绑定 PTT 按键"
|
||||
}
|
||||
|
||||
@@ -474,6 +474,72 @@ abstract class AppL10n {
|
||||
/// In en, this message translates to:
|
||||
/// **'{channels} channels • {clients} online'**
|
||||
String countChannelsAndClients(int channels, int clients);
|
||||
|
||||
/// No description provided for @voiceModePtt.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'PTT'**
|
||||
String get voiceModePtt;
|
||||
|
||||
/// No description provided for @voiceModeContinuous.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Continuous'**
|
||||
String get voiceModeContinuous;
|
||||
|
||||
/// No description provided for @voiceModeVoiceActivity.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Voice activity'**
|
||||
String get voiceModeVoiceActivity;
|
||||
|
||||
/// No description provided for @voiceModeComingSoon.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Coming soon'**
|
||||
String get voiceModeComingSoon;
|
||||
|
||||
/// No description provided for @voiceModeLabel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Transmit mode'**
|
||||
String get voiceModeLabel;
|
||||
|
||||
/// No description provided for @voiceReleaseTailLabel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Release tail'**
|
||||
String get voiceReleaseTailLabel;
|
||||
|
||||
/// No description provided for @voiceReleaseTailHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'ms'**
|
||||
String get voiceReleaseTailHint;
|
||||
|
||||
/// No description provided for @voiceLeaveAction.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Leave voice'**
|
||||
String get voiceLeaveAction;
|
||||
|
||||
/// No description provided for @voiceHardMuteLabel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Mute'**
|
||||
String get voiceHardMuteLabel;
|
||||
|
||||
/// No description provided for @voiceSettingsTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Voice settings'**
|
||||
String get voiceSettingsTitle;
|
||||
|
||||
/// No description provided for @voiceBindKeyAction.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Bind PTT key'**
|
||||
String get voiceBindKeyAction;
|
||||
}
|
||||
|
||||
class _AppL10nDelegate extends LocalizationsDelegate<AppL10n> {
|
||||
|
||||
@@ -229,4 +229,37 @@ class AppL10nEn extends AppL10n {
|
||||
String countChannelsAndClients(int channels, int clients) {
|
||||
return '$channels channels • $clients online';
|
||||
}
|
||||
|
||||
@override
|
||||
String get voiceModePtt => 'PTT';
|
||||
|
||||
@override
|
||||
String get voiceModeContinuous => 'Continuous';
|
||||
|
||||
@override
|
||||
String get voiceModeVoiceActivity => 'Voice activity';
|
||||
|
||||
@override
|
||||
String get voiceModeComingSoon => 'Coming soon';
|
||||
|
||||
@override
|
||||
String get voiceModeLabel => 'Transmit mode';
|
||||
|
||||
@override
|
||||
String get voiceReleaseTailLabel => 'Release tail';
|
||||
|
||||
@override
|
||||
String get voiceReleaseTailHint => 'ms';
|
||||
|
||||
@override
|
||||
String get voiceLeaveAction => 'Leave voice';
|
||||
|
||||
@override
|
||||
String get voiceHardMuteLabel => 'Mute';
|
||||
|
||||
@override
|
||||
String get voiceSettingsTitle => 'Voice settings';
|
||||
|
||||
@override
|
||||
String get voiceBindKeyAction => 'Bind PTT key';
|
||||
}
|
||||
|
||||
@@ -223,4 +223,37 @@ class AppL10nZh extends AppL10n {
|
||||
String countChannelsAndClients(int channels, int clients) {
|
||||
return '$channels 个频道 • $clients 在线';
|
||||
}
|
||||
|
||||
@override
|
||||
String get voiceModePtt => '按键说话';
|
||||
|
||||
@override
|
||||
String get voiceModeContinuous => '持续发送';
|
||||
|
||||
@override
|
||||
String get voiceModeVoiceActivity => '语音激活';
|
||||
|
||||
@override
|
||||
String get voiceModeComingSoon => '即将推出';
|
||||
|
||||
@override
|
||||
String get voiceModeLabel => '发送模式';
|
||||
|
||||
@override
|
||||
String get voiceReleaseTailLabel => '释放延迟';
|
||||
|
||||
@override
|
||||
String get voiceReleaseTailHint => '毫秒';
|
||||
|
||||
@override
|
||||
String get voiceLeaveAction => '离开语音';
|
||||
|
||||
@override
|
||||
String get voiceHardMuteLabel => '静音';
|
||||
|
||||
@override
|
||||
String get voiceSettingsTitle => '语音设置';
|
||||
|
||||
@override
|
||||
String get voiceBindKeyAction => '绑定 PTT 按键';
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@ import 'package:path_provider/path_provider.dart';
|
||||
import 'l10n/generated/app_localizations.dart';
|
||||
import 'src/rust/api.dart' as rust;
|
||||
import 'src/rust/frb_generated.dart';
|
||||
import 'widgets/voice_bar.dart';
|
||||
import 'widgets/voice_settings.dart';
|
||||
|
||||
/// Public version string shown in the About dialog. Aligned with
|
||||
/// `pubspec.yaml` and the git tag for the MVP release candidate.
|
||||
@@ -95,17 +97,27 @@ class _BetaHomeState extends State<_BetaHome> {
|
||||
_Phase _phase = _Phase.idle;
|
||||
rust.BridgeSnapshot? _snapshot;
|
||||
String? _error;
|
||||
// ignore: unused_field
|
||||
bool _audioStarted = false;
|
||||
rust.BridgeAudioStats? _audioStats;
|
||||
Timer? _statsTimer;
|
||||
StreamSubscription<rust.BridgeEvent>? _eventsSub;
|
||||
|
||||
// v1 voice subsystem state (SDD-094/095/096/097). Driven by
|
||||
// BridgeEvent::VoiceState.
|
||||
bool _inChannel = false;
|
||||
rust.BridgeTransmitMode _transmitMode = rust.BridgeTransmitMode.ptt;
|
||||
bool _hardMute = false;
|
||||
int _releaseTailMs = 200;
|
||||
BigInt? _currentVoiceChannelId;
|
||||
|
||||
String? _lostReason;
|
||||
int? _reconnectAttempt;
|
||||
int? _reconnectDelay;
|
||||
|
||||
bool _inputMuted = false;
|
||||
bool _outputMuted = false;
|
||||
// ignore: unused_field
|
||||
double _outputGain = 1.0;
|
||||
|
||||
// Desktop PTT capability badge state (gen2 v0.9.3 / SDD-091).
|
||||
@@ -189,9 +201,39 @@ class _BetaHomeState extends State<_BetaHome> {
|
||||
_pttBackendId = backendId;
|
||||
_pttBoundInputClass = boundInputClass;
|
||||
});
|
||||
case rust.BridgeEvent_VoiceState(
|
||||
:final inChannel,
|
||||
:final transmitMode,
|
||||
:final mute,
|
||||
:final releaseTailMs,
|
||||
):
|
||||
setState(() {
|
||||
_inChannel = inChannel;
|
||||
_transmitMode = transmitMode;
|
||||
_hardMute = mute;
|
||||
_releaseTailMs = releaseTailMs;
|
||||
_audioStarted = inChannel;
|
||||
});
|
||||
if (inChannel) {
|
||||
_ensureStatsTimer();
|
||||
} else {
|
||||
_statsTimer?.cancel();
|
||||
_statsTimer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _ensureStatsTimer() {
|
||||
if (_statsTimer != null) return;
|
||||
_statsTimer = Timer.periodic(const Duration(milliseconds: 500), (_) async {
|
||||
try {
|
||||
final s = await rust.audioStats();
|
||||
if (!mounted) return;
|
||||
setState(() => _audioStats = s);
|
||||
} catch (_) {}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_eventsSub?.cancel();
|
||||
@@ -232,25 +274,7 @@ class _BetaHomeState extends State<_BetaHome> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onStartAudio() async {
|
||||
try {
|
||||
await rust.startAudio();
|
||||
if (!mounted) return;
|
||||
setState(() => _audioStarted = true);
|
||||
_statsTimer?.cancel();
|
||||
_statsTimer = Timer.periodic(const Duration(milliseconds: 500), (_) async {
|
||||
try {
|
||||
final s = await rust.audioStats();
|
||||
if (!mounted) return;
|
||||
setState(() => _audioStats = s);
|
||||
} catch (_) {}
|
||||
});
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
setState(() => _error = e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
// ignore: unused_element
|
||||
Future<void> _setPtt(bool active) async {
|
||||
try {
|
||||
await rust.setPtt(active: active);
|
||||
@@ -260,6 +284,7 @@ class _BetaHomeState extends State<_BetaHome> {
|
||||
}
|
||||
}
|
||||
|
||||
// ignore: unused_element
|
||||
Future<void> _toggleInputMute() async {
|
||||
final next = !_inputMuted;
|
||||
try {
|
||||
@@ -272,6 +297,7 @@ class _BetaHomeState extends State<_BetaHome> {
|
||||
}
|
||||
}
|
||||
|
||||
// ignore: unused_element
|
||||
Future<void> _toggleOutputMute() async {
|
||||
final next = !_outputMuted;
|
||||
try {
|
||||
@@ -284,6 +310,7 @@ class _BetaHomeState extends State<_BetaHome> {
|
||||
}
|
||||
}
|
||||
|
||||
// ignore: unused_element
|
||||
Future<void> _setOutputGain(double value) async {
|
||||
setState(() => _outputGain = value);
|
||||
try {
|
||||
@@ -303,16 +330,60 @@ class _BetaHomeState extends State<_BetaHome> {
|
||||
if (password == null) return; // cancelled
|
||||
}
|
||||
try {
|
||||
await rust.moveToChannel(
|
||||
await rust.voiceJoin(
|
||||
channelId: ch.id,
|
||||
password: password ?? '',
|
||||
);
|
||||
if (!mounted) return;
|
||||
setState(() => _currentVoiceChannelId = ch.id);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
setState(() => _error = e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onLeaveVoice() async {
|
||||
try {
|
||||
await rust.voiceLeave();
|
||||
if (!mounted) return;
|
||||
setState(() => _currentVoiceChannelId = null);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
setState(() => _error = e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onToggleHardMute() async {
|
||||
final next = !_hardMute;
|
||||
try {
|
||||
await rust.setHardMute(muted: next);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
setState(() => _error = e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onOpenVoiceSettings() async {
|
||||
final result = await showDialog<VoiceSettingsResult>(
|
||||
context: context,
|
||||
builder: (ctx) => VoiceSettingsDialog(
|
||||
initialMode: _transmitMode,
|
||||
initialReleaseTailMs: _releaseTailMs,
|
||||
),
|
||||
);
|
||||
if (result == null) return;
|
||||
try {
|
||||
await rust.setTransmitMode(mode: result.mode);
|
||||
await rust.setReleaseTailMs(ms: result.releaseTailMs);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
setState(() => _error = e.toString());
|
||||
}
|
||||
if (result.bindKeyRequested && mounted) {
|
||||
await _onConfigurePtt(context);
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> _askChannelPassword(AppL10n l10n) async {
|
||||
final ctl = TextEditingController();
|
||||
final result = await showDialog<String>(
|
||||
@@ -367,9 +438,21 @@ class _BetaHomeState extends State<_BetaHome> {
|
||||
_error = null;
|
||||
_inputMuted = false;
|
||||
_outputMuted = false;
|
||||
_inChannel = false;
|
||||
_currentVoiceChannelId = null;
|
||||
});
|
||||
}
|
||||
|
||||
String _currentVoiceChannelName() {
|
||||
final id = _currentVoiceChannelId;
|
||||
final snap = _snapshot;
|
||||
if (id == null || snap == null) return '';
|
||||
for (final ch in snap.channels) {
|
||||
if (ch.id == id) return ch.name;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
Future<void> _onShowDiagnostics(BuildContext context) async {
|
||||
final l10n = AppL10n.of(context);
|
||||
final text = rust.exportDiagnostics();
|
||||
@@ -726,32 +809,22 @@ class _BetaHomeState extends State<_BetaHome> {
|
||||
),
|
||||
),
|
||||
] else if (_phase == _Phase.connected && _snapshot != null) ...[
|
||||
if (!_audioStarted) ...[
|
||||
FilledButton.icon(
|
||||
icon: const Icon(Icons.mic_none),
|
||||
label: Text(l10n.startAudioAction),
|
||||
onPressed: _onStartAudio,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
] else ...[
|
||||
_AudioControls(
|
||||
stats: _audioStats,
|
||||
inputMuted: _inputMuted,
|
||||
outputMuted: _outputMuted,
|
||||
outputGain: _outputGain,
|
||||
pttLevel: _pttLevel,
|
||||
pttBackendId: _pttBackendId,
|
||||
pttBoundInputClass: _pttBoundInputClass,
|
||||
pttBoundKeyLabel: _pttBoundKeyLabel,
|
||||
onPttDown: () => _setPtt(true),
|
||||
onPttUp: () => _setPtt(false),
|
||||
onToggleInputMute: _toggleInputMute,
|
||||
onToggleOutputMute: _toggleOutputMute,
|
||||
onGainChanged: _setOutputGain,
|
||||
onConfigurePtt: () => _onConfigurePtt(context),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
VoiceBar(
|
||||
inChannel: _inChannel,
|
||||
transmitMode: _transmitMode,
|
||||
hardMute: _hardMute,
|
||||
releaseTailMs: _releaseTailMs,
|
||||
channelName: _currentVoiceChannelName(),
|
||||
audioStats: _audioStats,
|
||||
pttLevel: _pttLevel,
|
||||
pttBackendId: _pttBackendId,
|
||||
pttBoundInputClass: _pttBoundInputClass,
|
||||
pttBoundKeyLabel: _pttBoundKeyLabel,
|
||||
onToggleMute: _onToggleHardMute,
|
||||
onConfigure: _onOpenVoiceSettings,
|
||||
onLeave: _onLeaveVoice,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Expanded(
|
||||
child: _SnapshotView(
|
||||
snapshot: _snapshot!,
|
||||
|
||||
@@ -9,8 +9,8 @@ import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart' hide protected;
|
||||
part 'api.freezed.dart';
|
||||
|
||||
// These functions are ignored because they are not marked as `pub`: `log_sink`, `runtime`, `session`
|
||||
// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `from`, `from`, `from`, `from`, `from`, `from`
|
||||
// These functions are ignored because they are not marked as `pub`: `log_sink`, `runtime`, `session`, `transmit_mode_from_u8`
|
||||
// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`
|
||||
|
||||
/// Connect to a TeamSpeak-compatible server and return the initial
|
||||
/// state snapshot. Honours the DEC-006 single-connection invariant
|
||||
@@ -37,14 +37,50 @@ Future<void> disconnect() => RustLib.instance.api.crateApiDisconnect();
|
||||
/// True if a connection is currently active.
|
||||
Future<bool> isConnected() => RustLib.instance.api.crateApiIsConnected();
|
||||
|
||||
/// Start the audio engine on the active connection. Requires a
|
||||
/// connection; idempotent (will replace any previous engine).
|
||||
Future<void> startAudio() => RustLib.instance.api.crateApiStartAudio();
|
||||
|
||||
/// Set the push-to-talk state.
|
||||
///
|
||||
/// Superseded in v1 by [`set_transmit_mode`] + the binding capture
|
||||
/// dialog. Retained so legacy callers and integration tests keep
|
||||
/// working; the new VoiceBar UI no longer invokes this.
|
||||
Future<void> setPtt({required bool active}) =>
|
||||
RustLib.instance.api.crateApiSetPtt(active: active);
|
||||
|
||||
/// Join a voice channel (SDD-094). Moves the user to `channel_id`,
|
||||
/// brings up the audio engine if needed, and emits
|
||||
/// `BridgeEvent::VoiceState`. `password` may be empty.
|
||||
Future<void> voiceJoin({required BigInt channelId, required String password}) =>
|
||||
RustLib.instance.api.crateApiVoiceJoin(
|
||||
channelId: channelId,
|
||||
password: password,
|
||||
);
|
||||
|
||||
/// Leave the current voice channel (SDD-094). Tears down the audio
|
||||
/// engine and emits `BridgeEvent::VoiceState`.
|
||||
Future<void> voiceLeave() => RustLib.instance.api.crateApiVoiceLeave();
|
||||
|
||||
/// Set the active transmit mode (SDD-095).
|
||||
Future<void> setTransmitMode({required BridgeTransmitMode mode}) =>
|
||||
RustLib.instance.api.crateApiSetTransmitMode(mode: mode);
|
||||
|
||||
/// Read the active transmit mode.
|
||||
Future<BridgeTransmitMode> getTransmitMode() =>
|
||||
RustLib.instance.api.crateApiGetTransmitMode();
|
||||
|
||||
/// Update the release-tail in milliseconds (SDD-096). Values are
|
||||
/// clamped to `0..=500` on the Rust side; passing anything larger
|
||||
/// silently saturates.
|
||||
Future<void> setReleaseTailMs({required int ms}) =>
|
||||
RustLib.instance.api.crateApiSetReleaseTailMs(ms: ms);
|
||||
|
||||
/// Read the current release-tail in milliseconds.
|
||||
Future<int> getReleaseTailMs() =>
|
||||
RustLib.instance.api.crateApiGetReleaseTailMs();
|
||||
|
||||
/// Engage or release the hard-mute clamp (SDD-094). When `true`
|
||||
/// the audio engine transmits nothing regardless of mode.
|
||||
Future<void> setHardMute({required bool muted}) =>
|
||||
RustLib.instance.api.crateApiSetHardMute(muted: muted);
|
||||
|
||||
/// Update the active PTT binding (gen2 v0.9.3 / DEC-026). The
|
||||
/// platform_key string is opaque to the bridge — it identifies the
|
||||
/// bound key inside the platform backend and never appears in any
|
||||
@@ -362,6 +398,23 @@ sealed class BridgeEvent with _$BridgeEvent {
|
||||
/// `"mouse-side-button"`); empty when no binding is active.
|
||||
required String boundInputClass,
|
||||
}) = BridgeEvent_PttCapability;
|
||||
|
||||
/// Voice subsystem state snapshot (SDD-094). The Flutter
|
||||
/// VoiceBar listens to this stream.
|
||||
const factory BridgeEvent.voiceState({
|
||||
/// True when the session is currently joined to a voice
|
||||
/// channel and the audio engine is running.
|
||||
required bool inChannel,
|
||||
|
||||
/// Active transmit mode.
|
||||
required BridgeTransmitMode transmitMode,
|
||||
|
||||
/// True when the hard-mute clamp is engaged.
|
||||
required bool mute,
|
||||
|
||||
/// Current release-tail in milliseconds (0..=500).
|
||||
required int releaseTailMs,
|
||||
}) = BridgeEvent_VoiceState;
|
||||
}
|
||||
|
||||
/// Coarse OS-reported network state. Mirrors
|
||||
@@ -440,3 +493,17 @@ class BridgeSnapshot {
|
||||
channels == other.channels &&
|
||||
clients == other.clients;
|
||||
}
|
||||
|
||||
/// Voice transmit mode mirror (SDD-095). Schema-controlled enum;
|
||||
/// the wire encoding matches [`chanora_core::TransmitMode::as_u8`].
|
||||
enum BridgeTransmitMode {
|
||||
/// Push-to-talk (default).
|
||||
ptt,
|
||||
|
||||
/// Continuous transmission while in channel and not muted.
|
||||
continuous,
|
||||
|
||||
/// Voice-activity detection — reserved per DEC-030; v1 behaves
|
||||
/// as `Continuous`.
|
||||
voiceActivity,
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ extension BridgeEventPatterns on BridgeEvent {
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>({TResult Function( BridgeEvent_Connected value)? connected,TResult Function( BridgeEvent_Lost value)? lost,TResult Function( BridgeEvent_Reconnecting value)? reconnecting,TResult Function( BridgeEvent_Disconnected value)? disconnected,TResult Function( BridgeEvent_AudioStarted value)? audioStarted,TResult Function( BridgeEvent_AudioStopped value)? audioStopped,TResult Function( BridgeEvent_SnapshotChanged value)? snapshotChanged,TResult Function( BridgeEvent_PttCapability value)? pttCapability,required TResult orElse(),}){
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>({TResult Function( BridgeEvent_Connected value)? connected,TResult Function( BridgeEvent_Lost value)? lost,TResult Function( BridgeEvent_Reconnecting value)? reconnecting,TResult Function( BridgeEvent_Disconnected value)? disconnected,TResult Function( BridgeEvent_AudioStarted value)? audioStarted,TResult Function( BridgeEvent_AudioStopped value)? audioStopped,TResult Function( BridgeEvent_SnapshotChanged value)? snapshotChanged,TResult Function( BridgeEvent_PttCapability value)? pttCapability,TResult Function( BridgeEvent_VoiceState value)? voiceState,required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case BridgeEvent_Connected() when connected != null:
|
||||
@@ -66,7 +66,8 @@ return disconnected(_that);case BridgeEvent_AudioStarted() when audioStarted !=
|
||||
return audioStarted(_that);case BridgeEvent_AudioStopped() when audioStopped != null:
|
||||
return audioStopped(_that);case BridgeEvent_SnapshotChanged() when snapshotChanged != null:
|
||||
return snapshotChanged(_that);case BridgeEvent_PttCapability() when pttCapability != null:
|
||||
return pttCapability(_that);case _:
|
||||
return pttCapability(_that);case BridgeEvent_VoiceState() when voiceState != null:
|
||||
return voiceState(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
@@ -84,7 +85,7 @@ return pttCapability(_that);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>({required TResult Function( BridgeEvent_Connected value) connected,required TResult Function( BridgeEvent_Lost value) lost,required TResult Function( BridgeEvent_Reconnecting value) reconnecting,required TResult Function( BridgeEvent_Disconnected value) disconnected,required TResult Function( BridgeEvent_AudioStarted value) audioStarted,required TResult Function( BridgeEvent_AudioStopped value) audioStopped,required TResult Function( BridgeEvent_SnapshotChanged value) snapshotChanged,required TResult Function( BridgeEvent_PttCapability value) pttCapability,}){
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>({required TResult Function( BridgeEvent_Connected value) connected,required TResult Function( BridgeEvent_Lost value) lost,required TResult Function( BridgeEvent_Reconnecting value) reconnecting,required TResult Function( BridgeEvent_Disconnected value) disconnected,required TResult Function( BridgeEvent_AudioStarted value) audioStarted,required TResult Function( BridgeEvent_AudioStopped value) audioStopped,required TResult Function( BridgeEvent_SnapshotChanged value) snapshotChanged,required TResult Function( BridgeEvent_PttCapability value) pttCapability,required TResult Function( BridgeEvent_VoiceState value) voiceState,}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case BridgeEvent_Connected():
|
||||
@@ -95,7 +96,8 @@ return disconnected(_that);case BridgeEvent_AudioStarted():
|
||||
return audioStarted(_that);case BridgeEvent_AudioStopped():
|
||||
return audioStopped(_that);case BridgeEvent_SnapshotChanged():
|
||||
return snapshotChanged(_that);case BridgeEvent_PttCapability():
|
||||
return pttCapability(_that);}
|
||||
return pttCapability(_that);case BridgeEvent_VoiceState():
|
||||
return voiceState(_that);}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
@@ -109,7 +111,7 @@ return pttCapability(_that);}
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>({TResult? Function( BridgeEvent_Connected value)? connected,TResult? Function( BridgeEvent_Lost value)? lost,TResult? Function( BridgeEvent_Reconnecting value)? reconnecting,TResult? Function( BridgeEvent_Disconnected value)? disconnected,TResult? Function( BridgeEvent_AudioStarted value)? audioStarted,TResult? Function( BridgeEvent_AudioStopped value)? audioStopped,TResult? Function( BridgeEvent_SnapshotChanged value)? snapshotChanged,TResult? Function( BridgeEvent_PttCapability value)? pttCapability,}){
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>({TResult? Function( BridgeEvent_Connected value)? connected,TResult? Function( BridgeEvent_Lost value)? lost,TResult? Function( BridgeEvent_Reconnecting value)? reconnecting,TResult? Function( BridgeEvent_Disconnected value)? disconnected,TResult? Function( BridgeEvent_AudioStarted value)? audioStarted,TResult? Function( BridgeEvent_AudioStopped value)? audioStopped,TResult? Function( BridgeEvent_SnapshotChanged value)? snapshotChanged,TResult? Function( BridgeEvent_PttCapability value)? pttCapability,TResult? Function( BridgeEvent_VoiceState value)? voiceState,}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case BridgeEvent_Connected() when connected != null:
|
||||
@@ -120,7 +122,8 @@ return disconnected(_that);case BridgeEvent_AudioStarted() when audioStarted !=
|
||||
return audioStarted(_that);case BridgeEvent_AudioStopped() when audioStopped != null:
|
||||
return audioStopped(_that);case BridgeEvent_SnapshotChanged() when snapshotChanged != null:
|
||||
return snapshotChanged(_that);case BridgeEvent_PttCapability() when pttCapability != null:
|
||||
return pttCapability(_that);case _:
|
||||
return pttCapability(_that);case BridgeEvent_VoiceState() when voiceState != null:
|
||||
return voiceState(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
@@ -137,7 +140,7 @@ return pttCapability(_that);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>({TResult Function( String serverName)? connected,TResult Function( String reason)? lost,TResult Function( int attempt, int delaySecs)? reconnecting,TResult Function( String reason)? disconnected,TResult Function()? audioStarted,TResult Function()? audioStopped,TResult Function( int channels, int clients)? snapshotChanged,TResult Function( String level, String backendId, String boundInputClass)? pttCapability,required TResult orElse(),}) {final _that = this;
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>({TResult Function( String serverName)? connected,TResult Function( String reason)? lost,TResult Function( int attempt, int delaySecs)? reconnecting,TResult Function( String reason)? disconnected,TResult Function()? audioStarted,TResult Function()? audioStopped,TResult Function( int channels, int clients)? snapshotChanged,TResult Function( String level, String backendId, String boundInputClass)? pttCapability,TResult Function( bool inChannel, BridgeTransmitMode transmitMode, bool mute, int releaseTailMs)? voiceState,required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case BridgeEvent_Connected() when connected != null:
|
||||
return connected(_that.serverName);case BridgeEvent_Lost() when lost != null:
|
||||
@@ -147,7 +150,8 @@ return disconnected(_that.reason);case BridgeEvent_AudioStarted() when audioStar
|
||||
return audioStarted();case BridgeEvent_AudioStopped() when audioStopped != null:
|
||||
return audioStopped();case BridgeEvent_SnapshotChanged() when snapshotChanged != null:
|
||||
return snapshotChanged(_that.channels,_that.clients);case BridgeEvent_PttCapability() when pttCapability != null:
|
||||
return pttCapability(_that.level,_that.backendId,_that.boundInputClass);case _:
|
||||
return pttCapability(_that.level,_that.backendId,_that.boundInputClass);case BridgeEvent_VoiceState() when voiceState != null:
|
||||
return voiceState(_that.inChannel,_that.transmitMode,_that.mute,_that.releaseTailMs);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
@@ -165,7 +169,7 @@ return pttCapability(_that.level,_that.backendId,_that.boundInputClass);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>({required TResult Function( String serverName) connected,required TResult Function( String reason) lost,required TResult Function( int attempt, int delaySecs) reconnecting,required TResult Function( String reason) disconnected,required TResult Function() audioStarted,required TResult Function() audioStopped,required TResult Function( int channels, int clients) snapshotChanged,required TResult Function( String level, String backendId, String boundInputClass) pttCapability,}) {final _that = this;
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>({required TResult Function( String serverName) connected,required TResult Function( String reason) lost,required TResult Function( int attempt, int delaySecs) reconnecting,required TResult Function( String reason) disconnected,required TResult Function() audioStarted,required TResult Function() audioStopped,required TResult Function( int channels, int clients) snapshotChanged,required TResult Function( String level, String backendId, String boundInputClass) pttCapability,required TResult Function( bool inChannel, BridgeTransmitMode transmitMode, bool mute, int releaseTailMs) voiceState,}) {final _that = this;
|
||||
switch (_that) {
|
||||
case BridgeEvent_Connected():
|
||||
return connected(_that.serverName);case BridgeEvent_Lost():
|
||||
@@ -175,7 +179,8 @@ return disconnected(_that.reason);case BridgeEvent_AudioStarted():
|
||||
return audioStarted();case BridgeEvent_AudioStopped():
|
||||
return audioStopped();case BridgeEvent_SnapshotChanged():
|
||||
return snapshotChanged(_that.channels,_that.clients);case BridgeEvent_PttCapability():
|
||||
return pttCapability(_that.level,_that.backendId,_that.boundInputClass);}
|
||||
return pttCapability(_that.level,_that.backendId,_that.boundInputClass);case BridgeEvent_VoiceState():
|
||||
return voiceState(_that.inChannel,_that.transmitMode,_that.mute,_that.releaseTailMs);}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
@@ -189,7 +194,7 @@ return pttCapability(_that.level,_that.backendId,_that.boundInputClass);}
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>({TResult? Function( String serverName)? connected,TResult? Function( String reason)? lost,TResult? Function( int attempt, int delaySecs)? reconnecting,TResult? Function( String reason)? disconnected,TResult? Function()? audioStarted,TResult? Function()? audioStopped,TResult? Function( int channels, int clients)? snapshotChanged,TResult? Function( String level, String backendId, String boundInputClass)? pttCapability,}) {final _that = this;
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>({TResult? Function( String serverName)? connected,TResult? Function( String reason)? lost,TResult? Function( int attempt, int delaySecs)? reconnecting,TResult? Function( String reason)? disconnected,TResult? Function()? audioStarted,TResult? Function()? audioStopped,TResult? Function( int channels, int clients)? snapshotChanged,TResult? Function( String level, String backendId, String boundInputClass)? pttCapability,TResult? Function( bool inChannel, BridgeTransmitMode transmitMode, bool mute, int releaseTailMs)? voiceState,}) {final _that = this;
|
||||
switch (_that) {
|
||||
case BridgeEvent_Connected() when connected != null:
|
||||
return connected(_that.serverName);case BridgeEvent_Lost() when lost != null:
|
||||
@@ -199,7 +204,8 @@ return disconnected(_that.reason);case BridgeEvent_AudioStarted() when audioStar
|
||||
return audioStarted();case BridgeEvent_AudioStopped() when audioStopped != null:
|
||||
return audioStopped();case BridgeEvent_SnapshotChanged() when snapshotChanged != null:
|
||||
return snapshotChanged(_that.channels,_that.clients);case BridgeEvent_PttCapability() when pttCapability != null:
|
||||
return pttCapability(_that.level,_that.backendId,_that.boundInputClass);case _:
|
||||
return pttCapability(_that.level,_that.backendId,_that.boundInputClass);case BridgeEvent_VoiceState() when voiceState != null:
|
||||
return voiceState(_that.inChannel,_that.transmitMode,_that.mute,_that.releaseTailMs);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
@@ -686,6 +692,83 @@ as String,
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
|
||||
class BridgeEvent_VoiceState extends BridgeEvent {
|
||||
const BridgeEvent_VoiceState({required this.inChannel, required this.transmitMode, required this.mute, required this.releaseTailMs}): super._();
|
||||
|
||||
|
||||
/// True when the session is currently joined to a voice
|
||||
/// channel and the audio engine is running.
|
||||
final bool inChannel;
|
||||
/// Active transmit mode.
|
||||
final BridgeTransmitMode transmitMode;
|
||||
/// True when the hard-mute clamp is engaged.
|
||||
final bool mute;
|
||||
/// Current release-tail in milliseconds (0..=500).
|
||||
final int releaseTailMs;
|
||||
|
||||
/// Create a copy of BridgeEvent
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$BridgeEvent_VoiceStateCopyWith<BridgeEvent_VoiceState> get copyWith => _$BridgeEvent_VoiceStateCopyWithImpl<BridgeEvent_VoiceState>(this, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is BridgeEvent_VoiceState&&(identical(other.inChannel, inChannel) || other.inChannel == inChannel)&&(identical(other.transmitMode, transmitMode) || other.transmitMode == transmitMode)&&(identical(other.mute, mute) || other.mute == mute)&&(identical(other.releaseTailMs, releaseTailMs) || other.releaseTailMs == releaseTailMs));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,inChannel,transmitMode,mute,releaseTailMs);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'BridgeEvent.voiceState(inChannel: $inChannel, transmitMode: $transmitMode, mute: $mute, releaseTailMs: $releaseTailMs)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $BridgeEvent_VoiceStateCopyWith<$Res> implements $BridgeEventCopyWith<$Res> {
|
||||
factory $BridgeEvent_VoiceStateCopyWith(BridgeEvent_VoiceState value, $Res Function(BridgeEvent_VoiceState) _then) = _$BridgeEvent_VoiceStateCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
bool inChannel, BridgeTransmitMode transmitMode, bool mute, int releaseTailMs
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$BridgeEvent_VoiceStateCopyWithImpl<$Res>
|
||||
implements $BridgeEvent_VoiceStateCopyWith<$Res> {
|
||||
_$BridgeEvent_VoiceStateCopyWithImpl(this._self, this._then);
|
||||
|
||||
final BridgeEvent_VoiceState _self;
|
||||
final $Res Function(BridgeEvent_VoiceState) _then;
|
||||
|
||||
/// Create a copy of BridgeEvent
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') $Res call({Object? inChannel = null,Object? transmitMode = null,Object? mute = null,Object? releaseTailMs = null,}) {
|
||||
return _then(BridgeEvent_VoiceState(
|
||||
inChannel: null == inChannel ? _self.inChannel : inChannel // ignore: cast_nullable_to_non_nullable
|
||||
as bool,transmitMode: null == transmitMode ? _self.transmitMode : transmitMode // ignore: cast_nullable_to_non_nullable
|
||||
as BridgeTransmitMode,mute: null == mute ? _self.mute : mute // ignore: cast_nullable_to_non_nullable
|
||||
as bool,releaseTailMs: null == releaseTailMs ? _self.releaseTailMs : releaseTailMs // ignore: cast_nullable_to_non_nullable
|
||||
as int,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
|
||||
@@ -67,7 +67,7 @@ class RustLib extends BaseEntrypoint<RustLibApi, RustLibApiImpl, RustLibWire> {
|
||||
String get codegenVersion => '2.12.0';
|
||||
|
||||
@override
|
||||
int get rustContentHash => -427953414;
|
||||
int get rustContentHash => -330689763;
|
||||
|
||||
static const kDefaultExternalLibraryLoaderConfig =
|
||||
ExternalLibraryLoaderConfig(
|
||||
@@ -99,6 +99,10 @@ abstract class RustLibApi extends BaseApi {
|
||||
|
||||
String crateApiExportDiagnostics();
|
||||
|
||||
Future<int> crateApiGetReleaseTailMs();
|
||||
|
||||
Future<BridgeTransmitMode> crateApiGetTransmitMode();
|
||||
|
||||
Future<void> crateApiInitStorage({required String dir});
|
||||
|
||||
Future<bool> crateApiIsConnected();
|
||||
@@ -112,6 +116,8 @@ abstract class RustLibApi extends BaseApi {
|
||||
|
||||
Future<(String, String, String)> crateApiPttDescriptor();
|
||||
|
||||
Future<void> crateApiSetHardMute({required bool muted});
|
||||
|
||||
Future<void> crateApiSetInputMuted({required bool muted});
|
||||
|
||||
void crateApiSetNetworkState({required BridgeNetworkState state});
|
||||
@@ -127,11 +133,20 @@ abstract class RustLibApi extends BaseApi {
|
||||
required String platformKey,
|
||||
});
|
||||
|
||||
Future<void> crateApiSetReleaseTailMs({required int ms});
|
||||
|
||||
Future<void> crateApiSetTransmitMode({required BridgeTransmitMode mode});
|
||||
|
||||
Future<BridgeSnapshot> crateApiSnapshot();
|
||||
|
||||
Future<void> crateApiStartAudio();
|
||||
|
||||
Future<void> crateApiUpdateBookmark({required BridgeBookmark b});
|
||||
|
||||
Future<void> crateApiVoiceJoin({
|
||||
required BigInt channelId,
|
||||
required String password,
|
||||
});
|
||||
|
||||
Future<void> crateApiVoiceLeave();
|
||||
}
|
||||
|
||||
class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
@@ -369,6 +384,60 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
TaskConstMeta get kCrateApiExportDiagnosticsConstMeta =>
|
||||
const TaskConstMeta(debugName: "export_diagnostics", argNames: []);
|
||||
|
||||
@override
|
||||
Future<int> crateApiGetReleaseTailMs() {
|
||||
return handler.executeNormal(
|
||||
NormalTask(
|
||||
callFfi: (port_) {
|
||||
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 9,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
codec: SseCodec(
|
||||
decodeSuccessData: sse_decode_u_32,
|
||||
decodeErrorData: null,
|
||||
),
|
||||
constMeta: kCrateApiGetReleaseTailMsConstMeta,
|
||||
argValues: [],
|
||||
apiImpl: this,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
TaskConstMeta get kCrateApiGetReleaseTailMsConstMeta =>
|
||||
const TaskConstMeta(debugName: "get_release_tail_ms", argNames: []);
|
||||
|
||||
@override
|
||||
Future<BridgeTransmitMode> crateApiGetTransmitMode() {
|
||||
return handler.executeNormal(
|
||||
NormalTask(
|
||||
callFfi: (port_) {
|
||||
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 10,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
codec: SseCodec(
|
||||
decodeSuccessData: sse_decode_bridge_transmit_mode,
|
||||
decodeErrorData: null,
|
||||
),
|
||||
constMeta: kCrateApiGetTransmitModeConstMeta,
|
||||
argValues: [],
|
||||
apiImpl: this,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
TaskConstMeta get kCrateApiGetTransmitModeConstMeta =>
|
||||
const TaskConstMeta(debugName: "get_transmit_mode", argNames: []);
|
||||
|
||||
@override
|
||||
Future<void> crateApiInitStorage({required String dir}) {
|
||||
return handler.executeNormal(
|
||||
@@ -379,7 +448,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 9,
|
||||
funcId: 11,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -406,7 +475,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 10,
|
||||
funcId: 12,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -433,7 +502,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 11,
|
||||
funcId: 13,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -465,7 +534,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 12,
|
||||
funcId: 14,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -494,7 +563,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 13,
|
||||
funcId: 15,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -512,6 +581,34 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
TaskConstMeta get kCrateApiPttDescriptorConstMeta =>
|
||||
const TaskConstMeta(debugName: "ptt_descriptor", argNames: []);
|
||||
|
||||
@override
|
||||
Future<void> crateApiSetHardMute({required bool muted}) {
|
||||
return handler.executeNormal(
|
||||
NormalTask(
|
||||
callFfi: (port_) {
|
||||
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||
sse_encode_bool(muted, serializer);
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 16,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
codec: SseCodec(
|
||||
decodeSuccessData: sse_decode_unit,
|
||||
decodeErrorData: sse_decode_bridge_error,
|
||||
),
|
||||
constMeta: kCrateApiSetHardMuteConstMeta,
|
||||
argValues: [muted],
|
||||
apiImpl: this,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
TaskConstMeta get kCrateApiSetHardMuteConstMeta =>
|
||||
const TaskConstMeta(debugName: "set_hard_mute", argNames: ["muted"]);
|
||||
|
||||
@override
|
||||
Future<void> crateApiSetInputMuted({required bool muted}) {
|
||||
return handler.executeNormal(
|
||||
@@ -522,7 +619,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 14,
|
||||
funcId: 17,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -547,7 +644,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
callFfi: () {
|
||||
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||
sse_encode_bridge_network_state(state, serializer);
|
||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 15)!;
|
||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 18)!;
|
||||
},
|
||||
codec: SseCodec(
|
||||
decodeSuccessData: sse_decode_unit,
|
||||
@@ -573,7 +670,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 16,
|
||||
funcId: 19,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -601,7 +698,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 17,
|
||||
funcId: 20,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -629,7 +726,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 18,
|
||||
funcId: 21,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -661,7 +758,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 19,
|
||||
funcId: 22,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -681,6 +778,62 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
argNames: ["inputClass", "platformKey"],
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void> crateApiSetReleaseTailMs({required int ms}) {
|
||||
return handler.executeNormal(
|
||||
NormalTask(
|
||||
callFfi: (port_) {
|
||||
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||
sse_encode_u_32(ms, serializer);
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 23,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
codec: SseCodec(
|
||||
decodeSuccessData: sse_decode_unit,
|
||||
decodeErrorData: sse_decode_bridge_error,
|
||||
),
|
||||
constMeta: kCrateApiSetReleaseTailMsConstMeta,
|
||||
argValues: [ms],
|
||||
apiImpl: this,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
TaskConstMeta get kCrateApiSetReleaseTailMsConstMeta =>
|
||||
const TaskConstMeta(debugName: "set_release_tail_ms", argNames: ["ms"]);
|
||||
|
||||
@override
|
||||
Future<void> crateApiSetTransmitMode({required BridgeTransmitMode mode}) {
|
||||
return handler.executeNormal(
|
||||
NormalTask(
|
||||
callFfi: (port_) {
|
||||
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||
sse_encode_bridge_transmit_mode(mode, serializer);
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 24,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
codec: SseCodec(
|
||||
decodeSuccessData: sse_decode_unit,
|
||||
decodeErrorData: sse_decode_bridge_error,
|
||||
),
|
||||
constMeta: kCrateApiSetTransmitModeConstMeta,
|
||||
argValues: [mode],
|
||||
apiImpl: this,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
TaskConstMeta get kCrateApiSetTransmitModeConstMeta =>
|
||||
const TaskConstMeta(debugName: "set_transmit_mode", argNames: ["mode"]);
|
||||
|
||||
@override
|
||||
Future<BridgeSnapshot> crateApiSnapshot() {
|
||||
return handler.executeNormal(
|
||||
@@ -690,7 +843,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 20,
|
||||
funcId: 25,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -708,33 +861,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
TaskConstMeta get kCrateApiSnapshotConstMeta =>
|
||||
const TaskConstMeta(debugName: "snapshot", argNames: []);
|
||||
|
||||
@override
|
||||
Future<void> crateApiStartAudio() {
|
||||
return handler.executeNormal(
|
||||
NormalTask(
|
||||
callFfi: (port_) {
|
||||
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 21,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
codec: SseCodec(
|
||||
decodeSuccessData: sse_decode_unit,
|
||||
decodeErrorData: sse_decode_bridge_error,
|
||||
),
|
||||
constMeta: kCrateApiStartAudioConstMeta,
|
||||
argValues: [],
|
||||
apiImpl: this,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
TaskConstMeta get kCrateApiStartAudioConstMeta =>
|
||||
const TaskConstMeta(debugName: "start_audio", argNames: []);
|
||||
|
||||
@override
|
||||
Future<void> crateApiUpdateBookmark({required BridgeBookmark b}) {
|
||||
return handler.executeNormal(
|
||||
@@ -745,7 +871,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 22,
|
||||
funcId: 26,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
@@ -763,6 +889,67 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
TaskConstMeta get kCrateApiUpdateBookmarkConstMeta =>
|
||||
const TaskConstMeta(debugName: "update_bookmark", argNames: ["b"]);
|
||||
|
||||
@override
|
||||
Future<void> crateApiVoiceJoin({
|
||||
required BigInt channelId,
|
||||
required String password,
|
||||
}) {
|
||||
return handler.executeNormal(
|
||||
NormalTask(
|
||||
callFfi: (port_) {
|
||||
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||
sse_encode_u_64(channelId, serializer);
|
||||
sse_encode_String(password, serializer);
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 27,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
codec: SseCodec(
|
||||
decodeSuccessData: sse_decode_unit,
|
||||
decodeErrorData: sse_decode_bridge_error,
|
||||
),
|
||||
constMeta: kCrateApiVoiceJoinConstMeta,
|
||||
argValues: [channelId, password],
|
||||
apiImpl: this,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
TaskConstMeta get kCrateApiVoiceJoinConstMeta => const TaskConstMeta(
|
||||
debugName: "voice_join",
|
||||
argNames: ["channelId", "password"],
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void> crateApiVoiceLeave() {
|
||||
return handler.executeNormal(
|
||||
NormalTask(
|
||||
callFfi: (port_) {
|
||||
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||
pdeCallFfi(
|
||||
generalizedFrbRustBinding,
|
||||
serializer,
|
||||
funcId: 28,
|
||||
port: port_,
|
||||
);
|
||||
},
|
||||
codec: SseCodec(
|
||||
decodeSuccessData: sse_decode_unit,
|
||||
decodeErrorData: sse_decode_bridge_error,
|
||||
),
|
||||
constMeta: kCrateApiVoiceLeaveConstMeta,
|
||||
argValues: [],
|
||||
apiImpl: this,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
TaskConstMeta get kCrateApiVoiceLeaveConstMeta =>
|
||||
const TaskConstMeta(debugName: "voice_leave", argNames: []);
|
||||
|
||||
@protected
|
||||
AnyhowException dco_decode_AnyhowException(dynamic raw) {
|
||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||
@@ -904,6 +1091,13 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
backendId: dco_decode_String(raw[2]),
|
||||
boundInputClass: dco_decode_String(raw[3]),
|
||||
);
|
||||
case 8:
|
||||
return BridgeEvent_VoiceState(
|
||||
inChannel: dco_decode_bool(raw[1]),
|
||||
transmitMode: dco_decode_bridge_transmit_mode(raw[2]),
|
||||
mute: dco_decode_bool(raw[3]),
|
||||
releaseTailMs: dco_decode_u_32(raw[4]),
|
||||
);
|
||||
default:
|
||||
throw Exception("unreachable");
|
||||
}
|
||||
@@ -937,6 +1131,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
);
|
||||
}
|
||||
|
||||
@protected
|
||||
BridgeTransmitMode dco_decode_bridge_transmit_mode(dynamic raw) {
|
||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||
return BridgeTransmitMode.values[raw as int];
|
||||
}
|
||||
|
||||
@protected
|
||||
double dco_decode_f_32(dynamic raw) {
|
||||
// Codec=Dco (DartCObject based), see doc to use other codecs
|
||||
@@ -1177,6 +1377,17 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
backendId: var_backendId,
|
||||
boundInputClass: var_boundInputClass,
|
||||
);
|
||||
case 8:
|
||||
var var_inChannel = sse_decode_bool(deserializer);
|
||||
var var_transmitMode = sse_decode_bridge_transmit_mode(deserializer);
|
||||
var var_mute = sse_decode_bool(deserializer);
|
||||
var var_releaseTailMs = sse_decode_u_32(deserializer);
|
||||
return BridgeEvent_VoiceState(
|
||||
inChannel: var_inChannel,
|
||||
transmitMode: var_transmitMode,
|
||||
mute: var_mute,
|
||||
releaseTailMs: var_releaseTailMs,
|
||||
);
|
||||
default:
|
||||
throw UnimplementedError('');
|
||||
}
|
||||
@@ -1219,6 +1430,15 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
);
|
||||
}
|
||||
|
||||
@protected
|
||||
BridgeTransmitMode sse_decode_bridge_transmit_mode(
|
||||
SseDeserializer deserializer,
|
||||
) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
var inner = sse_decode_i_32(deserializer);
|
||||
return BridgeTransmitMode.values[inner];
|
||||
}
|
||||
|
||||
@protected
|
||||
double sse_decode_f_32(SseDeserializer deserializer) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
@@ -1472,6 +1692,17 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
sse_encode_String(level, serializer);
|
||||
sse_encode_String(backendId, serializer);
|
||||
sse_encode_String(boundInputClass, serializer);
|
||||
case BridgeEvent_VoiceState(
|
||||
inChannel: final inChannel,
|
||||
transmitMode: final transmitMode,
|
||||
mute: final mute,
|
||||
releaseTailMs: final releaseTailMs,
|
||||
):
|
||||
sse_encode_i_32(8, serializer);
|
||||
sse_encode_bool(inChannel, serializer);
|
||||
sse_encode_bridge_transmit_mode(transmitMode, serializer);
|
||||
sse_encode_bool(mute, serializer);
|
||||
sse_encode_u_32(releaseTailMs, serializer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1507,6 +1738,15 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
||||
sse_encode_list_bridge_client(self.clients, serializer);
|
||||
}
|
||||
|
||||
@protected
|
||||
void sse_encode_bridge_transmit_mode(
|
||||
BridgeTransmitMode self,
|
||||
SseSerializer serializer,
|
||||
) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
sse_encode_i_32(self.index, serializer);
|
||||
}
|
||||
|
||||
@protected
|
||||
void sse_encode_f_32(double self, SseSerializer serializer) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
|
||||
@@ -63,6 +63,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
@protected
|
||||
BridgeSnapshot dco_decode_bridge_snapshot(dynamic raw);
|
||||
|
||||
@protected
|
||||
BridgeTransmitMode dco_decode_bridge_transmit_mode(dynamic raw);
|
||||
|
||||
@protected
|
||||
double dco_decode_f_32(dynamic raw);
|
||||
|
||||
@@ -149,6 +152,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
@protected
|
||||
BridgeSnapshot sse_decode_bridge_snapshot(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
BridgeTransmitMode sse_decode_bridge_transmit_mode(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
double sse_decode_f_32(SseDeserializer deserializer);
|
||||
|
||||
@@ -259,6 +267,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_bridge_transmit_mode(
|
||||
BridgeTransmitMode self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_f_32(double self, SseSerializer serializer);
|
||||
|
||||
|
||||
@@ -65,6 +65,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
@protected
|
||||
BridgeSnapshot dco_decode_bridge_snapshot(dynamic raw);
|
||||
|
||||
@protected
|
||||
BridgeTransmitMode dco_decode_bridge_transmit_mode(dynamic raw);
|
||||
|
||||
@protected
|
||||
double dco_decode_f_32(dynamic raw);
|
||||
|
||||
@@ -151,6 +154,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
@protected
|
||||
BridgeSnapshot sse_decode_bridge_snapshot(SseDeserializer deserializer);
|
||||
|
||||
@protected
|
||||
BridgeTransmitMode sse_decode_bridge_transmit_mode(
|
||||
SseDeserializer deserializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
double sse_decode_f_32(SseDeserializer deserializer);
|
||||
|
||||
@@ -261,6 +269,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_bridge_transmit_mode(
|
||||
BridgeTransmitMode self,
|
||||
SseSerializer serializer,
|
||||
);
|
||||
|
||||
@protected
|
||||
void sse_encode_f_32(double self, SseSerializer serializer);
|
||||
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
// Voice bar widget (SDD-097). Replaces the legacy `_AudioControls`
|
||||
// block that used to live in `main.dart`. Driven by the
|
||||
// `BridgeEvent::VoiceState` stream the bridge publishes from the
|
||||
// core's transmit-mode selector + release-tail timer.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../l10n/generated/app_localizations.dart';
|
||||
import '../main.dart' show PttCapabilityBadge;
|
||||
import '../src/rust/api.dart' as rust;
|
||||
|
||||
/// Voice bar — surfaces the live voice state, mode badge, hard-mute
|
||||
/// toggle, level meter, and a leave-channel affordance.
|
||||
class VoiceBar extends StatelessWidget {
|
||||
/// Construct a voice bar.
|
||||
const VoiceBar({
|
||||
super.key,
|
||||
required this.inChannel,
|
||||
required this.transmitMode,
|
||||
required this.hardMute,
|
||||
required this.releaseTailMs,
|
||||
required this.channelName,
|
||||
required this.audioStats,
|
||||
required this.pttLevel,
|
||||
required this.pttBackendId,
|
||||
required this.pttBoundInputClass,
|
||||
required this.pttBoundKeyLabel,
|
||||
required this.onToggleMute,
|
||||
required this.onConfigure,
|
||||
required this.onLeave,
|
||||
});
|
||||
|
||||
/// True when the session is currently joined to a voice channel.
|
||||
final bool inChannel;
|
||||
|
||||
/// Active transmit mode.
|
||||
final rust.BridgeTransmitMode transmitMode;
|
||||
|
||||
/// Hard-mute clamp state.
|
||||
final bool hardMute;
|
||||
|
||||
/// Configured release-tail in milliseconds (0..=500). Surfaced as
|
||||
/// a hint underneath the mode badge.
|
||||
final int releaseTailMs;
|
||||
|
||||
/// Channel display name to show in the channel-name pill. Empty
|
||||
/// string suppresses the pill (typically when `!inChannel`).
|
||||
final String channelName;
|
||||
|
||||
/// Audio statistics (TX/RX frames + ptt_active) to drive the
|
||||
/// level meter. Pass `null` to render an idle meter.
|
||||
final rust.BridgeAudioStats? audioStats;
|
||||
|
||||
/// PTT capability badge inputs — passed through to
|
||||
/// [`PttCapabilityBadge`].
|
||||
final String pttLevel;
|
||||
|
||||
/// Backend id, e.g. `focused`, `windows-raw-input`.
|
||||
final String pttBackendId;
|
||||
|
||||
/// Coarse bound input class.
|
||||
final String pttBoundInputClass;
|
||||
|
||||
/// Platform-neutral key label captured by the binding dialog.
|
||||
final String pttBoundKeyLabel;
|
||||
|
||||
/// Toggle the hard-mute clamp.
|
||||
final VoidCallback onToggleMute;
|
||||
|
||||
/// Open the voice settings dialog.
|
||||
final VoidCallback onConfigure;
|
||||
|
||||
/// Leave the voice channel.
|
||||
final VoidCallback onLeave;
|
||||
|
||||
String _modeLabel(AppL10n l10n) {
|
||||
switch (transmitMode) {
|
||||
case rust.BridgeTransmitMode.ptt:
|
||||
final key = pttBoundKeyLabel.isEmpty ? '—' : pttBoundKeyLabel;
|
||||
return '${l10n.voiceModePtt}: $key';
|
||||
case rust.BridgeTransmitMode.continuous:
|
||||
return l10n.voiceModeContinuous;
|
||||
case rust.BridgeTransmitMode.voiceActivity:
|
||||
return '${l10n.voiceModeVoiceActivity} (${l10n.voiceModeComingSoon})';
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppL10n.of(context);
|
||||
final theme = Theme.of(context);
|
||||
final stats = audioStats;
|
||||
final levelActive = stats?.pttActive ?? false;
|
||||
|
||||
return Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// Row 1: channel pill + mute toggle
|
||||
Row(
|
||||
children: [
|
||||
if (inChannel && channelName.isNotEmpty) ...[
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.tag,
|
||||
size: 14,
|
||||
color: theme.colorScheme.onPrimaryContainer,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
channelName,
|
||||
style: TextStyle(
|
||||
color: theme.colorScheme.onPrimaryContainer,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
tooltip: l10n.voiceHardMuteLabel,
|
||||
icon: Icon(hardMute ? Icons.mic_off : Icons.mic),
|
||||
isSelected: hardMute,
|
||||
selectedIcon: const Icon(Icons.mic_off),
|
||||
onPressed: onToggleMute,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
// Row 2: mode badge
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
transmitMode == rust.BridgeTransmitMode.ptt
|
||||
? Icons.radio_button_checked
|
||||
: Icons.podcasts,
|
||||
size: 16,
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_modeLabel(l10n),
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.tune, size: 16),
|
||||
label: Text(l10n.voiceSettingsTitle),
|
||||
onPressed: onConfigure,
|
||||
),
|
||||
],
|
||||
),
|
||||
// Row 3: release tail hint (only meaningful for PTT mode)
|
||||
if (transmitMode == rust.BridgeTransmitMode.ptt)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 22, top: 2),
|
||||
child: Text(
|
||||
'${l10n.voiceReleaseTailLabel}: $releaseTailMs${l10n.voiceReleaseTailHint}',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
// Row 4: level meter
|
||||
_LevelMeter(active: levelActive),
|
||||
const SizedBox(height: 4),
|
||||
if (stats != null)
|
||||
Text(
|
||||
l10n.audioStatsLine(
|
||||
stats.framesSent,
|
||||
stats.framesReceived,
|
||||
stats.pttActive ? 'on' : 'off',
|
||||
),
|
||||
style: theme.textTheme.bodySmall,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
// PTT capability badge
|
||||
PttCapabilityBadge(
|
||||
level: pttLevel,
|
||||
backendId: pttBackendId,
|
||||
boundInputClass: pttBoundInputClass,
|
||||
boundKeyLabel: pttBoundKeyLabel,
|
||||
onConfigure: onConfigure,
|
||||
),
|
||||
if (inChannel) ...[
|
||||
const SizedBox(height: 6),
|
||||
Align(
|
||||
alignment: AlignmentDirectional.centerEnd,
|
||||
child: TextButton.icon(
|
||||
icon: const Icon(Icons.call_end),
|
||||
label: Text(l10n.voiceLeaveAction),
|
||||
onPressed: onLeave,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LevelMeter extends StatelessWidget {
|
||||
const _LevelMeter({required this.active});
|
||||
|
||||
final bool active;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Container(
|
||||
height: 8,
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: FractionallySizedBox(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
widthFactor: active ? 0.75 : 0.05,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: active
|
||||
? theme.colorScheme.primary
|
||||
: theme.colorScheme.outlineVariant,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
// 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(),
|
||||
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),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ void main() {
|
||||
expect(snap.serverName, isNotEmpty);
|
||||
expect(snap.channels, isNotEmpty);
|
||||
|
||||
await rust.startAudio();
|
||||
await rust.voiceJoin(channelId: snap.channels.first.id, password: '');
|
||||
|
||||
// Initial stats: PTT off, no frames sent yet.
|
||||
final s0 = await rust.audioStats();
|
||||
|
||||
Reference in New Issue
Block a user