117 lines
3.7 KiB
TypeScript
117 lines
3.7 KiB
TypeScript
"use client";
|
|
|
|
import type { CSSProperties } from "react";
|
|
import { Text } from "@mantine/core";
|
|
import { IconAlertTriangle, IconDeviceTv, IconMovie, IconPlugConnected } from "@tabler/icons-react";
|
|
|
|
import { clientApi } from "@homarr/api/client";
|
|
import { useScopedI18n } from "@homarr/translation/client";
|
|
|
|
import { WidgetEmptyState } from "../common/empty-state";
|
|
import type { WidgetComponentProps } from "../definition";
|
|
import classes from "./component.module.css";
|
|
|
|
const statVisibilityByOption = {
|
|
showMissingEpisodes: "episodes",
|
|
showMissingMovies: "movies",
|
|
showProviderIssues: "providers",
|
|
showHealthIssues: "status",
|
|
} as const;
|
|
|
|
const statIcons = {
|
|
episodes: IconDeviceTv,
|
|
movies: IconMovie,
|
|
providers: IconPlugConnected,
|
|
status: IconAlertTriangle,
|
|
} as const;
|
|
|
|
const gridColsByWidth = [
|
|
{ minWidth: 640, cols: 4 },
|
|
{ minWidth: 380, cols: 2 },
|
|
{ minWidth: 0, cols: 1 },
|
|
] as const;
|
|
|
|
const iconSizeByWidth = [
|
|
{ minWidth: 320, size: 22 },
|
|
{ minWidth: 200, size: 18 },
|
|
{ minWidth: 0, size: 16 },
|
|
] as const;
|
|
|
|
export default function BazarrWidget({ integrationIds, options, width, height }: WidgetComponentProps<"bazarr">) {
|
|
const t = useScopedI18n("widget.bazarr");
|
|
const { data: badges, error } = clientApi.widget.bazarr.getBadges.useQuery(
|
|
{ integrationId: integrationIds[0] ?? "" },
|
|
{ enabled: Boolean(integrationIds[0]) },
|
|
);
|
|
|
|
if (error && !badges) throw error;
|
|
if (!badges) return <WidgetEmptyState />;
|
|
|
|
const statValues = {
|
|
episodes: badges.episodes,
|
|
movies: badges.movies,
|
|
providers: badges.providers,
|
|
status: badges.status,
|
|
} as const;
|
|
|
|
const visibleStatKeys = Object.entries(statVisibilityByOption)
|
|
.filter(([optionKey]) => options[optionKey as keyof typeof options])
|
|
.map(([, statKey]) => statKey);
|
|
|
|
const gridCols = getGridCols(width, height, visibleStatKeys.length);
|
|
const iconSize = getIconSize(Math.min(width, height));
|
|
|
|
if (visibleStatKeys.length === 0) {
|
|
return (
|
|
<div className={classes.root}>
|
|
<div className={classes.emptyState}>
|
|
<Text size="sm" c="dimmed">
|
|
—
|
|
</Text>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className={classes.root}>
|
|
<div
|
|
className={classes.grid}
|
|
data-short={height < 120 || undefined}
|
|
style={{ "--stat-cols": gridCols } as CSSProperties}
|
|
>
|
|
{visibleStatKeys.map((statKey) => {
|
|
const Icon = statIcons[statKey];
|
|
const value = statValues[statKey];
|
|
const isWarning = (statKey === "providers" || statKey === "status") && value > 0;
|
|
|
|
return (
|
|
<div key={statKey} className={`${classes.statTile} ${isWarning ? classes.statTileWarning : ""}`}>
|
|
<Icon className={classes.statIcon} size={iconSize} stroke={1.5} />
|
|
<span className={`${classes.statValue} ${isWarning ? classes.statValueWarning : ""}`}>{value}</span>
|
|
<span className={classes.statLabel} title={t(statKey)}>
|
|
{t(statKey)}
|
|
</span>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function getGridCols(width: number, height: number, itemCount: number): number {
|
|
if (itemCount <= 1) return 1;
|
|
|
|
const preferredColumns = gridColsByWidth.find(({ minWidth }) => width >= minWidth)?.cols ?? 1;
|
|
const maxColumnsByWidth = Math.max(1, Math.floor(width / 100));
|
|
const maxRowsByHeight = Math.max(1, Math.floor(height / 72));
|
|
const columnsNeededToFit = Math.ceil(itemCount / maxRowsByHeight);
|
|
return Math.min(itemCount, maxColumnsByWidth, Math.max(preferredColumns, columnsNeededToFit));
|
|
}
|
|
|
|
export function getIconSize(width: number): number {
|
|
const match = iconSizeByWidth.find(({ minWidth }) => width >= minWidth);
|
|
return match?.size ?? 16;
|
|
}
|