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
@@ -50,6 +50,7 @@ import {
} from "../../lib/playlistsUrlState";
import { formatRuntime } from "../../lib/format";
import { applyView, sameOrder } from "../../lib/playlistView";
import { useOpenLinkedPlayer } from "../../features/player/useOpenLinkedPlayer";
const PlayerModal = lazy(() => import("../../features/player/PlayerModal"));
import { Button } from "../../components/ui/Button";
import { LoadingState, StateMessage } from "../../components/ui/QueryState";
@@ -580,6 +581,17 @@ function PlaylistDetail() {
shownRef.current = shown;
removeRef.current = removeItem;
});
// A reload or a bookmark of the player reopens it once this playlist is in, its view as the queue
// again (the playlist and its view are in the address under it). A video the view does not show
// (filtered out, gone from the list) plays alone, and nothing removes it from a list.
useOpenLinkedPlayer(
detail != null && items.length > 0,
(id) => shown.find((v) => v.id === id),
(video, inList) => {
if (inList) openPlayer(video.id);
else setPlaying({ video, queue: [video], index: 0, listId: selectedId, editable: false });
},
);
async function moveItemTo(video: Video, target: Playlist) {
if (selectedId == null) return;