import { useLayoutEffect, useRef, useState, type ReactNode } from "react"; import { useTranslation } from "react-i18next"; import { useQueryClient } from "@tanstack/react-query"; import { SlidersHorizontal } from "lucide-react"; import { useDismiss } from "./useDismiss"; import { api } from "./api"; // Shared "detail page" UI reused by the movie info page (PlexInfo) and the series show/season pages, // so they look consistent (the standing glassy/HTPC brief): the faint fixed art backdrop, the two // per-user display prefs (art background / cast row — same keys as the movie info page), and the // customize menu that toggles them. export function useDetailPrefs() { const qc = useQueryClient(); const prefs = (qc.getQueryData<{ preferences?: Record }>(["me"])?.preferences ?? {}) as { plexInfoArtBg?: boolean; plexInfoCast?: boolean; plexInfoRelated?: boolean; }; const [artBg, setArtBg] = useState(prefs.plexInfoArtBg !== false); const [showCast, setShowCast] = useState(prefs.plexInfoCast !== false); const [showRelated, setShowRelated] = useState(prefs.plexInfoRelated !== false); const save = (patch: Record) => { api.savePrefs(patch).catch(() => {}); qc.setQueryData<{ preferences?: Record } | undefined>(["me"], (m) => m ? { ...m, preferences: { ...(m.preferences || {}), ...patch } } : m, ); }; return { artBg, showCast, showRelated, savePref: save, // expose the raw persister for callers with extra per-user prefs (e.g. strip sources) toggleArtBg: () => { const next = !artBg; setArtBg(next); save({ plexInfoArtBg: next }); }, toggleCast: () => { const next = !showCast; setShowCast(next); save({ plexInfoCast: next }); }, toggleRelated: () => { const next = !showRelated; setShowRelated(next); save({ plexInfoRelated: next }); }, }; } // A metadata value that filters the grid when clicked (if onClick is wired), else plain text. Shared // by the movie info page (PlexInfo) and the unified grid / show pages (PlexBrowse). The optional // `title` gives a hover hint (e.g. "Filter by 1998"); callers that don't need it just omit it. export function Filterable({ onClick, title, className, children, }: { onClick?: () => void; title?: string; className?: string; children: ReactNode; }) { if (!onClick) return {children}; return ( ); } // Paint the item's art as a faint FIXED backdrop on the page's
scroll container (HTPC-style), // so the glass panels float over it. Cleared on unmount / when disabled / when there's no art. export function useArtBackdrop(art: string | null | undefined, enabled: boolean) { useLayoutEffect(() => { const main = document.querySelector("main"); if (!main) return; const s = main.style; const clear = () => { s.backgroundImage = ""; s.backgroundSize = ""; s.backgroundPosition = ""; s.backgroundRepeat = ""; s.backgroundAttachment = ""; }; if (enabled && art) { s.backgroundImage = `linear-gradient(color-mix(in srgb, var(--bg) 60%, transparent), ` + `color-mix(in srgb, var(--bg) 64%, transparent)), url("${art}")`; s.backgroundSize = "cover"; s.backgroundPosition = "center 20%"; s.backgroundRepeat = "no-repeat"; s.backgroundAttachment = "fixed"; } else { clear(); } return clear; }, [art, enabled]); } export function DetailCustomizeMenu({ artBg, onToggleArtBg, showCast, onToggleCast, hasCast, showRelated, onToggleRelated, hasRelated, overlay, extra, }: { artBg: boolean; onToggleArtBg: () => void; showCast: boolean; onToggleCast: () => void; hasCast: boolean; showRelated?: boolean; onToggleRelated?: () => void; hasRelated?: boolean; overlay?: boolean; // white-on-dark button styling when floated over the player extra?: ReactNode; // additional toggles rendered after the built-in ones (e.g. strip-source buckets) }) { const { t } = useTranslation(); const [open, setOpen] = useState(false); const btnRef = useRef(null); const menuRef = useRef(null); useDismiss(open, () => setOpen(false), [menuRef, btnRef]); return (
{open && (
{hasCast && } {hasRelated && onToggleRelated && ( )} {extra}
)}
); } export function PrefToggle({ label, on, onClick }: { label: string; on: boolean; onClick: () => void }) { return ( ); }