refactor: remove deprecated navidrome/audiobookshelf widgets

Consolidate into the unified Audio Stats widget which adapts its UI
based on the linked integration kind — Navidrome shows artists/albums/
songs/now-playing, Audiobookshelf shows libraries/audiobooks/podcasts/
listening-time/sessions.

Removes: widget definitions, components, API routers, and the
deprecatedWidgetKinds mechanism (no longer needed).
This commit is contained in:
Thomas Camlong committed 2026-06-09 20:58:54 +02:00
1 parent 9da8ea1dec
commit 69ed608241
13 files changed
+4 -220

No files matched your search

@@ -3,7 +3,7 @@ import { Avatar, Box, Button, Card, Center, Group, Stack, Text, Tooltip } from "
import { clientApi } from "@homarr/api/client";
import { createId, objectEntries } from "@homarr/common";
import { deprecatedWidgetKinds, getIconUrl, getIntegrationName } from "@homarr/definitions";
import { getIconUrl, getIntegrationName } from "@homarr/definitions";
import type { IntegrationKind, WidgetKind } from "@homarr/definitions";
import { createModal, modalSizeSelect, useModalAction } from "@homarr/modals";
import { useSettings } from "@homarr/settings";
@@ -26,7 +26,6 @@ export const ItemSelectModal = createModal<void>(({ actions }) => {
const items = useMemo(
() =>
objectEntries(widgetImports)
.filter(([kind]) => !deprecatedWidgetKinds.has(kind))
.map(([kind, value]) => ({
kind,
supportedIntegrations:
@@ -1,12 +0,0 @@
import { audiobookshelfRequestHandler } from "@homarr/request-handler/audiobookshelf";
import { createOneIntegrationMiddleware } from "../../middlewares/integration";
import { createTRPCRouter, publicProcedure } from "../../trpc";
export const audiobookshelfRouter = createTRPCRouter({
getStats: publicProcedure.concat(createOneIntegrationMiddleware("query", "audiobookshelf")).query(async ({ ctx }) => {
const innerHandler = audiobookshelfRequestHandler.handler(ctx.integration, {});
const data = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
return data.data;
}),
});
-2
View File
@@ -33,8 +33,6 @@ export const widgetRouter = createTRPCRouter({
speedtestTracker: lazy(() => import("./speedtest-tracker").then((mod) => mod.speedtestTrackerRouter)),
uptimeKuma: lazy(() => import("./uptime-kuma").then((mod) => mod.uptimeKumaRouter)),
audioStats: lazy(() => import("./audio-stats").then((mod) => mod.audioStatsRouter)),
audiobookshelf: lazy(() => import("./audiobookshelf").then((mod) => mod.audiobookshelfRouter)),
navidrome: lazy(() => import("./navidrome").then((mod) => mod.navidromeRouter)),
umami: lazy(() => import("./umami").then((mod) => mod.umamiRouter)),
ups: lazy(() => import("./ups").then((mod) => mod.upsRouter)),
});
@@ -1,21 +0,0 @@
import { z } from "zod/v4";
import { navidromeRequestHandler } from "@homarr/request-handler/navidrome";
import { createOneIntegrationMiddleware } from "../../middlewares/integration";
import { createTRPCRouter, publicProcedure } from "../../trpc";
export const navidromeRouter = createTRPCRouter({
getStats: publicProcedure
.concat(createOneIntegrationMiddleware("query", "navidrome"))
.input(
z.object({
integrationId: z.string(),
}),
)
.query(async ({ ctx }) => {
const innerHandler = navidromeRequestHandler.handler(ctx.integration, {});
const data = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
return data.data;
}),
});
-6
View File
@@ -40,19 +40,13 @@ export const widgetKinds = [
"speedtestTracker",
"uptimeKuma",
"audioStats",
"audiobookshelf",
"navidrome",
"umami",
"ups",
] as const;
export type WidgetKind = (typeof widgetKinds)[number];
export const deprecatedWidgetKinds = new Set<WidgetKind>(["navidrome", "audiobookshelf"]);
export const widgetDefaultSizes: Partial<Record<WidgetKind, { width: number; height: number }>> = {
uptimeKuma: { width: 2, height: 3 },
audioStats: { width: 2, height: 2 },
audiobookshelf: { width: 2, height: 2 },
paperlessNgx: { width: 2, height: 2 },
navidrome: { width: 2, height: 2 },
};
@@ -9,7 +9,7 @@ import { useScopedI18n } from "@homarr/translation/client";
import classes from "./component.module.css";
import type { AudioStatsBackend, AudioStatsDisplayOptions } from "./shared";
import { getGridCols, getIconSize, getVisibleStats, translationScopeByBackend } from "./shared";
import { AUDIO_STATS_TRANSLATION_SCOPE, getGridCols, getIconSize, getVisibleStats } from "./shared";
const rootClassByCompact = {
true: classes.rootCompact,
@@ -31,7 +31,6 @@ interface AudioStatsContentProps {
stats: NavidromeDashboardData | AudiobookshelfDashboardData;
options: AudioStatsDisplayOptions;
width: number;
translationScope?: keyof typeof translationScopeByBackend;
}
export function AudioStatsContent({
@@ -39,9 +38,8 @@ export function AudioStatsContent({
stats,
options,
width,
translationScope = "audioStats",
}: AudioStatsContentProps) {
const t = useScopedI18n(translationScopeByBackend[translationScope]);
const t = useScopedI18n(AUDIO_STATS_TRANSLATION_SCOPE);
const visibleStats = getVisibleStats(backend, options, stats);
const compactKey = String(options.compactMode ?? false) as keyof typeof rootClassByCompact;
const gridCols = getGridCols(width, visibleStats.length, options.compactMode ?? false);
@@ -34,7 +34,6 @@ export default function AudioStatsWidget({ integrationIds, options, width }: Wid
stats={response.data}
options={displayOptions}
width={width}
translationScope="audioStats"
/>
);
}
+1 -5
View File
@@ -166,8 +166,4 @@ export const getIconSize = (width: number, compact: boolean): number => {
return match?.size ?? fallbackIconSize[String(compact) as keyof typeof fallbackIconSize];
};
export const translationScopeByBackend = {
navidrome: "widget.navidrome",
audiobookshelf: "widget.audiobookshelf",
audioStats: "widget.audioStats",
} as const;
export const AUDIO_STATS_TRANSLATION_SCOPE = "widget.audioStats" as const;
@@ -1,38 +0,0 @@
"use client";
import { clientApi } from "@homarr/api/client";
import type { WidgetComponentProps } from "../definition";
import { AudioStatsContent } from "../audio-stats/audio-stats-content";
import type { AudiobookshelfDisplayOptions } from "../audio-stats/shared";
const defaultAudiobookshelfOptions: AudiobookshelfDisplayOptions = {
showLibraryCount: true,
showAudiobooks: true,
showPodcasts: true,
showListeningTime: true,
showActiveSessions: true,
compactMode: false,
};
export default function AudiobookshelfWidget({
integrationIds,
options,
width,
}: WidgetComponentProps<"audiobookshelf">) {
const [stats] = clientApi.widget.audiobookshelf.getStats.useSuspenseQuery({
integrationId: integrationIds[0] ?? "",
});
const displayOptions = { ...defaultAudiobookshelfOptions, ...options };
return (
<AudioStatsContent
backend="audiobookshelf"
stats={stats}
options={displayOptions}
width={width}
translationScope="audiobookshelf"
/>
);
}
@@ -1,44 +0,0 @@
import { IconHeadphones, IconServerOff } from "@tabler/icons-react";
import { createWidgetDefinition } from "../definition";
import { optionsBuilder } from "../options";
export const { definition, componentLoader } = createWidgetDefinition("audiobookshelf", {
icon: IconHeadphones,
supportedIntegrations: ["audiobookshelf"],
integrationsRequired: true,
createOptions() {
return optionsBuilder.from((factory) => ({
showLibraryCount: factory.switch({
defaultValue: true,
withDescription: true,
}),
showAudiobooks: factory.switch({
defaultValue: true,
withDescription: true,
}),
showPodcasts: factory.switch({
defaultValue: true,
withDescription: true,
}),
showListeningTime: factory.switch({
defaultValue: true,
withDescription: true,
}),
showActiveSessions: factory.switch({
defaultValue: true,
withDescription: true,
}),
compactMode: factory.switch({
defaultValue: false,
withDescription: true,
}),
}));
},
errors: {
INTERNAL_SERVER_ERROR: {
icon: IconServerOff,
message: (t) => t("widget.audiobookshelf.error.internalServerError"),
},
},
}).withDynamicImport(() => import("./component"));
-4
View File
@@ -9,8 +9,6 @@ import type { SettingsContextProps } from "@homarr/settings/creator";
import * as anchorNote from "./anchor-note";
import * as audioStats from "./audio-stats";
import * as audiobookshelf from "./audiobookshelf";
import * as navidrome from "./navidrome";
import * as app from "./app";
import * as bookmarks from "./bookmarks";
import * as calendar from "./calendar";
@@ -101,8 +99,6 @@ export const widgetImports = {
speedtestTracker,
uptimeKuma,
audioStats,
audiobookshelf,
navidrome,
umami,
ups,
} satisfies WidgetImportRecord;
@@ -1,34 +0,0 @@
"use client";
import { clientApi } from "@homarr/api/client";
import type { WidgetComponentProps } from "../definition";
import { AudioStatsContent } from "../audio-stats/audio-stats-content";
import type { NavidromeDisplayOptions } from "../audio-stats/shared";
const defaultNavidromeOptions: NavidromeDisplayOptions = {
showArtists: true,
showAlbums: true,
showSongs: true,
showNowPlaying: true,
showNowPlayingList: true,
maxNowPlayingItems: 3,
};
export default function NavidromeWidget({ integrationIds, options, width }: WidgetComponentProps<"navidrome">) {
const [stats] = clientApi.widget.navidrome.getStats.useSuspenseQuery({
integrationId: integrationIds[0] ?? "",
});
const displayOptions = { ...defaultNavidromeOptions, ...options };
return (
<AudioStatsContent
backend="navidrome"
stats={stats}
options={displayOptions}
width={width}
translationScope="navidrome"
/>
);
}
-47
View File
@@ -1,47 +0,0 @@
import { IconMusic, IconServerOff } from "@tabler/icons-react";
import { z } from "zod/v4";
import { createWidgetDefinition } from "../definition";
import { optionsBuilder } from "../options";
export const { definition, componentLoader } = createWidgetDefinition("navidrome", {
icon: IconMusic,
supportedIntegrations: ["navidrome"],
integrationsRequired: true,
createOptions() {
return optionsBuilder.from((factory) => ({
showArtists: factory.switch({
defaultValue: true,
withDescription: true,
}),
showAlbums: factory.switch({
defaultValue: true,
withDescription: true,
}),
showSongs: factory.switch({
defaultValue: true,
withDescription: true,
}),
showNowPlaying: factory.switch({
defaultValue: true,
withDescription: true,
}),
showNowPlayingList: factory.switch({
defaultValue: true,
withDescription: true,
}),
maxNowPlayingItems: factory.number({
defaultValue: 3,
validate: z.number().min(1).max(5),
step: 1,
withDescription: true,
}),
}));
},
errors: {
INTERNAL_SERVER_ERROR: {
icon: IconServerOff,
message: (t) => t("widget.navidrome.error.internalServerError"),
},
},
}).withDynamicImport(() => import("./component"));