feat(player): the YouTube player and the file viewer are named in the address

The player names what it plays in its own history entry (watch, wrel, wtab): a reload or a
bookmark reopens it on the page that hosts it, with that page's list as the queue, or alone
from the catalog (GET /api/videos/{id}/row); a page that hosts none gets the shell's. Back
lands on the page's address without it (useBackToClose's url; ui/Modal passes it on). The
Files viewer and player do the same (fview, fplay). The Plex and channel-manager views are
always written in full, as their fallback is remembered state.
This commit is contained in:
2026-10-27 05:29:49 +01:00
parent d4f24de4ba
commit fa893c73a3
45 changed files with 1005 additions and 89 deletions
+42
View File
@@ -0,0 +1,42 @@
// The YouTube player's address-bar keys. The player opens over any page (the feed, a channel page, Home,
// a playlist, a channel's watched videos), so its keys are the shell's: no module owns them, a page
// switch clears them, and the page under it keeps its own. The player names them in its own history
// entry (lib/history's `useBackToClose`), so Back lands on the page as it was, without the player.
// The list it plays from is the page's: the feed's filters, the open playlist (`pl`) — already named.
// No position: the player resumes from the server's (`video_states`), which is where it was left.
export const PLAYER_WATCH_KEY = "watch"; // the item of the list playing now
export const PLAYER_RELATED_KEY = "wrel"; // a related video played in place over it
export const PLAYER_PANEL_KEY = "wtab"; // the wide panel's tab, when it is not the description
export const PLAYER_KEYS = [PLAYER_WATCH_KEY, PLAYER_RELATED_KEY, PLAYER_PANEL_KEY] as const;
export type PlayerPanelTab = "description" | "related";
export type PlayerUrlState = { watch: string; related: string | null; panel: PlayerPanelTab };
/** The player's keys for its entry. The panel's tab is written only when it is not the default: no
* browser remembers it, so a link without one opens the description as this one would. */
export function playerParamUpdates(state: PlayerUrlState): Record<string, string | null> {
return {
[PLAYER_WATCH_KEY]: state.watch,
[PLAYER_RELATED_KEY]: state.related,
[PLAYER_PANEL_KEY]: state.panel === "related" ? "related" : null,
};
}
/** The player an address names, or null when it names none. */
export function playerFrom(params: URLSearchParams): PlayerUrlState | null {
const watch = params.get(PLAYER_WATCH_KEY);
if (!watch) return null;
return {
watch,
related: params.get(PLAYER_RELATED_KEY) || null,
panel: params.get(PLAYER_PANEL_KEY) === "related" ? "related" : "description",
};
}
/** The player the address names now: read by a host as it mounts (a reload, a bookmark), and by the
* player as it opens. No entry but the player's own carries these keys once the history has caught
* up, so a host mounted later (a page switch back) finds none. */
export function linkedPlayer(): PlayerUrlState | null {
return playerFrom(new URLSearchParams(window.location.search));
}