"use client";
import type { ReactNode } from "react";
import { Badge, Box, Center, Group, Image, Paper, RingProgress, ScrollArea, SimpleGrid, Stack, Tabs, Text, ThemeIcon } from "@mantine/core";
import { IconDownload, IconMovie, IconQuestionMark, IconVideo } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client";
import type { MissingMediaItem, QueuedMediaItem } from "@homarr/integrations/types";
import { useScopedI18n } from "@homarr/translation/client";
import { WidgetEmptyState } from "../common/empty-state";
import type { WidgetComponentProps } from "../definition";
import { NoIntegrationDataError } from "../errors/no-data-integration";
import classes from "./component.module.css";
export default function MediaMissingWidget({ integrationIds, options, width, height }: WidgetComponentProps<"mediaMissing">) {
const t = useScopedI18n("widget.mediaMissing");
const pageSize = Number(options.pageSize);
const { data } = clientApi.widget.mediaOrganizer.getData.useQuery(
{ integrationIds, pageSize },
{ staleTime: 60 * 1000, refetchOnWindowFocus: false, refetchOnReconnect: false },
);
if (!data) return ;
if (data.length === 0) throw new NoIntegrationDataError();
if (!options.showMissing && !options.showQueued)
return (
{t("empty.noTabsEnabled")}
);
const missing = data.flatMap((entry) =>
entry.missing.map((item) => ({ item, integrationId: entry.integrationId })),
);
const queued = data.flatMap((entry) =>
entry.queued.map((item) => ({ item, integrationId: entry.integrationId })),
);
const missingCount = data.reduce((sum, entry) => sum + entry.missingCount, 0);
const queuedCount = data.reduce((sum, entry) => sum + entry.queuedCount, 0);
const isThin = width > 0 && width < 160;
const isShort = height > 0 && height < 180;
const targetCardWidth = isShort ? 130 : 200;
const columns = width > 0 ? Math.max(1, Math.min(Math.floor(width / targetCardWidth), 4)) : 1;
const density: Density = isThin ? "thin" : width > 0 && width / columns < 180 ? "compact" : "comfortable";
const tabLabel = (label: string, shown: number, total: number) =>
isThin ? total : `${label} (${shown}/${total})`;
const renderPanel = (entries: { item: MissingMediaItem | QueuedMediaItem; integrationId: string }[], emptyLabel: string) => (
{entries.length === 0 ? (
{emptyLabel}
) : (
{entries.map(({ item, integrationId }) => (
))}
)}
);
return (
{options.showMissing && (
}>
{tabLabel(t("tab.missing"), missing.length, missingCount)}
)}
{options.showQueued && (
}>
{tabLabel(t("tab.queued"), queued.length, queuedCount)}
)}
{options.showMissing && (
{renderPanel(missing, t("empty.missing"))}
)}
{options.showQueued && (
{renderPanel(queued, t("empty.queued"))}
)}
);
}
type Density = "thin" | "compact" | "comfortable";
const CARD_HEIGHT: Record = { thin: 52, compact: 56, comfortable: 68 };
const posterSizes: Record = { thin: 34, compact: 40, comfortable: 52 };
const Poster = ({ src, type, density }: { src?: string | null; type: "movie" | "episode"; density: Density }) => {
const size = posterSizes[density];
const w = Math.round(size * 0.68);
if (src) {
return ;
}
return (
{type === "movie" ? : }
);
};
const episodeCode = (item: MissingMediaItem | QueuedMediaItem) =>
item.type === "episode" && item.seasonNumber !== undefined && item.episodeNumber !== undefined
? `S${String(item.seasonNumber).padStart(2, "0")}E${String(item.episodeNumber).padStart(2, "0")}`
: null;
const TypeBadge = ({ item, density }: { item: MissingMediaItem | QueuedMediaItem; density: Density }) => {
const t = useScopedI18n("widget.mediaMissing");
const color = item.type === "movie" ? "yellow" : "blue";
const code = episodeCode(item);
if (density !== "comfortable") {
return (
{code ?? t(`type.${item.type}`)}
);
}
return (
{t(`type.${item.type}`)}
{code && (
{code}
)}
);
};
const primaryTitle = (item: MissingMediaItem | QueuedMediaItem) =>
item.type === "episode" ? (item.seriesTitle ?? item.title) : item.title;
const progressColor = (percent: number) => (percent >= 90 ? "green" : percent >= 40 ? "cyan" : "orange");
const ringSizes: Record = {
thin: { size: 30, thickness: 3, fz: "8px" },
compact: { size: 34, thickness: 3, fz: "9px" },
comfortable: { size: 40, thickness: 4, fz: "10px" },
};
const ProgressRing = ({ percent, density }: { percent: number; density: Density }) => {
const color = progressColor(percent);
const ring = ringSizes[density];
return (
{percent}
}
/>
);
};
const CardShell = ({ item, density, children }: { item: MissingMediaItem | QueuedMediaItem; density: Density; children: ReactNode }) => (
{item.imageUrl && (
)}
{children}
);
const MediaCard = ({ item, density }: { item: MissingMediaItem | QueuedMediaItem; density: Density }) => {
const isQueued = "percentComplete" in item;
return (
{primaryTitle(item)}
{density === "comfortable" && (
{item.type === "episode" ? item.title : item.year}
)}
{isQueued && }
);
};