import { useEffect, useRef } from "react"; import { ExternalLink, X } from "lucide-react"; import type { Video } from "../lib/api"; import { formatDuration, formatViews, relativeTime } from "../lib/format"; // 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 // and resume where the user left off. The modal closes via the header X button, // the backdrop, or ESC (ESC only while focus is on our page, not inside the // cross-origin player iframe — a browser security boundary we can't cross). // --- IFrame Player API loader (singleton) --- let apiPromise: Promise | null = null; function loadYouTubeApi(): Promise { if (apiPromise) return apiPromise; apiPromise = new Promise((resolve) => { const w = window as any; if (w.YT && w.YT.Player) return resolve(w.YT); const prev = w.onYouTubeIframeAPIReady; w.onYouTubeIframeAPIReady = () => { if (typeof prev === "function") prev(); resolve(w.YT); }; const tag = document.createElement("script"); tag.src = "https://www.youtube.com/iframe_api"; document.head.appendChild(tag); }); return apiPromise; } // --- Per-video resume position, persisted in localStorage so it survives a refresh --- const posKey = (id: string) => `subfeed:player-pos:${id}`; function savePos(id: string, seconds: number, duration: number): void { // Don't store trivially-early or near-finished positions (start fresh next time). if (!Number.isFinite(seconds) || seconds < 5 || (duration > 0 && seconds > duration - 10)) { localStorage.removeItem(posKey(id)); return; } localStorage.setItem(posKey(id), String(Math.floor(seconds))); } function loadPos(id: string): number { const v = localStorage.getItem(posKey(id)); const n = v ? parseInt(v, 10) : 0; return Number.isFinite(n) && n > 0 ? n : 0; } export default function PlayerModal({ video, onClose, }: { video: Video; onClose: () => void; }) { const mountRef = useRef(null); const playerRef = useRef(null); // ESC + background scroll lock. useEffect(() => { const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); }; window.addEventListener("keydown", onKey); const prevOverflow = document.body.style.overflow; document.body.style.overflow = "hidden"; return () => { window.removeEventListener("keydown", onKey); document.body.style.overflow = prevOverflow; }; }, [onClose]); // Create the player, resume from the saved position, and persist progress. useEffect(() => { let cancelled = false; const id = video.id; const persist = () => { const p = playerRef.current; if (!p || typeof p.getCurrentTime !== "function") return; try { savePos(id, p.getCurrentTime(), typeof p.getDuration === "function" ? p.getDuration() : 0); } catch { /* player may be tearing down */ } }; loadYouTubeApi().then((YT) => { if (cancelled || !mountRef.current) return; playerRef.current = new YT.Player(mountRef.current, { width: "100%", height: "100%", videoId: id, playerVars: { autoplay: 1, start: loadPos(id) || undefined, rel: 0, // limit "related" to the same channel (full removal is no longer possible) enablejsapi: 1, origin: window.location.origin, playsinline: 1, }, }); }); // Periodic checkpoint so progress survives a crash/refresh, not just a clean close. const timer = window.setInterval(persist, 5000); return () => { cancelled = true; window.clearInterval(timer); persist(); const p = playerRef.current; if (p && typeof p.destroy === "function") { try { p.destroy(); } catch { /* ignore */ } } playerRef.current = null; }; }, [video.id]); return (
e.stopPropagation()} > {/* Header bar — keeps our close button off the player's own top-right controls. */}
{video.title}

{video.title}

{video.channel_thumbnail && ( )} {video.channel_title}
{video.view_count != null && {formatViews(video.view_count)} views} {video.view_count != null && ·} {relativeTime(video.published_at)} {video.duration_seconds != null && ( <> · {formatDuration(video.duration_seconds)} )} {video.live_status === "was_live" && ( stream )}
); }