fix(audio,storage,ui): allow PTT binding before audio is running
Save-binding before joining a voice channel used to return BridgeError.invalidCommand(audio not started) because the PttController only exists after start_audio runs and set_ptt_binding required a live controller. Users naturally want to bind their PTT key once on first launch, not every time they join a channel — fix: * chanora_storage::IdentityFileStore::set_ptt_binding / get_ptt_binding persist the privacy-safe binding triple (input_class, platform_key, key_label) into audio_meta.json next to transmit_mode and release_tail_ms. * ChanoraSession holds pending_binding: Arc<Mutex<Option<PttBinding>>>. set_ptt_binding now (1) persists to storage best-effort, (2) stashes into pending_binding, (3) forwards live to the controller only if one exists. No more AudioNotStarted. * init_storage loads the persisted binding into pending_binding so it survives app restarts. * start_audio applies pending_binding immediately after constructing the PttController so the first key-press after join already works. * supervisor_loop carries pending_binding and re-applies it after any reconnect-driven audio engine restart, so reconnects don't silently drop the hotkey. * New bridge call get_ptt_binding() -> (input_class, key_label) plus a matching Flutter _hydratePttBinding() in initState lets the Voice Bar show the user's saved hotkey label on launch (e.g. 'PTT: Space') before any voice channel is joined. cargo test --workspace --lib: 72 passed / 0 failed / 1 ignored. flutter analyze: clean (6 pre-existing Radio.groupValue infos). FRB bindings regenerated.
This commit is contained in:
@@ -518,6 +518,18 @@ pub async fn ptt_descriptor() -> (String, String, String) {
|
||||
.unwrap_or_else(|_| (String::new(), String::new(), String::new()))
|
||||
}
|
||||
|
||||
/// Return the persisted PTT binding as a
|
||||
/// `(input_class, platform_key)` pair so the UI can hydrate its
|
||||
/// display state at launch (e.g. show "PTT: Space" next to the
|
||||
/// badge before the user re-opens the binding dialog). Empty
|
||||
/// strings mean no binding has been persisted yet.
|
||||
pub async fn get_ptt_binding() -> (String, String) {
|
||||
runtime()
|
||||
.spawn(async { session().get_ptt_binding().await })
|
||||
.await
|
||||
.unwrap_or_else(|_| (String::new(), String::new()))
|
||||
}
|
||||
|
||||
/// Move our own client to `channel_id`. Optional channel password
|
||||
/// for password-protected channels — pass an empty string when not
|
||||
/// required.
|
||||
|
||||
@@ -38,7 +38,7 @@ flutter_rust_bridge::frb_generated_boilerplate!(
|
||||
default_rust_auto_opaque = RustAutoOpaqueMoi,
|
||||
);
|
||||
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.12.0";
|
||||
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 651188866;
|
||||
pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 1306308591;
|
||||
|
||||
// Section: executor
|
||||
|
||||
@@ -326,6 +326,41 @@ fn wire__crate__api__export_diagnostics_impl(
|
||||
},
|
||||
)
|
||||
}
|
||||
fn wire__crate__api__get_ptt_binding_impl(
|
||||
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||
rust_vec_len_: i32,
|
||||
data_len_: i32,
|
||||
) {
|
||||
FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::<flutter_rust_bridge::for_generated::SseCodec, _, _, _>(
|
||||
flutter_rust_bridge::for_generated::TaskInfo {
|
||||
debug_name: "get_ptt_binding",
|
||||
port: Some(port_),
|
||||
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
|
||||
},
|
||||
move || {
|
||||
let message = unsafe {
|
||||
flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire(
|
||||
ptr_,
|
||||
rust_vec_len_,
|
||||
data_len_,
|
||||
)
|
||||
};
|
||||
let mut deserializer =
|
||||
flutter_rust_bridge::for_generated::SseDeserializer::new(message);
|
||||
deserializer.end();
|
||||
move |context| async move {
|
||||
transform_result_sse::<_, ()>(
|
||||
(move || async move {
|
||||
let output_ok = Result::<_, ()>::Ok(crate::api::get_ptt_binding().await)?;
|
||||
Ok(output_ok)
|
||||
})()
|
||||
.await,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
fn wire__crate__api__get_release_tail_ms_impl(
|
||||
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||
@@ -1406,6 +1441,15 @@ impl SseDecode for Vec<u8> {
|
||||
}
|
||||
}
|
||||
|
||||
impl SseDecode for (String, String) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||
let mut var_field0 = <String>::sse_decode(deserializer);
|
||||
let mut var_field1 = <String>::sse_decode(deserializer);
|
||||
return (var_field0, var_field1);
|
||||
}
|
||||
}
|
||||
|
||||
impl SseDecode for (String, String, String) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {
|
||||
@@ -1458,25 +1502,26 @@ fn pde_ffi_dispatcher_primary_impl(
|
||||
5 => wire__crate__api__delete_bookmark_impl(port, ptr, rust_vec_len, data_len),
|
||||
6 => wire__crate__api__disconnect_impl(port, ptr, rust_vec_len, data_len),
|
||||
7 => wire__crate__api__events_stream_impl(port, ptr, rust_vec_len, data_len),
|
||||
9 => wire__crate__api__get_release_tail_ms_impl(port, ptr, rust_vec_len, data_len),
|
||||
10 => wire__crate__api__get_transmit_mode_impl(port, ptr, rust_vec_len, data_len),
|
||||
11 => wire__crate__api__init_storage_impl(port, ptr, rust_vec_len, data_len),
|
||||
12 => wire__crate__api__is_connected_impl(port, ptr, rust_vec_len, data_len),
|
||||
13 => wire__crate__api__list_bookmarks_impl(port, ptr, rust_vec_len, data_len),
|
||||
15 => wire__crate__api__move_to_channel_impl(port, ptr, rust_vec_len, data_len),
|
||||
16 => wire__crate__api__ptt_descriptor_impl(port, ptr, rust_vec_len, data_len),
|
||||
17 => wire__crate__api__set_hard_mute_impl(port, ptr, rust_vec_len, data_len),
|
||||
18 => wire__crate__api__set_input_muted_impl(port, ptr, rust_vec_len, data_len),
|
||||
20 => wire__crate__api__set_output_gain_impl(port, ptr, rust_vec_len, data_len),
|
||||
21 => wire__crate__api__set_output_muted_impl(port, ptr, rust_vec_len, data_len),
|
||||
22 => wire__crate__api__set_ptt_impl(port, ptr, rust_vec_len, data_len),
|
||||
23 => wire__crate__api__set_ptt_binding_impl(port, ptr, rust_vec_len, data_len),
|
||||
24 => wire__crate__api__set_release_tail_ms_impl(port, ptr, rust_vec_len, data_len),
|
||||
25 => wire__crate__api__set_transmit_mode_impl(port, ptr, rust_vec_len, data_len),
|
||||
26 => wire__crate__api__snapshot_impl(port, ptr, rust_vec_len, data_len),
|
||||
27 => wire__crate__api__update_bookmark_impl(port, ptr, rust_vec_len, data_len),
|
||||
28 => wire__crate__api__voice_join_impl(port, ptr, rust_vec_len, data_len),
|
||||
29 => wire__crate__api__voice_leave_impl(port, ptr, rust_vec_len, data_len),
|
||||
9 => wire__crate__api__get_ptt_binding_impl(port, ptr, rust_vec_len, data_len),
|
||||
10 => wire__crate__api__get_release_tail_ms_impl(port, ptr, rust_vec_len, data_len),
|
||||
11 => wire__crate__api__get_transmit_mode_impl(port, ptr, rust_vec_len, data_len),
|
||||
12 => wire__crate__api__init_storage_impl(port, ptr, rust_vec_len, data_len),
|
||||
13 => wire__crate__api__is_connected_impl(port, ptr, rust_vec_len, data_len),
|
||||
14 => wire__crate__api__list_bookmarks_impl(port, ptr, rust_vec_len, data_len),
|
||||
16 => wire__crate__api__move_to_channel_impl(port, ptr, rust_vec_len, data_len),
|
||||
17 => wire__crate__api__ptt_descriptor_impl(port, ptr, rust_vec_len, data_len),
|
||||
18 => wire__crate__api__set_hard_mute_impl(port, ptr, rust_vec_len, data_len),
|
||||
19 => wire__crate__api__set_input_muted_impl(port, ptr, rust_vec_len, data_len),
|
||||
21 => wire__crate__api__set_output_gain_impl(port, ptr, rust_vec_len, data_len),
|
||||
22 => wire__crate__api__set_output_muted_impl(port, ptr, rust_vec_len, data_len),
|
||||
23 => wire__crate__api__set_ptt_impl(port, ptr, rust_vec_len, data_len),
|
||||
24 => wire__crate__api__set_ptt_binding_impl(port, ptr, rust_vec_len, data_len),
|
||||
25 => wire__crate__api__set_release_tail_ms_impl(port, ptr, rust_vec_len, data_len),
|
||||
26 => wire__crate__api__set_transmit_mode_impl(port, ptr, rust_vec_len, data_len),
|
||||
27 => wire__crate__api__snapshot_impl(port, ptr, rust_vec_len, data_len),
|
||||
28 => wire__crate__api__update_bookmark_impl(port, ptr, rust_vec_len, data_len),
|
||||
29 => wire__crate__api__voice_join_impl(port, ptr, rust_vec_len, data_len),
|
||||
30 => wire__crate__api__voice_leave_impl(port, ptr, rust_vec_len, data_len),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
@@ -1490,8 +1535,8 @@ fn pde_ffi_dispatcher_sync_impl(
|
||||
// Codec=Pde (Serialization + dispatch), see doc to use other codecs
|
||||
match func_id {
|
||||
8 => wire__crate__api__export_diagnostics_impl(ptr, rust_vec_len, data_len),
|
||||
14 => wire__crate__api__log_file_path_str_impl(ptr, rust_vec_len, data_len),
|
||||
19 => wire__crate__api__set_network_state_impl(ptr, rust_vec_len, data_len),
|
||||
15 => wire__crate__api__log_file_path_str_impl(ptr, rust_vec_len, data_len),
|
||||
20 => wire__crate__api__set_network_state_impl(ptr, rust_vec_len, data_len),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
@@ -2048,6 +2093,14 @@ impl SseEncode for Vec<u8> {
|
||||
}
|
||||
}
|
||||
|
||||
impl SseEncode for (String, String) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||
<String>::sse_encode(self.0, serializer);
|
||||
<String>::sse_encode(self.1, serializer);
|
||||
}
|
||||
}
|
||||
|
||||
impl SseEncode for (String, String, String) {
|
||||
// Codec=Sse (Serialization based), see doc to use other codecs
|
||||
fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {
|
||||
|
||||
Reference in New Issue
Block a user