feat: expand advanced widget experiences

This commit is contained in:
Thomas Camlong authored and ajnart committed 2026-08-13 19:54:55 +02:00
1 parent 55226842f9
commit 65a7e84bea
137 files changed
+3684 -850

No files matched your search

@@ -69,6 +69,8 @@
color: var(--mantine-primary-color-filled);
line-height: 1.1;
text-align: center;
max-width: 100%;
overflow-wrap: anywhere;
}
.statValueWarning {
+50 -14
View File
@@ -2,7 +2,7 @@
import type { CSSProperties } from "react";
import { Text } from "@mantine/core";
import { IconAlertTriangle, IconDeviceTv, IconMovie, IconPlugConnected } from "@tabler/icons-react";
import { IconAlertTriangle, IconBell, IconDeviceTv, IconMovie, IconPlugConnected } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client";
import { useScopedI18n } from "@homarr/translation/client";
@@ -11,18 +11,34 @@ 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 compactStatOptions = [
{ option: "showMissingEpisodes", stat: "episodes" },
{ option: "showMissingMovies", stat: "movies" },
{ option: "showProviderIssues", stat: "providers" },
{ option: "showHealthIssues", stat: "status" },
] as const;
const allStatKeys = [
"episodes",
"movies",
"providers",
"status",
"sonarrSignalr",
"radarrSignalr",
"announcements",
] as const;
type BazarrStatKey = (typeof allStatKeys)[number];
type BazarrVisibilityOptions = Record<(typeof compactStatOptions)[number]["option"], boolean>;
const statIcons = {
episodes: IconDeviceTv,
movies: IconMovie,
providers: IconPlugConnected,
status: IconAlertTriangle,
sonarrSignalr: IconPlugConnected,
radarrSignalr: IconPlugConnected,
announcements: IconBell,
} as const;
const gridColsByWidth = [
@@ -37,7 +53,13 @@ const iconSizeByWidth = [
{ minWidth: 0, size: 16 },
] as const;
export default function BazarrWidget({ integrationIds, options, width, height }: WidgetComponentProps<"bazarr">) {
export default function BazarrWidget({
integrationIds,
options,
width,
height,
displayMode,
}: WidgetComponentProps<"bazarr">) {
const t = useScopedI18n("widget.bazarr");
const { data: badges, error } = clientApi.widget.bazarr.getBadges.useQuery(
{ integrationId: integrationIds[0] ?? "" },
@@ -52,11 +74,12 @@ export default function BazarrWidget({ integrationIds, options, width, height }:
movies: badges.movies,
providers: badges.providers,
status: badges.status,
} as const;
sonarrSignalr: badges.sonarr_signalr,
radarrSignalr: badges.radarr_signalr,
announcements: badges.announcements,
} satisfies Record<BazarrStatKey, number | string>;
const visibleStatKeys = Object.entries(statVisibilityByOption)
.filter(([optionKey]) => options[optionKey as keyof typeof options])
.map(([, statKey]) => statKey);
const visibleStatKeys = getVisibleBazarrStatKeys(options, displayMode);
const gridCols = getGridCols(width, height, visibleStatKeys.length);
const iconSize = getIconSize(Math.min(width, height));
@@ -83,12 +106,17 @@ export default function BazarrWidget({ integrationIds, options, width, height }:
{visibleStatKeys.map((statKey) => {
const Icon = statIcons[statKey];
const value = statValues[statKey];
const isWarning = (statKey === "providers" || statKey === "status") && value > 0;
const isWarning =
typeof value === "number" &&
(statKey === "providers" || statKey === "status" || statKey === "announcements") &&
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.statValue} ${isWarning ? classes.statValueWarning : ""}`}>
{typeof value === "string" && value.trim() === "" ? "—" : value}
</span>
<span className={classes.statLabel} title={t(statKey)}>
{t(statKey)}
</span>
@@ -100,6 +128,14 @@ export default function BazarrWidget({ integrationIds, options, width, height }:
);
}
export const getVisibleBazarrStatKeys = (
options: BazarrVisibilityOptions,
displayMode?: "compact" | "advanced",
): BazarrStatKey[] => {
if (displayMode === "advanced") return [...allStatKeys];
return compactStatOptions.filter(({ option }) => options[option]).map(({ stat }) => stat);
};
export function getGridCols(width: number, height: number, itemCount: number): number {
if (itemCount <= 1) return 1;
+1 -1
View File
@@ -4,7 +4,7 @@ import { createWidgetDefinition } from "../definition";
import { optionsBuilder } from "../options";
export const { definition, componentLoader } = createWidgetDefinition("bazarr", {
supportsAdvancedFocus: false,
supportsAdvancedFocus: true,
icon: IconBadgeCc,
supportedIntegrations: ["bazarr"],
integrationsRequired: true,
+15 -1
View File
@@ -1,6 +1,6 @@
import { describe, expect, test } from "vitest";
import { getGridCols, getIconSize } from "./component";
import { getGridCols, getIconSize, getVisibleBazarrStatKeys } from "./component";
describe("Bazarr responsive layout", () => {
test("uses two columns for a short widget so all stats remain visible", () => {
@@ -15,4 +15,18 @@ describe("Bazarr responsive layout", () => {
expect(getIconSize(180)).toBe(16);
expect(getIconSize(340)).toBe(22);
});
test("advanced mode includes hidden summaries and all fetched status fields", () => {
expect(
getVisibleBazarrStatKeys(
{
showMissingEpisodes: false,
showMissingMovies: false,
showProviderIssues: false,
showHealthIssues: false,
},
"advanced",
),
).toEqual(["episodes", "movies", "providers", "status", "sonarrSignalr", "radarrSignalr", "announcements"]);
});
});