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
+22 -5
View File
@@ -80,6 +80,8 @@ import ViewSwitcher from "../../components/ui/ViewSwitcher";
import VirtualFeed from "./VirtualFeed";
import SegmentedToggle from "../../components/ui/SegmentedToggle";
import { IconButton } from "../../components/ui/IconButton";
import { useOpenLinkedPlayer } from "../../features/player/useOpenLinkedPlayer";
// Lazy: the in-app YouTube player (IFrame API) loads only when a video is first opened.
const PlayerModal = lazy(() => import("../../features/player/PlayerModal"));
@@ -209,10 +211,13 @@ export default function Feed({
// A reset's position 0 lives in the cached lists (see onResetState), where a refetch that set out
// before the server took the reset puts the old position back: written again on its landing.
const resets = useRef(new Map<string, number>());
// The open player: which video and where to start (null = resume from saved position).
const [activeVideo, setActiveVideo] = useState<{ video: Video; startAt: number | null } | null>(
null,
);
// The open player: which video and where to start (null = resume from saved position). `alone`:
// opened from an address naming a video this list has not loaded, so it plays without the queue.
const [activeVideo, setActiveVideo] = useState<{
video: Video;
startAt: number | null;
alone?: boolean;
} | null>(null);
const qc = useQueryClient();
const videoMarks = useVideoMarks();
// Live search turned off on this instance: the empty feed offers no search that would be refused.
@@ -509,6 +514,18 @@ export default function Feed({
// sequence (no reindex/reload mid-play), matching "watched doesn't affect the list". A video that
// goes hidden still drops out ("visible affects").
const playerQueue: Video[] = merged.filter((v) => v.status !== "hidden");
// A reload or a bookmark of the player reopens it once this list is in, the list as its queue again
// (the feed's filters are in the address under it). Not over a live search: that is not in the
// address, and a reload leaves it.
useOpenLinkedPlayer(
!ytActive && !query.isLoading,
(id) => playerQueue.find((v) => v.id === id),
(video, inList) => {
openVideo(video);
// Not in what this list has loaded (further down it, or no longer in it): it plays alone.
if (!inList) setActiveVideo((a) => a && { ...a, alone: true });
},
);
// A live search ingests new catalog videos, so the disabled feed queries still hold their stale
// pre-search cache. Drop them so the feed re-fetches fresh on return.
@@ -1006,7 +1023,7 @@ export default function Feed({
<PlayerModal
video={activeVideo.video}
startAt={activeVideo.startAt}
queue={playerQueue}
queue={activeVideo.alone ? undefined : playerQueue}
onClose={closePlayer}
onState={onState}
onWrite={trackSessionWrite}