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; by_direction?: { downloading?: number; uploading?: number; }; } const STATE_LABELS: Record = { 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 | undefined, states: Set, ): 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 ( {isLoading && !data ? ( ) : data?.error ? ( {data.error} ) : payload ? (
{payload.total ?? 0}
Total torrents
{downloading}
Downloading
{uploading}
Uploading
{stateEntries.length > 0 && (
Torrent states
{stateEntries.map(([state, count]) => (
{stateLabel(state)} {count}
))}
)}
) : (
No torrent data available.
)}
); }