import { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { useInfiniteQuery, useQuery } from "@tanstack/react-query"; import { ArrowLeft, Film, Search, Tv2 } from "lucide-react"; import { api, type FeedFilters, type PlexCard } from "../lib/api"; import { notify } from "../lib/notifications"; import { useDebounced } from "../lib/useDebounced"; // Plex mode of the feed: browse/search the mirrored Plex library as feed-style cards, drill from a // show into seasons/episodes. Rendered by App in place of when filters.librarySource==="plex" // (so the video-feed hooks never run in Plex mode). Playback lands in P2 — for now a card just // announces it; shows drill down. Self-contained: its own library scope + search + sort + paging. type Props = { filters: FeedFilters; setFilters: (f: FeedFilters) => void; }; const PAGE = 40; function secs(n?: number | null): string { if (!n) return ""; const h = Math.floor(n / 3600); const m = Math.floor((n % 3600) / 60); return h ? `${h}h ${m}m` : `${m}m`; } export default function PlexBrowse({ filters, setFilters }: Props) { const { t } = useTranslation(); const libsQ = useQuery({ queryKey: ["plex-libraries"], queryFn: api.plexLibraries }); const libs = useMemo(() => libsQ.data?.libraries ?? [], [libsQ.data]); const [library, setLibrary] = useState(null); const [q, setQ] = useState(""); const [sort, setSort] = useState("added"); const [openShow, setOpenShow] = useState(null); const dq = useDebounced(q.trim(), 350); // Default to the first library once loaded. useEffect(() => { if (library === null && libs.length) setLibrary(libs[0].key); }, [libs, library]); const activeLib = libs.find((l) => l.key === library); const browseQ = useInfiniteQuery({ queryKey: ["plex-browse", library, dq, sort], enabled: !!library && !openShow, initialPageParam: 0, queryFn: ({ pageParam }) => api.plexBrowse({ library: library!, q: dq || undefined, sort, offset: pageParam as number, limit: PAGE }), getNextPageParam: (last) => { const seen = last.offset + last.items.length; return seen < last.total ? seen : undefined; }, }); const items = browseQ.data?.pages.flatMap((p) => p.items) ?? []; const total = browseQ.data?.pages[0]?.total ?? 0; function exitPlex() { setFilters({ ...filters, librarySource: "organic", q: "" }); } function onPlay(card: PlexCard) { // P2 wires the real player; announce for now so the flow is visible in P1. notify({ level: "info", message: t("plex.playerSoon", { title: card.title }) }); } function onCard(card: PlexCard) { if (card.type === "show") setOpenShow(card.id); else onPlay(card); } if (openShow) { return setOpenShow(null)} onPlay={onPlay} onExit={exitPlex} />; } return (
{/* Toolbar: back-to-YouTube, library scope, search, sort */}

{dq ? t("plex.searchCount", { count: total }) : t("plex.count", { count: total })}

{browseQ.isLoading ? (

{t("plex.loading")}

) : items.length === 0 ? (

{t("plex.empty")}

) : (
{items.map((c) => ( onCard(c)} /> ))}
)} {browseQ.hasNextPage && (
)}
); } const PLAYABLE_TINT: Record = { direct: "bg-emerald-500", remux: "bg-amber-500", transcode: "bg-orange-600", }; function PlexPosterCard({ card, onClick }: { card: PlexCard; onClick: () => void }) { const { t } = useTranslation(); const inProgress = (card.position_seconds ?? 0) > 0 && card.status !== "watched"; const pct = inProgress && card.duration_seconds ? Math.min(100, Math.round(((card.position_seconds ?? 0) / card.duration_seconds) * 100)) : 0; return ( ); } function PlexShowView({ showId, onBack, onPlay, onExit, }: { showId: string; onBack: () => void; onPlay: (c: PlexCard) => void; onExit: () => void; }) { const { t } = useTranslation(); const q = useQuery({ queryKey: ["plex-show", showId], queryFn: () => api.plexShow(showId) }); const d = q.data; return (
{q.isLoading || !d ? (

{t("plex.loading")}

) : ( <>

{d.show.title}

{d.show.year &&

{d.show.year}

} {d.show.summary && (

{d.show.summary}

)}
{d.seasons.map((se) => (

{se.title}

{se.episodes.map((ep) => { const inProgress = (ep.position_seconds ?? 0) > 0 && ep.status !== "watched"; return ( ); })}
))} )}
); }