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
@@ -0,0 +1,40 @@
import { lazy, Suspense, useState } from "react";
import { useTranslation } from "react-i18next";
import { useQueryClient } from "@tanstack/react-query";
import { type Video, type VideoStatus } from "../../lib/api";
import { qk } from "../../lib/queryKeys";
import { notifyActionError } from "../../lib/actionErrors";
import { useStableCallback } from "../../lib/useLatest";
import { writeVideoState } from "../video/videoWrites";
import { useOpenLinkedPlayer } from "./useOpenLinkedPlayer";
const PlayerModal = lazy(() => import("./PlayerModal"));
/** The shell's player, for an address that names one over a page that hosts none of its own (the
* channel manager, where the watched-videos dialog played it; any page a link names): the video
* alone. Decided at boot, once — `hosted` is whether the page on screen then opens it itself. */
export function LinkedPlayer({ hosted }: { hosted: boolean }) {
const { t } = useTranslation();
const qc = useQueryClient();
const [own] = useState(!hosted);
const [video, setVideo] = useState<Video | null>(null);
useOpenLinkedPlayer(
own,
() => undefined,
(v) => setVideo(v),
);
const onState = useStableCallback((id: string, status: VideoStatus) => {
const write = writeVideoState(qc, id, status);
write
.then(() => qc.invalidateQueries({ queryKey: qk.feed() }))
.catch((e: unknown) => notifyActionError(e, t("errors.writeFailed.videoState")));
return write;
});
const close = useStableCallback(() => setVideo(null));
if (!video) return null;
return (
<Suspense fallback={null}>
<PlayerModal video={video} startAt={null} onClose={close} onState={onState} />
</Suspense>
);
}