import { type Accessor, batch, createEffect, createMemo, createSignal, For, type Setter, Show } from "solid-js"; import "./DownloadManager.css"; import "./Dialog.css"; import "toastify-js/src/toastify.css"; import { type DownloadStatus, downloadStatusMap, type Progress, setDownloadStatusMap } from "../offline"; import { formatBytes } from "../utils"; import { ClearSVG, RefreshSVG, StopSVG } from "./SVG"; interface DownloadManagerProps { showDownloadManager: Accessor; setShowDownloadManager: Setter; } export default function DownloadManager(props: DownloadManagerProps) { let dialogElement!: HTMLDialogElement; createEffect(() => { if (props.showDownloadManager()) { dialogElement.showModal(); } }); function handleClose() { dialogElement.close(); props.setShowDownloadManager(false); } const downloadKeys = createMemo(() => { const keys = Object.keys(downloadStatusMap); const progressKeys = keys.filter((key) => isProgress(downloadStatusMap[key])); // Sort the keys: Running first (by path), then Failed (by path), then Aborted (by path) progressKeys.sort((pathA, pathB) => { const statusA = downloadStatusMap[pathA] as Progress; const statusB = downloadStatusMap[pathB] as Progress; // 0: Running, 1: Failed, 2: Aborted const typeA = typeof statusA.status === "number" ? (statusA.status > 0 ? 0 : 3) : statusA.status === "Failed" ? 1 : 2; const typeB = typeof statusB.status === "number" ? (statusB.status > 0 ? 0 : 3) : statusB.status === "Failed" ? 1 : 2; if (typeA !== typeB) { return typeA - typeB; // Sort by type first } // If types are the same, sort by path numerically/alphabetically return pathA.localeCompare(pathB, undefined, { numeric: true }); }); return progressKeys; }); function isProgress(status: DownloadStatus | undefined): status is Progress { return status !== undefined && typeof status === "object" && "status" in status; } const [downloading, setDownloading] = createSignal(0); const [aborted, setAborted] = createSignal(0); const [failed, setFailed] = createSignal(0); createEffect(() => { batch(() => { setDownloading(0); setAborted(0); setFailed(0); for (const key of downloadKeys()) { const status = downloadStatusMap[key]; if (!isProgress(status)) continue; if (typeof status.status === "number") setDownloading((prev) => prev + 1); if (status.status === "Aborted") setAborted((prev) => prev + 1); if (status.status === "Failed") setFailed((prev) => prev + 1); } }); }); return ( e.target.nodeName === "DIALOG" && handleClose()} >
Downloads

In progress: {downloading()} Aborted: {aborted()} Failed: {failed()}
{(path) => { const [showReason, setShowReason] = createSignal(false); //an accessor, not a snapshot: progress updates replace the whole value object while //keeps this row alive, so reading it once would freeze the byte counter and the status styling const status = () => downloadStatusMap[path] as Progress | undefined; return ( {(currentStatus) => (
{path}
{typeof currentStatus().status === "number" ? ( <> {formatBytes(currentStatus().status as number)} ) : ( <> {(currentStatus().status === "Failed" && ( )) || "Aborted"} )}
{(() => { const reasonStatus = currentStatus(); return "reason" in reasonStatus && reasonStatus.reason; })()}
)}
); }}
); }