Add split overlay scraping part with title and info

This commit is contained in:
Pas 2026-02-21 22:23:07 -07:00
parent 24132618fd
commit b3c2d58f8d

View file

@ -9,12 +9,15 @@ import {
scrapePartsToProviderMetric,
useReportProviders,
} from "@/backend/helpers/report";
import { getMediaDetails, getMediaLogo } from "@/backend/metadata/tmdb";
import { TMDBContentTypes } from "@/backend/metadata/types/tmdb";
import { Button } from "@/components/buttons/Button";
import { Loading } from "@/components/layout/Loading";
import {
ScrapeCard,
ScrapeItem,
} from "@/components/player/internals/ScrapeCard";
import { useIsMobile } from "@/hooks/useIsMobile";
import {
ScrapingItems,
ScrapingSegment,
@ -23,6 +26,12 @@ import {
} from "@/hooks/useProviderScrape";
import { playerStatus } from "@/stores/player/slices/source";
import { usePlayerStore } from "@/stores/player/store";
import { usePreferencesStore } from "@/stores/preferences";
interface ScrapingMediaDetails {
voteAverage: number | null;
genres: string[];
}
export interface ScrapingProps {
media: ScrapeMedia;
@ -43,6 +52,66 @@ export function ScrapingPart(props: ScrapingProps) {
const setStatus = usePlayerStore((s) => s.setStatus);
const addFailedSource = usePlayerStore((s) => s.addFailedSource);
const sourceId = usePlayerStore((s) => s.sourceId);
const meta = usePlayerStore((s) => s.meta);
const enablePauseOverlay = usePreferencesStore((s) => s.enablePauseOverlay);
const enableImageLogos = usePreferencesStore((s) => s.enableImageLogos);
const { isMobile } = useIsMobile();
const showMediaColumn = enablePauseOverlay && !isMobile && !!meta;
const [logoUrl, setLogoUrl] = useState<string | null>(null);
const [details, setDetails] = useState<ScrapingMediaDetails>({
voteAverage: null,
genres: [],
});
useEffect(() => {
if (!showMediaColumn || !meta?.tmdbId) return;
let mounted = true;
const fetchLogo = async () => {
if (!enableImageLogos) {
setLogoUrl(null);
return;
}
try {
const type =
meta.type === "movie" ? TMDBContentTypes.MOVIE : TMDBContentTypes.TV;
const url = await getMediaLogo(meta.tmdbId, type);
if (mounted) setLogoUrl(url || null);
} catch {
if (mounted) setLogoUrl(null);
}
};
fetchLogo();
return () => {
mounted = false;
};
}, [showMediaColumn, meta?.tmdbId, meta?.type, enableImageLogos]);
useEffect(() => {
if (!showMediaColumn || !meta?.tmdbId) return;
let mounted = true;
const fetchDetails = async () => {
try {
const type =
meta.type === "movie" ? TMDBContentTypes.MOVIE : TMDBContentTypes.TV;
const data = await getMediaDetails(meta.tmdbId, type, false);
if (mounted && data) {
const voteAverage =
typeof data.vote_average === "number" ? data.vote_average : null;
const genres = (data.genres ?? []).map(
(g: { name: string }) => g.name,
);
setDetails({ voteAverage, genres });
}
} catch {
if (mounted) setDetails({ voteAverage: null, genres: [] });
}
};
fetchDetails();
return () => {
mounted = false;
};
}, [showMediaColumn, meta?.tmdbId, meta?.type]);
const containerRef = useRef<HTMLDivElement | null>(null);
const listRef = useRef<HTMLDivElement | null>(null);
@ -125,65 +194,128 @@ export function ScrapingPart(props: ScrapingProps) {
if (currentProviderIndex === -1)
currentProviderIndex = sourceOrder.length - 1;
const overview =
meta && (meta.type === "show" ? meta.episode?.overview : meta.overview);
const hasMediaDetails =
details.voteAverage !== null || details.genres.length > 0;
const hasMediaContent =
showMediaColumn &&
meta &&
(overview || logoUrl || meta.title || hasMediaDetails);
return (
<div
className="h-full w-full relative dir-neutral:origin-top-left flex"
className={classNames(
"h-full w-full relative dir-neutral:origin-top-left flex",
showMediaColumn && "gap-8 lg:gap-12",
)}
ref={containerRef}
>
{!sourceOrder || sourceOrder.length === 0 ? (
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 text-center flex flex-col justify-center z-0">
<Loading className="mb-8" />
<p>{t("player.scraping.items.pending")}</p>
{showMediaColumn && hasMediaContent && (
<div className="flex-shrink-0 w-80 max-w-[min(20rem,40%)] flex items-center py-6">
<div className="max-w-sm">
{logoUrl ? (
<img
src={logoUrl}
alt={meta.title}
className="mb-6 max-h-32 object-contain drop-shadow-lg"
/>
) : (
<h1 className="mb-4 text-4xl font-bold text-white drop-shadow-lg">
{meta.title}
</h1>
)}
{meta.type === "show" && meta.episode && (
<h2 className="mb-2 text-2xl font-semibold text-white/90 drop-shadow-md">
{meta.episode.title}
</h2>
)}
{hasMediaDetails && (
<div className="mb-3 flex flex-wrap items-center gap-x-2 gap-y-1 text-sm text-white/80 drop-shadow-md">
{details.voteAverage !== null && (
<span>
{details.voteAverage.toFixed(1)}
<span className="text-white/60 ml-0.5">/10</span>
</span>
)}
{details.genres.length > 0 && (
<>
{details.voteAverage !== null && (
<span className="text-white/60"></span>
)}
<span>{details.genres.slice(0, 4).join(", ")}</span>
</>
)}
</div>
)}
{overview && (
<p className="text-lg text-white/80 drop-shadow-md line-clamp-6">
{overview}
</p>
)}
</div>
</div>
) : null}
<div
className={classNames({
"absolute transition-[transform,opacity] opacity-0 dir-neutral:left-0": true,
"!opacity-100": renderedOnce,
})}
ref={listRef}
>
{sourceOrder.map((order) => {
const source = sources[order.id];
const distance = Math.abs(
sourceOrder.findIndex((o) => o.id === order.id) -
currentProviderIndex,
);
return (
<div
className="transition-opacity duration-100"
style={{ opacity: Math.max(0, 1 - distance * 0.3) }}
key={order.id}
>
<ScrapeCard
id={order.id}
name={source.name}
status={source.status}
hasChildren={order.children.length > 0}
percentage={source.percentage}
)}
<div className="flex-1 min-w-0 relative flex">
{!sourceOrder || sourceOrder.length === 0 ? (
<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 text-center flex flex-col justify-center z-0">
<Loading className="mb-8" />
<p>{t("player.scraping.items.pending")}</p>
</div>
) : null}
<div
className={classNames({
"absolute transition-[transform,opacity] opacity-0 dir-neutral:left-0": true,
"!opacity-100": renderedOnce,
})}
ref={listRef}
>
{sourceOrder.map((order) => {
const source = sources[order.id];
const distance = Math.abs(
sourceOrder.findIndex((o) => o.id === order.id) -
currentProviderIndex,
);
return (
<div
className="transition-opacity duration-100"
style={{ opacity: Math.max(0, 1 - distance * 0.3) }}
key={order.id}
>
<div
className={classNames({
"space-y-6 mt-8": order.children.length > 0,
})}
<ScrapeCard
id={order.id}
name={source.name}
status={source.status}
hasChildren={order.children.length > 0}
percentage={source.percentage}
>
{order.children.map((embedId) => {
const embed = sources[embedId];
return (
<ScrapeItem
id={embedId}
name={embed.name}
status={embed.status}
percentage={embed.percentage}
key={embedId}
/>
);
})}
</div>
</ScrapeCard>
</div>
);
})}
<div
className={classNames({
"space-y-6 mt-8": order.children.length > 0,
})}
>
{order.children.map((embedId) => {
const embed = sources[embedId];
return (
<ScrapeItem
id={embedId}
name={embed.name}
status={embed.status}
percentage={embed.percentage}
key={embedId}
/>
);
})}
</div>
</ScrapeCard>
</div>
);
})}
</div>
</div>
</div>
);