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:
@@ -102,6 +102,20 @@ struct AudioMeta {
|
||||
/// Release-tail in milliseconds (SDD-096). Default 200.
|
||||
#[serde(default = "default_release_tail_ms")]
|
||||
release_tail_ms: u32,
|
||||
/// Bound PTT input class as the privacy-safe string accepted by
|
||||
/// the bridge (`""`, `"keyboard"`, `"mouse-side-button"`).
|
||||
/// Default empty (no binding).
|
||||
#[serde(default)]
|
||||
ptt_input_class: String,
|
||||
/// Opaque platform key string the binding dialog produced
|
||||
/// (e.g. `"Space"`, `"F10"`, `"mouse-side-button:8"`). Default
|
||||
/// empty.
|
||||
#[serde(default)]
|
||||
ptt_platform_key: String,
|
||||
/// Display-only platform-neutral key label the UI shows next
|
||||
/// to the binding (e.g. `"Space"`). Default empty.
|
||||
#[serde(default)]
|
||||
ptt_key_label: String,
|
||||
}
|
||||
|
||||
fn default_release_tail_ms() -> u32 {
|
||||
@@ -113,6 +127,9 @@ impl Default for AudioMeta {
|
||||
Self {
|
||||
transmit_mode: 0,
|
||||
release_tail_ms: 200,
|
||||
ptt_input_class: String::new(),
|
||||
ptt_platform_key: String::new(),
|
||||
ptt_key_label: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -505,6 +522,35 @@ impl IdentityFileStore {
|
||||
self.read_meta().release_tail_ms
|
||||
}
|
||||
|
||||
/// Persist the user's PTT binding (SDD-094 follow-up). The
|
||||
/// three fields together carry the privacy-safe binding
|
||||
/// surface — `input_class` is a stable category string
|
||||
/// (`""`, `"keyboard"`, `"mouse-side-button"`), `platform_key`
|
||||
/// is the opaque key identifier the platform backend
|
||||
/// understands, and `key_label` is the display string the UI
|
||||
/// renders next to the binding. None of these are raw key
|
||||
/// codes or scan codes per DEC-027.
|
||||
pub fn set_ptt_binding(
|
||||
&self,
|
||||
input_class: &str,
|
||||
platform_key: &str,
|
||||
key_label: &str,
|
||||
) -> Result<(), StorageError> {
|
||||
let mut m = self.read_meta();
|
||||
m.ptt_input_class = input_class.to_string();
|
||||
m.ptt_platform_key = platform_key.to_string();
|
||||
m.ptt_key_label = key_label.to_string();
|
||||
self.write_meta(&m)
|
||||
}
|
||||
|
||||
/// Read the persisted PTT binding. Returns
|
||||
/// `(input_class, platform_key, key_label)` with empty strings
|
||||
/// meaning "no binding".
|
||||
pub fn get_ptt_binding(&self) -> (String, String, String) {
|
||||
let m = self.read_meta();
|
||||
(m.ptt_input_class, m.ptt_platform_key, m.ptt_key_label)
|
||||
}
|
||||
|
||||
/// Remove any persisted identity. No-op if none exists. Leaves
|
||||
/// the DEK in place so future saves don't generate a new one.
|
||||
pub fn clear(&self) -> Result<(), StorageError> {
|
||||
|
||||
Reference in New Issue
Block a user