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:
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;
|
||||
}),
|
||||
});
|
||||
@@ -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;
|
||||
}),
|
||||
});
|
||||
@@ -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"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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"));
|
||||
@@ -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"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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"));
|
||||
Reference in new issue
Block a user