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
+19 -15
View File
@@ -1,30 +1,34 @@
import { decodeSortParam, encodeSortParam, type SortState } from "./columnSort";
// The Files page's address-bar keys: the folder shown (`?folder=`, its encoding in
// modules/files/filesContext), the file it turns to once (`?item=`), the name search (`?fq=`) and the
// sort (`?fsort=`). Here, beside the other pages' key lists, so the page's definition can declare them
// without importing its own folder.
// sort (`?fsort=`, always written, lib/columnSort's `none` for the list's own order). Here, beside the
// other pages' key lists, so the page's definition can declare them without importing its own folder.
export const FILES_FOLDER_KEY = "folder";
export const FILES_ITEM_KEY = "item";
export const FILES_SEARCH_KEY = "fq";
export const FILES_SORT_KEY = "fsort";
// The file open over the list: in the viewer, or in the player. Each dialog names itself in its own
// history entry (ui/Modal's `url`), so Back lands on the list's address without it.
export const FILES_VIEW_KEY = "fview";
export const FILES_PLAY_KEY = "fplay";
export const FILES_KEYS = [
FILES_FOLDER_KEY,
FILES_ITEM_KEY,
FILES_SEARCH_KEY,
FILES_SORT_KEY,
FILES_VIEW_KEY,
FILES_PLAY_KEY,
] as const;
// The list's own order (no column sort) has a word of its own: the sort is always written, because a
// page opened without one takes the account's remembered sort, which may not be the one bookmarked.
const UNSORTED = "none";
export type FilesOpenFile = { open: "view" | "play"; id: number };
export function encodeFilesSort(sort: SortState): string {
return encodeSortParam(sort) ?? UNSORTED;
}
/** The sort an address names; `undefined` when it names none (the remembered one applies). */
export function decodeFilesSort(raw: string | null): SortState | undefined {
if (raw == null) return undefined;
return raw === UNSORTED ? null : decodeSortParam(raw);
/** The file an address opens over the list (a reload, a bookmark), or null. */
export function openFileFrom(params: URLSearchParams): FilesOpenFile | null {
for (const [open, key] of [
["play", FILES_PLAY_KEY],
["view", FILES_VIEW_KEY],
] as const) {
const id = Number(params.get(key));
if (Number.isInteger(id) && id > 0) return { open, id };
}
return null;
}