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.