141 lines
3.9 KiB
TypeScript
141 lines
3.9 KiB
TypeScript
import { Alert, AlertDescription } from "@/components/ui/alert";
|
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
import { SectionCard } from "../components/SectionCard";
|
|
import { useWidgetData } from "../hooks/useWidgets";
|
|
import type { WidgetInstance } from "../types";
|
|
|
|
interface Props {
|
|
widget: WidgetInstance;
|
|
refreshIntervalMs: number;
|
|
description?: string;
|
|
}
|
|
|
|
interface TotalsPayload {
|
|
total?: number;
|
|
by_state?: Record<string, number>;
|
|
by_direction?: {
|
|
downloading?: number;
|
|
uploading?: number;
|
|
};
|
|
}
|
|
|
|
const STATE_LABELS: Record<string, string> = {
|
|
downloading: "Downloading",
|
|
forcedDL: "Downloading",
|
|
stalledDL: "Download stalled",
|
|
queuedDL: "Queued download",
|
|
metaDL: "Downloading metadata",
|
|
allocating: "Allocating",
|
|
checkingDL: "Checking download",
|
|
uploading: "Uploading",
|
|
forcedUP: "Uploading",
|
|
stalledUP: "Upload stalled",
|
|
queuedUP: "Queued upload",
|
|
checkingUP: "Checking upload",
|
|
pausedDL: "Paused download",
|
|
pausedUP: "Paused upload",
|
|
moving: "Moving",
|
|
unknown: "Unknown state",
|
|
};
|
|
|
|
const DOWNLOAD_STATES = new Set([
|
|
"downloading",
|
|
"forcedDL",
|
|
"stalledDL",
|
|
"metaDL",
|
|
"allocating",
|
|
]);
|
|
const UPLOAD_STATES = new Set(["uploading", "forcedUP", "stalledUP"]);
|
|
|
|
function stateLabel(state: string): string {
|
|
return (
|
|
STATE_LABELS[state] ??
|
|
state
|
|
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
|
.replace(/^./, (char) => char.toUpperCase())
|
|
);
|
|
}
|
|
|
|
function fallbackDirectionCount(
|
|
byState: Record<string, number> | undefined,
|
|
states: Set<string>,
|
|
): number {
|
|
return Object.entries(byState ?? {}).reduce(
|
|
(total, [state, count]) => total + (states.has(state) ? count : 0),
|
|
0,
|
|
);
|
|
}
|
|
|
|
export function QbittorrentTotalsWidget({
|
|
widget,
|
|
refreshIntervalMs,
|
|
description,
|
|
}: Props) {
|
|
const { data, isLoading } = useWidgetData(widget.id, refreshIntervalMs);
|
|
const payload = data?.data as TotalsPayload | undefined;
|
|
const byState = payload?.by_state ?? {};
|
|
const downloading =
|
|
payload?.by_direction?.downloading ??
|
|
fallbackDirectionCount(byState, DOWNLOAD_STATES);
|
|
const uploading =
|
|
payload?.by_direction?.uploading ??
|
|
fallbackDirectionCount(byState, UPLOAD_STATES);
|
|
const stateEntries = Object.entries(byState).sort(
|
|
([stateA, countA], [stateB, countB]) =>
|
|
countB - countA || stateLabel(stateA).localeCompare(stateLabel(stateB)),
|
|
);
|
|
|
|
return (
|
|
<SectionCard title={widget.title} description={description}>
|
|
{isLoading && !data ? (
|
|
<Skeleton className="h-32 w-full" />
|
|
) : data?.error ? (
|
|
<Alert variant="destructive">
|
|
<AlertDescription>{data.error}</AlertDescription>
|
|
</Alert>
|
|
) : payload ? (
|
|
<div className="space-y-4">
|
|
<div className="grid grid-cols-3 gap-2">
|
|
<div className="rounded-md border p-3">
|
|
<div className="text-2xl font-semibold">{payload.total ?? 0}</div>
|
|
<div className="text-xs text-muted-foreground">
|
|
Total torrents
|
|
</div>
|
|
</div>
|
|
<div className="rounded-md border p-3">
|
|
<div className="text-2xl font-semibold">{downloading}</div>
|
|
<div className="text-xs text-muted-foreground">Downloading</div>
|
|
</div>
|
|
<div className="rounded-md border p-3">
|
|
<div className="text-2xl font-semibold">{uploading}</div>
|
|
<div className="text-xs text-muted-foreground">Uploading</div>
|
|
</div>
|
|
</div>
|
|
{stateEntries.length > 0 && (
|
|
<div>
|
|
<div className="mb-2 text-xs font-medium text-muted-foreground uppercase">
|
|
Torrent states
|
|
</div>
|
|
<div className="grid gap-1.5 sm:grid-cols-2">
|
|
{stateEntries.map(([state, count]) => (
|
|
<div
|
|
key={state}
|
|
className="flex items-center justify-between rounded-md bg-muted/50 px-3 py-2 text-sm"
|
|
>
|
|
<span>{stateLabel(state)}</span>
|
|
<span className="font-medium">{count}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<div className="text-xs text-muted-foreground">
|
|
No torrent data available.
|
|
</div>
|
|
)}
|
|
</SectionCard>
|
|
);
|
|
}
|