feat(player): in-app modal YouTube player with resume
Left-clicking a feed card (Ctrl/Cmd/middle still open youtube.com in a new tab) opens a modal that plays the video in-app via the YouTube IFrame Player API instead of leaving the app. Using the JS API (not a bare embed) lets us read the playback position: it's checkpointed per-video in localStorage and on close, and restored via the 'start' param when the video is reopened. The modal closes via a header button, the backdrop, or ESC (ESC only while focus is on our page — a cross-origin iframe owns its own key events).
This commit is contained in:
parent
777d818c9a
commit
9332420ddf
3 changed files with 236 additions and 4 deletions
|
|
@ -70,13 +70,33 @@ function Actions({
|
|||
);
|
||||
}
|
||||
|
||||
function Thumb({ video, className }: { video: Video; className?: string }) {
|
||||
// Plain left-click opens the in-app player (the experiment); Ctrl/Cmd/middle-click
|
||||
// keep the native "open youtube.com in a new tab" behavior via the underlying href.
|
||||
function openInApp(
|
||||
e: React.MouseEvent,
|
||||
video: Video,
|
||||
onOpen?: (v: Video) => void
|
||||
): void {
|
||||
if (!onOpen || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0) return;
|
||||
e.preventDefault();
|
||||
onOpen(video);
|
||||
}
|
||||
|
||||
function Thumb({
|
||||
video,
|
||||
className,
|
||||
onOpen,
|
||||
}: {
|
||||
video: Video;
|
||||
className?: string;
|
||||
onOpen?: (v: Video) => void;
|
||||
}) {
|
||||
return (
|
||||
<a
|
||||
href={video.watch_url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
onClick={() => video.status === "new" && undefined}
|
||||
onClick={(e) => openInApp(e, video, onOpen)}
|
||||
className={clsx(
|
||||
"block relative rounded-xl overflow-hidden bg-surface border border-border shadow-md group-hover:shadow-2xl transition-shadow",
|
||||
className
|
||||
|
|
@ -116,11 +136,13 @@ export default function VideoCard({
|
|||
view,
|
||||
onState,
|
||||
onChannelFilter,
|
||||
onOpen,
|
||||
}: {
|
||||
video: Video;
|
||||
view: "grid" | "list";
|
||||
onState: (id: string, status: string) => void;
|
||||
onChannelFilter?: (channelId: string, channelName: string) => void;
|
||||
onOpen?: (v: Video) => void;
|
||||
}) {
|
||||
const watched = video.status === "watched";
|
||||
const meta = (
|
||||
|
|
@ -134,6 +156,7 @@ export default function VideoCard({
|
|||
href={video.watch_url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
onClick={(e) => openInApp(e, video, onOpen)}
|
||||
className="font-medium leading-snug line-clamp-2 hover:text-accent"
|
||||
>
|
||||
{video.title}
|
||||
|
|
@ -148,7 +171,7 @@ export default function VideoCard({
|
|||
watched && "opacity-55"
|
||||
)}
|
||||
>
|
||||
<Thumb video={video} className="w-44 aspect-video shrink-0" />
|
||||
<Thumb video={video} className="w-44 aspect-video shrink-0" onOpen={onOpen} />
|
||||
<div className="min-w-0 flex-1">
|
||||
{title}
|
||||
<a
|
||||
|
|
@ -174,7 +197,7 @@ export default function VideoCard({
|
|||
watched && "opacity-55"
|
||||
)}
|
||||
>
|
||||
<Thumb video={video} className="aspect-video" />
|
||||
<Thumb video={video} className="aspect-video" onOpen={onOpen} />
|
||||
<div className="flex gap-3 mt-2.5 px-0.5 pb-0.5">
|
||||
{video.channel_thumbnail && (
|
||||
<img
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue