diff --git a/packages/translation/src/lang/en.json b/packages/translation/src/lang/en.json index 8a7cdd148..e2dc60cef 100644 --- a/packages/translation/src/lang/en.json +++ b/packages/translation/src/lang/en.json @@ -1890,10 +1890,10 @@ "statistics": { "empty": "Empty", "transcodes": "Transcodes", - "transcodesCount": "Transcodes: {value}", - "healthChecksCount": "Health checks: {value}", - "filesCount": "Files: {value}", - "savedSpace": "Saved space: {value}", + "transcodesCount": "Transcodes", + "healthChecksCount": "Health checks", + "filesCount": "Files", + "savedSpace": "Saved space", "healthChecks": "Health checks", "videoCodecs": "Codecs", "videoContainers": "Containers", diff --git a/packages/widgets/src/media-transcoding/component.tsx b/packages/widgets/src/media-transcoding/component.tsx index 10342ec6a..cad468681 100644 --- a/packages/widgets/src/media-transcoding/component.tsx +++ b/packages/widgets/src/media-transcoding/component.tsx @@ -2,20 +2,32 @@ import { useState } from "react"; import { Center, Divider, Group, Pagination, SegmentedControl, Stack, Text } from "@mantine/core"; +import type { TablerIcon } from "@tabler/icons-react"; import { IconClipboardList, IconCpu2, IconReportAnalytics } from "@tabler/icons-react"; import { clientApi } from "@homarr/api/client"; import { useI18n } from "@homarr/translation/client"; +import { views } from "."; import type { WidgetComponentProps } from "../definition"; import { HealthCheckStatus } from "./health-check-status"; import { QueuePanel } from "./panels/queue.panel"; import { StatisticsPanel } from "./panels/statistics.panel"; import { WorkersPanel } from "./panels/workers.panel"; -type Views = "workers" | "queue" | "statistics"; +type View = (typeof views)[number]; -export default function MediaTranscodingWidget({ integrationIds, options }: WidgetComponentProps<"mediaTranscoding">) { +const viewIcons = { + workers: IconCpu2, + queue: IconClipboardList, + statistics: IconReportAnalytics, +} satisfies Record; + +export default function MediaTranscodingWidget({ + integrationIds, + options, + width, +}: WidgetComponentProps<"mediaTranscoding">) { const [queuePage, setQueuePage] = useState(1); const queuePageSize = 10; const [transcodingData] = clientApi.widget.mediaTranscoding.getDataAsync.useSuspenseQuery( @@ -31,15 +43,16 @@ export default function MediaTranscodingWidget({ integrationIds, options }: Widg }, ); - const [view, setView] = useState(options.defaultView); + const [view, setView] = useState(options.defaultView); const totalQueuePages = Math.ceil((transcodingData.data.queue.totalCount || 1) / queuePageSize); const t = useI18n("widget.mediaTranscoding"); + const isTiny = width < 256; return ( {view === "workers" ? ( - + ) : view === "queue" ? ( ) : ( @@ -48,65 +61,48 @@ export default function MediaTranscodingWidget({ integrationIds, options }: Widg { + const Icon = viewIcons[value]; + return { label: ( -
- - - {t("tab.workers")} - +
+ + {!isTiny && ( + + {t(`tab.${value}`)} + + )}
), - value: "workers", - }, - { - label: ( -
- - - {t("tab.queue")} - -
- ), - value: "queue", - }, - { - label: ( -
- - - {t("tab.statistics")} - -
- ), - value: "statistics", - }, - ]} + value, + }; + })} value={view} - onChange={(value) => setView(value as Views)} + onChange={(value) => setView(value as View)} size="xs" /> - {view === "queue" && ( - <> - - - - - - - - - - {t("currentIndex", { - start: transcodingData.data.queue.startIndex + 1, - end: transcodingData.data.queue.endIndex + 1, - total: transcodingData.data.queue.totalCount, - })} - - - )} + + {view === "queue" && ( + <> + + + {!isTiny && } + + + {!isTiny && } + + + + {t("currentIndex", { + start: transcodingData.data.queue.startIndex + 1, + end: transcodingData.data.queue.endIndex + 1, + total: transcodingData.data.queue.totalCount, + })} + + + )} + diff --git a/packages/widgets/src/media-transcoding/health-check-status.tsx b/packages/widgets/src/media-transcoding/health-check-status.tsx index 6491f6289..d1b3f7df9 100644 --- a/packages/widgets/src/media-transcoding/health-check-status.tsx +++ b/packages/widgets/src/media-transcoding/health-check-status.tsx @@ -23,8 +23,8 @@ export function HealthCheckStatus(props: HealthCheckStatusProps) { return ( - - + + diff --git a/packages/widgets/src/media-transcoding/index.ts b/packages/widgets/src/media-transcoding/index.ts index 355f3d9d3..0c6239eea 100644 --- a/packages/widgets/src/media-transcoding/index.ts +++ b/packages/widgets/src/media-transcoding/index.ts @@ -1,20 +1,20 @@ import { IconTransform } from "@tabler/icons-react"; import { z } from "zod"; +import { capitalize } from "@homarr/common"; + import { createWidgetDefinition } from "../definition"; import { optionsBuilder } from "../options"; +export const views = ["workers", "queue", "statistics"] as const; + export const { componentLoader, definition } = createWidgetDefinition("mediaTranscoding", { icon: IconTransform, createOptions() { return optionsBuilder.from((factory) => ({ defaultView: factory.select({ defaultValue: "statistics", - options: [ - { label: "Workers", value: "workers" }, - { label: "Queue", value: "queue" }, - { label: "Statistics", value: "statistics" }, - ], + options: views.map((view) => ({ label: capitalize(view), value: view })), }), queuePageSize: factory.number({ defaultValue: 10, validate: z.number().min(1).max(30) }), })); diff --git a/packages/widgets/src/media-transcoding/panels/queue.panel.tsx b/packages/widgets/src/media-transcoding/panels/queue.panel.tsx index 9d4d6a680..45cc7581f 100644 --- a/packages/widgets/src/media-transcoding/panels/queue.panel.tsx +++ b/packages/widgets/src/media-transcoding/panels/queue.panel.tsx @@ -1,4 +1,4 @@ -import { Center, Group, ScrollArea, Table, Text, Title, Tooltip } from "@mantine/core"; +import { Center, Group, ScrollArea, Table, TableTd, TableTh, TableTr, Text, Title, Tooltip } from "@mantine/core"; import { IconHeartbeat, IconTransform } from "@tabler/icons-react"; import { humanFileSize } from "@homarr/common"; @@ -17,7 +17,7 @@ export function QueuePanel(props: QueuePanelProps) { if (queue.array.length === 0) { return (
- {t("empty")} + {t("empty")}
); } @@ -26,36 +26,42 @@ export function QueuePanel(props: QueuePanelProps) { - - - - + + + + {t("table.file")} + + + + + {t("table.size")} + + + {queue.array.map((item) => ( - - - - + + ))}
{t("table.file")}{t("table.size")}
- -
- {item.type === "transcode" ? ( - - - - ) : ( - - - - )} -
+ + + + {item.type === "transcode" ? ( + + + + ) : ( + + + + )} {item.filePath.split("\\").pop()?.split("/").pop() ?? item.filePath} -
+ + {humanFileSize(item.fileSize)} -
diff --git a/packages/widgets/src/media-transcoding/panels/statistics.panel.tsx b/packages/widgets/src/media-transcoding/panels/statistics.panel.tsx index 93da0c0ce..23ab97563 100644 --- a/packages/widgets/src/media-transcoding/panels/statistics.panel.tsx +++ b/packages/widgets/src/media-transcoding/panels/statistics.panel.tsx @@ -1,11 +1,12 @@ -import type react from "react"; import type { MantineColor, RingProgressProps } from "@mantine/core"; -import { Box, Center, Grid, Group, RingProgress, Stack, Text, Title, useMantineColorScheme } from "@mantine/core"; +import { Card, Center, Group, RingProgress, ScrollArea, Stack, Text, Title, Tooltip } from "@mantine/core"; import { IconDatabaseHeart, IconFileDescription, IconHeartbeat, IconTransform } from "@tabler/icons-react"; +import { useRequiredBoard } from "@homarr/boards/context"; import { humanFileSize } from "@homarr/common"; import type { TdarrPieSegment, TdarrStatistics } from "@homarr/integrations"; import { useI18n } from "@homarr/translation/client"; +import type { TablerIcon } from "@homarr/ui"; const PIE_COLORS: MantineColor[] = ["cyan", "grape", "gray", "orange", "pink"]; @@ -21,90 +22,54 @@ export function StatisticsPanel(props: StatisticsPanelProps) { if (!allLibs) { return (
- {t("empty")} + {t("empty")}
); } return ( - - - - - {t("transcodes")} - - - - } - label={t("transcodesCount", { - value: props.statistics.totalTranscodeCount, - })} - /> - - - } - label={t("healthChecksCount", { - value: props.statistics.totalHealthCheckCount, - })} - /> - - - } - label={t("filesCount", { - value: props.statistics.totalFileCount, - })} - /> - - - } - label={t("savedSpace", { - value: humanFileSize(Math.floor(allLibs.savedSpace)), - })} - /> - - - - - {t("healthChecks")} - + + + + + + - - - - {t("videoCodecs")} - - - - {t("videoContainers")} - - - - {t("videoResolutions")} - + + + + + + - +
); } +interface StatisticRingProgressProps { + items: TdarrPieSegment[]; + label: string; +} + +const StatisticRingProgress = ({ items, label }: StatisticRingProgressProps) => { + return ( + + + {label} + + + + ); +}; + function toRingProgressSections(segments: TdarrPieSegment[]): RingProgressProps["sections"] { const total = segments.reduce((prev, curr) => prev + curr.value, 0); return segments.map((segment, index) => ({ @@ -115,26 +80,22 @@ function toRingProgressSections(segments: TdarrPieSegment[]): RingProgressProps[ })); } -interface StatBoxProps { - icon: react.ReactNode; +interface StatisticItemProps { + icon: TablerIcon; + value: string | number; label: string; } -function StatBox(props: StatBoxProps) { - const { colorScheme } = useMantineColorScheme(); +function StatisticItem(props: StatisticItemProps) { + const board = useRequiredBoard(); return ( - ({ - padding: theme.spacing.xs, - border: "1px solid", - borderRadius: theme.radius.md, - borderColor: colorScheme === "dark" ? theme.colors.dark[5] : theme.colors.gray[1], - })} - > - - {props.icon} - {props.label} - - + + + + + {props.value} + + + ); } diff --git a/packages/widgets/src/media-transcoding/panels/workers.panel.tsx b/packages/widgets/src/media-transcoding/panels/workers.panel.tsx index 961ac5d7c..d1602c248 100644 --- a/packages/widgets/src/media-transcoding/panels/workers.panel.tsx +++ b/packages/widgets/src/media-transcoding/panels/workers.panel.tsx @@ -1,4 +1,16 @@ -import { Center, Group, Progress, ScrollArea, Table, Text, Title, Tooltip } from "@mantine/core"; +import { + Center, + Group, + Progress, + ScrollArea, + Table, + TableTd, + TableTh, + TableTr, + Text, + Title, + Tooltip, +} from "@mantine/core"; import { IconHeartbeat, IconTransform } from "@tabler/icons-react"; import type { TdarrWorker } from "@homarr/integrations"; @@ -6,6 +18,7 @@ import { useI18n } from "@homarr/translation/client"; interface WorkersPanelProps { workers: TdarrWorker[]; + isTiny: boolean; } export function WorkersPanel(props: WorkersPanelProps) { @@ -14,7 +27,7 @@ export function WorkersPanel(props: WorkersPanelProps) { if (props.workers.length === 0) { return (
- {t("empty")} + {t("empty")}
); } @@ -23,52 +36,71 @@ export function WorkersPanel(props: WorkersPanelProps) { - - - - - + + + + {t("table.file")} + + + + + {t("table.eta")} + + + + + {t("table.progress")} + + + - {props.workers.map((worker) => ( - - - - - - ))} + {Math.round(worker.percentage)}% + + + + ); + })}
{t("table.file")}{t("table.eta")}{t("table.progress")}
- -
- {worker.jobType === "transcode" ? ( - - - - ) : ( - - - + {props.workers.map((worker) => { + const fileName = worker.filePath.split("\\").pop()?.split("/").pop() ?? worker.filePath; + return ( + + + +
+ {worker.jobType === "transcode" ? ( + + + + ) : ( + + + + )} +
+ + {fileName} + +
+
+ + {worker.ETA.startsWith("0:") ? worker.ETA.substring(2) : worker.ETA} + + + + {!props.isTiny && ( + <> + {worker.step} + + )} -
- - {worker.filePath.split("\\").pop()?.split("/").pop() ?? worker.filePath} - -
-
- {worker.ETA.startsWith("0:") ? worker.ETA.substring(2) : worker.ETA} - - - {worker.step} - - {Math.round(worker.percentage)}% - -