feat: modernize dashboard widgets

This commit is contained in:
Thomas Camlong authored and ajnart committed 2026-08-13 19:54:28 +02:00
1 parent 1a5e083fd0
commit 6f2cd7fc2b
270 files changed
+10189 -3544

No files matched your search

+76 -2
View File
@@ -1,17 +1,91 @@
"use client";
import { Badge, Group, ScrollArea, SimpleGrid, Stack, Text } from "@mantine/core";
import { clientApi } from "@homarr/api/client";
import { formatBytes, formatDuration } from "@homarr/common";
import type { AudiobookshelfDashboardData } from "@homarr/integrations/types";
import { WidgetEmptyState } from "../common/empty-state";
import type { WidgetComponentProps } from "../definition";
import { AudioStatsContent } from "./audio-stats-content";
export default function AudioStatsWidget({ integrationIds, options, width }: WidgetComponentProps<"audioStats">) {
export default function AudioStatsWidget({
integrationIds,
options,
width,
height,
displayMode = "compact",
}: WidgetComponentProps<"audioStats">) {
const { data: response } = clientApi.widget.audioStats.getStats.useQuery({
integrationId: integrationIds[0] ?? "",
});
const { data: streamResults = [] } = clientApi.widget.mediaServer.getCurrentStreams.useQuery(
{ integrationIds, showOnlyPlaying: false },
{ enabled: displayMode === "advanced" && response?.kind === "navidrome" },
);
if (!response) return <WidgetEmptyState />;
return <AudioStatsContent backend={response.kind} stats={response.data} options={options} width={width} />;
const summary = (
<AudioStatsContent
backend={response.kind}
stats={response.data}
options={{ ...options, compactMode: displayMode === "compact" && (options.compactMode || height < 160) }}
width={width}
/>
);
if (displayMode === "compact") return summary;
const sessions = streamResults.flatMap((result) =>
result.sessions.map((session) => ({ integrationId: result.integrationId, session })),
);
const audiobookStats = response.kind === "audiobookshelf" ? (response.data as AudiobookshelfDashboardData) : null;
return (
<Stack h="100%" gap="lg" p="md">
<div style={{ minHeight: 150 }}>{summary}</div>
<ScrollArea style={{ flex: 1, minHeight: 0 }}>
{response.kind === "navidrome" ? (
<SimpleGrid cols={width >= 760 ? 2 : 1} spacing="xs">
{sessions.map(({ integrationId, session }) => {
const currentlyPlaying = session.currentlyPlaying;
if (!currentlyPlaying) return null;
return (
<Group key={`${integrationId}:${session.sessionId}`} justify="space-between" wrap="nowrap" p="sm">
<Stack gap={0} style={{ minWidth: 0 }}>
<Text size="sm" fw={600} truncate>
{currentlyPlaying.name}
</Text>
<Text size="xs" c="dimmed" truncate>
{currentlyPlaying.seasonName ?? currentlyPlaying.albumName ?? session.user.username}
</Text>
</Stack>
<Badge variant="light">{session.sessionName}</Badge>
</Group>
);
})}
</SimpleGrid>
) : (
<SimpleGrid cols={width >= 760 ? 2 : 1} spacing="xs">
{(audiobookStats?.libraries ?? []).map((library) => (
<Stack key={library.id} gap={2} p="sm">
<Group justify="space-between" wrap="nowrap">
<Text size="sm" fw={600} truncate>
{library.name}
</Text>
<Badge size="xs" variant="light">
{library.totalItems}
</Badge>
</Group>
<Text size="xs" c="dimmed">
{library.totalDurationSeconds === null ? "—" : formatDuration(library.totalDurationSeconds * 1000)} ·{" "}
{library.totalSizeBytes === null ? "—" : formatBytes(library.totalSizeBytes)}
</Text>
</Stack>
))}
</SimpleGrid>
)}
</ScrollArea>
</Stack>
);
}
@@ -13,6 +13,7 @@ const hideUnlessAudiobookshelf = {
export const { definition, componentLoader } = createWidgetDefinition("audioStats", {
icon: IconHeadphones,
queryKeys: [[["widget", "audioStats"]], [["widget", "mediaServer"]]],
supportedIntegrations: ["navidrome", "audiobookshelf"],
integrationsRequired: true,
maxIntegrations: 1,