feat(player): the OS media controls, the basic keys and resume for the library and shared-link players
Plex, library and /watch tell the operating system what plays (title, line, picture) and take its
play / pause / skip / seek through one hook (lib/mediaSession); the Plex player drives it with its own
seek and clock, so the lock screen shows the film's position, not the stream's. The YouTube player is
left alone: its iframe already publishes all of it, and the OS routes to the frame that plays.
The library and /watch players get the basic keys (lib/mediaKeys, the keymap's mediaPlayer rows) and
open where the viewer stopped (lib/resume): the library keeps the point on the server, per user and
library item (library_states, POST /api/downloads/{id}/progress, listed back on both listings), the
shared link in this browser. A started library row continues, its menu can start it over, and its
picture carries the cards' progress bar. The resume rule (too early / near the end = from the start)
is one function shared with the YouTube route.
The library dialog opens focused on its panel (Modal initialFocus "player"): on the ✕, Space closed
the film. While the media element itself has the focus, Space and the arrows stay its own controls'
— the browser acts on them whatever a listener does, and one Space played and paused.
This commit is contained in:
@@ -36,6 +36,7 @@ export type KeyScope =
|
||||
| "contextMenu"
|
||||
| "ytPlayer"
|
||||
| "plexPlayer"
|
||||
| "mediaPlayer"
|
||||
| "plexBrowse";
|
||||
|
||||
const PAGE_LEVEL: ReadonlySet<KeyScope> = new Set<KeyScope>([
|
||||
@@ -48,7 +49,7 @@ const PAGE_LEVEL: ReadonlySet<KeyScope> = new Set<KeyScope>([
|
||||
export const isPageLevelScope = (scope: KeyScope): boolean => PAGE_LEVEL.has(scope);
|
||||
|
||||
/** Extra render condition for the help sheet (the shortcut itself is always live). */
|
||||
type ShortcutWhen = "episode";
|
||||
type ShortcutWhen = "episode" | "video";
|
||||
|
||||
export interface Shortcut {
|
||||
/** Unique within its scope; the handler key `dispatch()` demands. */
|
||||
@@ -213,6 +214,23 @@ export const SHORTCUTS = [
|
||||
allowInInput: true,
|
||||
},
|
||||
|
||||
// --- a player on the browser's own media element (the library's, a shared link's) -----------
|
||||
// The basic set (`lib/mediaKeys.ts`): the element's own controls carry the rest. The step is the
|
||||
// YouTube player's 5 s, the other short-form player; Escape is the dialog's.
|
||||
{ id: "playPause", scope: "mediaPlayer", keys: ["Space", "k"], labelKey: "keys.playPause" },
|
||||
{ id: "seekBack", scope: "mediaPlayer", keys: ["ArrowLeft"], labelKey: "keys.seek5" },
|
||||
{ id: "seekFwd", scope: "mediaPlayer", keys: ["ArrowRight"], labelKey: "keys.seek5" },
|
||||
// An audio file has no picture to fill the screen with: the key does nothing there, so the sheet
|
||||
// does not promise it.
|
||||
{
|
||||
id: "fullscreen",
|
||||
scope: "mediaPlayer",
|
||||
keys: ["f"],
|
||||
labelKey: "keys.fullscreen",
|
||||
when: "video",
|
||||
},
|
||||
{ id: "mute", scope: "mediaPlayer", keys: ["m"], labelKey: "keys.mute" },
|
||||
|
||||
// --- the Plex browser (drill-down) ----------------------------------------------------------
|
||||
{ id: "back", scope: "plexBrowse", keys: ["Backspace"], labelKey: "keys.browseBack" },
|
||||
] as const satisfies readonly Shortcut[];
|
||||
@@ -540,14 +558,14 @@ export function useGlobalKeymap(
|
||||
export type HelpConditions = { readonly [K in ShortcutWhen]?: boolean };
|
||||
const helpConditions = createStore<HelpConditions>({});
|
||||
|
||||
/** Publish this surface's answers while it is mounted (the Plex player: is this an episode?). The
|
||||
* sheet lives in App and cannot ask, so the surface tells it. */
|
||||
/** Publish this surface's answers while it is mounted (the Plex player: is this an episode? the
|
||||
* library's: is it a video?). The sheet lives in App and cannot ask, so the surface tells it. */
|
||||
export function useHelpConditions(conditions: HelpConditions): void {
|
||||
const episode = conditions.episode;
|
||||
const { episode, video } = conditions;
|
||||
useEffect(() => {
|
||||
helpConditions.set({ episode });
|
||||
helpConditions.set({ episode, video });
|
||||
return () => helpConditions.set({});
|
||||
}, [episode]);
|
||||
}, [episode, video]);
|
||||
}
|
||||
|
||||
// ── The module's search box ───────────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user