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
@@ -53,11 +53,13 @@ vi.mock("../../features/player/PlayerModal", async () => {
return {
default: function Player({
video,
queue,
onState,
onWrite,
onClose,
}: {
video: { id: string };
queue?: { id: string }[];
onState: (id: string, status: string, v?: unknown) => void;
onWrite?: (write: Promise<unknown>) => void;
onClose: () => void;
@@ -71,6 +73,9 @@ vi.mock("../../features/player/PlayerModal", async () => {
);
return (
<div>
<p>
playing {video.id} in {queue ? queue.map((v) => v.id).join(",") : "no list"}
</p>
<button onClick={() => onState(video.id, "watched")}>mark</button>
<button
onClick={() =>
@@ -116,6 +121,7 @@ afterEach(() => {
player.watchOnUnmount = false;
player.flush = null;
clearAll();
window.history.replaceState(null, "", "/");
});
// A cached related list (the player's, kept for the session) holding these videos.
@@ -190,6 +196,23 @@ function mount({
return { qc, feed, summary };
}
// The URL is the view: a reload or a bookmark of the player reopens it once the list is in.
describe("Feed — a player the address names", () => {
it("reopens on its video, the list as its queue", async () => {
window.history.replaceState(null, "", "/?watch=v1");
mount();
expect(await screen.findByText("playing v1 in v1")).toBeTruthy();
});
it("plays a video the loaded list does not hold alone, from the catalog", async () => {
window.history.replaceState(null, "", "/?watch=far");
const row = vi.spyOn(api, "videoRow").mockResolvedValue({ ...video("new"), id: "far" });
mount();
expect(await screen.findByText("playing far in no list")).toBeTruthy();
expect(row).toHaveBeenCalledWith("far");
});
});
describe("Feed — a video marked watched in the player", () => {
it("stays out of an Unwatched view when the player writes its position into the cache", async () => {
const { qc } = mount();