feat(e10): one keyboard cheat sheet for the whole app
The Plex player had a shortcuts overlay nobody else did, so the YouTube player's Space/F/arrows and the Plex browser's Backspace were documented nowhere. ui/KeyboardHelp is that sheet for every surface, opened by a global `h` - not the conventional `?`, which is a Shift chord on a Hungarian layout, while `h` is the key this app's reader already has in their fingers from the player. It renders the shared table filtered to the scopes that are LIVE: a surface registers its scope with useKeyScope while mounted, and only while its keys really do something - PlexBrowse deactivates its own scope inside the player, where its Backspace no-ops, so the sheet cannot promise a "Back one level" the handler declines to perform. Same rule for the `when` rows: ⇧←/⇧→ are listed only inside a series, an answer the player publishes because the sheet cannot ask. No new overlay primitive, after checking: Modal already portals into document.fullscreenElement ?? document.body (so this works over a fullscreen player), registers an Escape layer, traps focus and handles browser Back. Third time this epic that the thing was already built. Shift+F10 is in the table as `handledLocally` - documented and sharing the chord parser, but matched by useContextMenu on the focused row. dispatch neither routes it nor demands a handler for it, so the exhaustiveness check stays honest about what it actually covers. Also: A/S have cycled the Plex audio and subtitle tracks since E5, but silently - the only way to see where you landed was to open the tracks menu, the exact round-trip the shortcut exists to avoid. cycleAudio and cycleSubtitle now return the track they moved to, and the player flashes it over the video.
This commit is contained in:
@@ -365,8 +365,11 @@ export function usePlexTracks(opts: {
|
||||
setSubOrd: (ord: number | null) => void;
|
||||
changeAudio: (ord: number | null) => void;
|
||||
changeSubtitle: (ord: number | null) => void;
|
||||
cycleAudio: () => void;
|
||||
cycleSubtitle: () => void;
|
||||
/** Step to the next track and RETURN the ord landed on (null = the default/off entry), or
|
||||
* `undefined` when there was nothing to cycle. The caller needs the answer to put it on screen:
|
||||
* a keyboard track switch used to change the audio and tell you nothing. */
|
||||
cycleAudio: () => number | null | undefined;
|
||||
cycleSubtitle: () => number | null | undefined;
|
||||
changeAudioOffset: (v: number) => void;
|
||||
textSubs: PlexItemDetail["subtitle_streams"];
|
||||
selectedSub: PlexItemDetail["subtitle_streams"][number] | null;
|
||||
@@ -423,16 +426,21 @@ export function usePlexTracks(opts: {
|
||||
// 'A' cycles audio tracks; 'S' cycles subtitle (off → each text sub → off).
|
||||
const cycleAudio = useCallback(() => {
|
||||
const streams = detail?.audio_streams ?? [];
|
||||
if (streams.length < 2) return;
|
||||
if (streams.length < 2) return undefined;
|
||||
const idx = streams.findIndex((s) => s.ord === (audioRef.current ?? 0));
|
||||
const next = streams[(idx + 1) % streams.length]!; // length >= 2 (guarded), so in-bounds
|
||||
changeAudio(next.ord === 0 ? null : next.ord);
|
||||
const ord = next.ord === 0 ? null : next.ord;
|
||||
changeAudio(ord);
|
||||
return ord;
|
||||
}, [detail, changeAudio, audioRef]);
|
||||
const cycleSubtitle = useCallback(() => {
|
||||
const subs = (detail?.subtitle_streams ?? []).filter((s) => s.text);
|
||||
if (subs.length === 0) return undefined; // nothing to cycle through but "off"
|
||||
const order: (number | null)[] = [null, ...subs.map((s) => s.ord)];
|
||||
const i = order.findIndex((o) => o === subOrd);
|
||||
changeSubtitle(order[(i + 1) % order.length]!); // order always has [null, ...] → non-empty
|
||||
const ord = order[(i + 1) % order.length]!; // order always has [null, ...] → non-empty
|
||||
changeSubtitle(ord);
|
||||
return ord;
|
||||
}, [detail, subOrd, changeSubtitle]);
|
||||
// Audio A/V-sync offset: persist immediately (for the slider) but debounce the session restart so
|
||||
// dragging doesn't respawn ffmpeg on every step.
|
||||
|
||||
Reference in New Issue
Block a user