// Binds soundboard entries to global shortcuts and keeps the registry in // sync with storage mutations. Starting returns a teardown function that // undoes every registration the binding made. // // Usage (in CallContext effect when call becomes connected): // const teardown = startSoundboardHotkeys((id) => void playSoundboard(id)); // return teardown; // useEffect cleanup import { isTauriRuntime, registerSoundShortcut, unregisterAllSoundShortcuts, unregisterSoundShortcut, } from './globalShortcut'; import { getPttSettings } from './pttSettings'; import { listSounds, subscribeSoundboardChanges } from './soundboardStorage'; export type FirePress = (id: string) => void; export function startSoundboardHotkeys(onPress: FirePress): () => void { if (!isTauriRuntime()) { // No-op on pure web preview — global shortcuts unsupported. Storage // change subscription would still fire, but there's nothing to sync. return () => undefined; } let active = true; // identity-keyed: sound id -> currently bound DOM code const bound = new Map(); const sync = async (): Promise => { if (!active) return; let entries; try { entries = await listSounds(); } catch (err: unknown) { console.warn('soundboardHotkeys list failed', err); return; } const pttKey = getPttSettings().key; const wantByCode = new Map(); // code -> id (winner on conflict) for (const e of entries) { if (!e.hotkey) continue; // PTT wins over soundboard: don't hijack the talk key, skip silently. if (pttKey && e.hotkey === pttKey) continue; // First-writer-wins for dupes (stable because listSounds is sorted // deterministically). Settings UI should prevent this upstream. if (!wantByCode.has(e.hotkey)) wantByCode.set(e.hotkey, e.id); } const want = new Map(); for (const [code, id] of wantByCode) want.set(id, code); // Unregister bindings that disappeared or changed key. for (const [id, code] of bound) { const nextCode = want.get(id); if (nextCode !== code) { await unregisterSoundShortcut(id); bound.delete(id); } } // Register new / updated bindings. for (const [id, code] of want) { if (bound.get(id) === code) continue; const ok = await registerSoundShortcut(id, code, () => { if (!active) return; onPress(id); }); if (ok) bound.set(id, code); } }; const unsubscribe = subscribeSoundboardChanges(() => { void sync(); }); // Initial binding pass. void sync(); return () => { active = false; unsubscribe(); void unregisterAllSoundShortcuts(); bound.clear(); }; }