DownloadManager.tsx
⎇
Raw
1import {
2 batch,
3 createEffect,
4 createMemo,
5 createSignal,
6 For,
7 Show,
8 untrack,
9 type Accessor,
10 type Setter,
11} from "solid-js";
12import "./DownloadManager.css";
13import "./Dialog.css";
14import "toastify-js/src/toastify.css";
15import { formatBytes } from "../utils";
16import { ClearSVG, RefreshSVG, StopSVG } from "./SVG";
17import { type DownloadStatus, downloadStatusMap, setDownloadStatusMap, type Progress } from "../offline";
18import { createStore } from "solid-js/store";
19
20interface DownloadManagerProps {
21 showDownloadManager: Accessor<boolean>;
22 setShowDownloadManager: Setter<boolean>;
23}
24
25export default function DownloadManager(props: DownloadManagerProps) {
26 let dialogElement!: HTMLDialogElement;
27
28 createEffect(() => {
29 if (props.showDownloadManager()) {
30 dialogElement.showModal();
31 }
32 });
33
34 function handleClose() {
35 dialogElement.close();
36 props.setShowDownloadManager(false);
37 }
38
39 const downloadItems = createMemo(() => {
40 return Object.entries(downloadStatusMap).filter(([_path, status]) => isProgress(status));
41 });
42
43 function isProgress(status: DownloadStatus | undefined): status is Progress {
44 return status !== undefined && typeof status === "object" && "status" in status;
45 }
46
47 return (
48 <dialog
49 style={{ height: "90%" }}
50 ref={dialogElement}
51 onClose={handleClose}
52 onclick={(e) => e.target.nodeName === "DIALOG" && handleClose()}
53 >
54 <div style={{ height: "100%" }}>
55 <div class="downloadmanager-header title">
56 Download
57 <div class="spacer" />
58 <button
59 title="Retry all failed/aborted downloads"
60 onClick={() => {
61 batch(() => {
62 for (const [, status] of downloadItems()) {
63 if (isProgress(status) && "retry" in status) status.retry();
64 }
65 });
66 }}
67 >
68 <RefreshSVG />
69 </button>
70 <button
71 title="stop all running downloads"
72 onClick={async () => {
73 batch(async () => {
74 for (const [, status] of downloadItems()) {
75 if (isProgress(status) && typeof status.status === "number") {
76 status.abort();
77 }
78 }
79 });
80 }}
81 >
82 <StopSVG />
83 </button>
84 <button
85 title="Clear all failed/aborted downloads"
86 onClick={() => {
87 batch(() => {
88 for (const [path, status] of downloadItems()) {
89 if (isProgress(status) && (status.status === "Aborted" || status.status === "Failed"))
90 setDownloadStatusMap(path, undefined);
91 }
92 });
93 }}
94 >
95 Clear
96 </button>
97 <button title="Close dialog" onClick={handleClose}>
98 <ClearSVG />
99 </button>
100 </div>
101 <hr />
102 <div class="downloadmanager-content">
103 <For each={downloadItems()}>
104 {([path, status]) => {
105 if (!isProgress(status)) return;
106 const [showReason, setShowReason] = createSignal(false);
107 return (
108 <div
109 classList={{
110 "downloadmanager-item": true,
111 "downloadmanager-item-failed": status.status === "Failed",
112 "downloadmanager-item-aborted": status.status === "Aborted",
113 }}
114 >
115 <span>{path}</span>
116 <hr />
117 <div class="downloadmanager-buttons">
118 {status && typeof status.status === "number" ? (
119 <>
120 <span>{formatBytes(status.status)}</span>
121 <button title="Stop Download" onClick={() => status.abort()}>
122 <StopSVG />
123 </button>
124 </>
125 ) : (
126 <>
127 {(status.status === "Failed" && (
128 <button title="Show reason" onclick={() => setShowReason(!showReason())}>
129 Reason
130 </button>
131 )) ||
132 "Aborted"}
133 <button title="Retry download" onClick={() => status.retry()}>
134 <RefreshSVG />
135 </button>
136 <button title="Clear Download" onClick={() => setDownloadStatusMap(path, undefined)}>
137 <ClearSVG />
138 </button>
139 </>
140 )}
141 </div>
142 <Show when={showReason()}>{"reason" in status && status.reason}</Show>
143 </div>
144 );
145 }}
146 </For>
147 </div>
148 </div>
149 </dialog>
150 );
151}
152