frontend: show upload progress

Massimo Melina committed Jan 21, 2023 at 15:52 UTC 7657b96b458ee4f78a0efb8f3fb0d80789285a02
2 files changed +52 -27
frontend/src/index.scss
+8
@@ -267,6 +267,14 @@ button label {
267 }
268 }
269
270 +.upload-progress::before {
271 + content: " – ";
272 +}
273 +.upload-progress {
274 + min-width: 4em;
275 + display: inline-block;
276 + margin-left: 0.5em;
277 +}
278 .upload-list {
279 & td:nth-child(1) { width: 0; }
280 & td:nth-child(2) { text-align: right; width: 0; white-space: nowrap; padding-left: 0.5em; }
frontend/src/upload.ts
+44 -27
@@ -15,7 +15,9 @@ export const uploadState = proxy<{
15 qs: { to: string, files: File[] }[]
16 paused: boolean
17 uploading?: File
18 + progress: number
19 }>({
20 + progress: 0,
21 paused: false,
22 qs: [],
23 errors: 0,
@@ -130,15 +132,20 @@ function path(f: File, pre='') {
132 }
133
134 function FilesList({ files, remove }: { files: File[], remove: (f:File) => any }) {
133 - const { uploading } = useSnapshot(uploadState)
135 + const { uploading, progress } = useSnapshot(uploadState)
136 return !files.length ? null : h('table', { className: 'upload-list', width: '100%' },
137 h('tbody', {},
136 - files.map((f,i) =>
137 - h('tr', { key: i },
138 + files.map((f,i) => {
139 + const working = f === uploading
140 + return h('tr', { key: i },
141 h('td', {}, iconBtn('trash', () => remove(f))),
142 h('td', {}, formatBytes(f.size)),
140 - h('td', { className: f === uploading ? 'ani-working' : undefined }, path(f)),
141 - ))
143 + h('td', { className: working ? 'ani-working' : undefined },
144 + path(f),
145 + working && h('span', { className: 'upload-progress' }, progress.toFixed(1), '%')
146 + ),
147 + )
148 + })
149 )
150 )
151 }
@@ -149,7 +156,7 @@ function iconBtn(icon: string, onClick: () => any, { small=true, style={}, ...pr
156
157 /// Manage upload queue
158
152 -let controller: AbortController | undefined
159 +let req: XMLHttpRequest | undefined
160 subscribe(uploadState, () => {
161 const [cur] = uploadState.qs
162 if (cur && !uploadState.uploading && !uploadState.paused)
@@ -159,35 +166,45 @@ subscribe(uploadState, () => {
166 function startUpload(f: File | undefined, to: string) {
167 if (!f) return
168 uploadState.uploading = f
162 - controller = new AbortController()
163 - const full = path(f, to)
164 - fetch(full, {
165 - method: 'PUT',
166 - body: f,
167 - signal: controller.signal,
168 - }).then(async res => {
169 - if (!res.ok)
170 - throw Error("Upload failed for " + full)
169 + req = new XMLHttpRequest()
170 + req.onloadend = () => {
171 + if (req?.readyState !== 4) return
172 + if (req.status >= 400 )
173 + error()
174 + else
175 + done()
176 + next()
177 + }
178 + req.upload.onprogress = (e:any) => uploadState.progress = e.loaded / e.total * 100
179 + req.open("POST", to, true)
180 + const form = new FormData()
181 + form.append('file', f, path(f))
182 + req.send(form)
183 +
184 + function error() {
185 + if (!uploadState.errors++)
186 + alertDialog("Upload error", 'error').then()
187 + }
188 +
189 + function done() {
190 uploadState.done++
172 - uploadState.doneByte += f.size
191 + uploadState.doneByte += f!.size
192 reloadOnClose = true
174 - }).finally(() => {
193 + }
194 +
195 + function next() {
196 uploadState.uploading = undefined
197 const { qs } = uploadState
198 + if (!qs.length) return
199 qs[0].files.shift()
200 if (!qs[0].files.length)
201 qs.shift()
180 - if (!qs.length) {
181 - reloadList()
182 - reloadOnClose = false
183 - }
184 - }).catch(e => {
185 - if (e.code === 20) return // aborted
186 - if (!uploadState.errors++)
187 - alertDialog("Upload error", 'error').then()
188 - })
202 + if (qs.length) return
203 + reloadList()
204 + reloadOnClose = false
205 + }
206 }
207
208 function abortCurrentUpload() {
192 - controller?.abort()
209 + req?.abort()
210 }
\ No newline at end of file