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