feat(player): a video's related videos, on demand beside it
The player shows what YouTube lists beside the video, without what the user has watched or hidden. Wide, a Related tab beside the description; stacked, a button beside the info opens them in a drawer. A row plays in the same player (Back returns) and the list stays the opened video's. A switch picks on-topic or Hungarian-trending lists, kept on the account. Each related row says whether the user follows its channel; the row is the video card's narrow form with its whole menu. VideoMenu moves to features/video, where the player can reach it.
This commit is contained in:
@@ -23,6 +23,7 @@ import {
|
||||
CheckCheck,
|
||||
ExternalLink,
|
||||
Info,
|
||||
ListVideo,
|
||||
Maximize,
|
||||
Minimize,
|
||||
Minimize2,
|
||||
@@ -83,6 +84,10 @@ import { SURFACE } from "../../components/ui/surfaces";
|
||||
import { IconLink, LinkButton } from "../../components/ui/LinkButton";
|
||||
import { SectionLabel } from "../../components/ui/Headings";
|
||||
import { useMediaQuery } from "../../lib/useMediaQuery";
|
||||
import Modal from "../../components/ui/Modal";
|
||||
import Tabs from "../../components/ui/Tabs";
|
||||
import { useTabPanel } from "../../components/ui/tabPanel";
|
||||
import { RelatedVideos } from "./RelatedVideos";
|
||||
|
||||
// Experiment (branch experiment/inline-player): play the video in-app via the
|
||||
// YouTube IFrame Player API (not a bare embed) so we can read playback position
|
||||
@@ -669,6 +674,17 @@ export default function PlayerModal({
|
||||
// a tablet could not read a description at all. Anchored ABOVE, so it grows upward over the player
|
||||
// rather than off-screen; `flip` still rescues it when there is no room up there.
|
||||
const [showDesc, setShowDesc] = useState(false);
|
||||
// The related videos, on demand (the owner's pick: the modal has little room to spare). Wide, they
|
||||
// take the side panel's place of the description, a tab beside it; stacked, a drawer over the video
|
||||
// opened from a button beside the ⓘ. The list is the OPENED video's: one played from it leaves the
|
||||
// list in place, to try the next one (the owner's pick).
|
||||
const [panelTab, setPanelTab] = useState<"description" | "related">("description");
|
||||
const [relatedOpen, setRelatedOpen] = useState(false);
|
||||
const panelTabs = useTabPanel();
|
||||
const playRelated = (v: Video, startAt: number) => {
|
||||
setRelatedOpen(false);
|
||||
loadVideo(v.id, startAt);
|
||||
};
|
||||
const descPop = useAnchoredPopover({
|
||||
open: showDesc,
|
||||
onOpenChange: setShowDesc,
|
||||
@@ -1245,6 +1261,29 @@ export default function PlayerModal({
|
||||
<Info />
|
||||
</IconButton>
|
||||
)}
|
||||
{!wide && (
|
||||
<IconButton
|
||||
variant="ghost"
|
||||
onClick={() => setRelatedOpen(true)}
|
||||
aria-expanded={relatedOpen}
|
||||
label={t("player.related.title")}
|
||||
>
|
||||
<ListVideo />
|
||||
</IconButton>
|
||||
)}
|
||||
{/* Inside the card, whose click stops at its edge: a portal's events still bubble
|
||||
along the React tree, and from the backdrop a click on a row closed the player. */}
|
||||
{relatedOpen && !wide && (
|
||||
<Modal
|
||||
title={t("player.related.title")}
|
||||
onClose={() => setRelatedOpen(false)}
|
||||
placement="drawer"
|
||||
maxWidth="max-w-sm"
|
||||
initialFocus="close"
|
||||
>
|
||||
<RelatedVideos videoId={active.id} onPlay={playRelated} />
|
||||
</Modal>
|
||||
)}
|
||||
{navigated && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -1455,16 +1494,35 @@ export default function PlayerModal({
|
||||
)}
|
||||
</div>
|
||||
{wide && (
|
||||
<section className="mt-5" data-testid="player-description">
|
||||
<SectionLabel level={3} className="mb-2">
|
||||
{t("player.description")}
|
||||
</SectionLabel>
|
||||
<PlayerDescription
|
||||
className="text-sm whitespace-pre-wrap wrap-break-word leading-relaxed"
|
||||
clamp
|
||||
{...descriptionProps}
|
||||
<div className="mt-5">
|
||||
<Tabs
|
||||
tabs={[
|
||||
{ id: "description", label: t("player.description") },
|
||||
{ id: "related", label: t("player.related.title") },
|
||||
]}
|
||||
active={panelTab}
|
||||
onChange={(id) => setPanelTab(id === "related" ? "related" : "description")}
|
||||
idBase={panelTabs.idBase}
|
||||
label={t("player.panelTabs")}
|
||||
className="mb-3"
|
||||
/>
|
||||
</section>
|
||||
<div {...panelTabs.panelProps(panelTab)}>
|
||||
{panelTab === "description" ? (
|
||||
<section data-testid="player-description">
|
||||
<PlayerDescription
|
||||
className="text-sm whitespace-pre-wrap wrap-break-word leading-relaxed"
|
||||
clamp
|
||||
{...descriptionProps}
|
||||
/>
|
||||
</section>
|
||||
) : (
|
||||
// Out into the panel's padding: in 20rem, every rem is the title's.
|
||||
<div className="-mx-3">
|
||||
<RelatedVideos videoId={active.id} onPlay={playRelated} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user