@samitouri / QOSami-HFS / commits / d7e64802

upload: "show details" button

Massimo Melina committed Apr 11, 2024 at 16:32 UTC d7e64802b5379b1201456f1c9df41fc0be1f853a
3 files changed +59 -32
frontend/src/components.ts
+7 -3
@@ -100,21 +100,25 @@ export interface BtnProps extends ComponentPropsWithoutRef<"button"> {
100 className?: string,
101 onClick?: () => unknown
102 onClickAnimation?: boolean
103 + asText?: boolean
104 }
105
105 -export function Btn({ icon, label, tooltip, toggled, onClick, onClickAnimation, ...rest }: BtnProps) {
106 +export function Btn({ icon, label, tooltip, toggled, onClick, onClickAnimation, asText, ...rest }: BtnProps) {
107 const [working, setWorking] = useState(false)
107 - return h('button', {
108 + return h(asText ? 'a' : 'button', {
109 title: label + prefix(' - ', tooltip),
110 'aria-label': label,
111 'aria-pressed': toggled,
111 - onClick() {
112 + onClick(ev) {
113 + if (asText)
114 + ev.preventDefault()
115 if (!onClick) return
116 if (onClickAnimation !== false)
117 setWorking(true)
118 Promise.resolve(onClick()).finally(() => setWorking(false))
119 },
120 ...rest,
121 + ...asText ? { role: 'button', style: { cursor: 'pointer', ...rest.style } } : undefined,
122 className: [rest.className, toggled && 'toggled', working && 'ani-working'].filter(Boolean).join(' '),
123 }, icon && hIcon(icon), h('span', { className: 'label' }, label) ) // don't use <label> as VoiceOver will get redundant
124 }
frontend/src/index.scss
+6 -3
@@ -31,9 +31,9 @@
31 --good-contrast: #aaa;
32 --button-bg: #345;
33 --button-text: #999;
34 - --success: #383;
35 - --warning: #883;
36 - --error: #a33;
34 + --success: #272;
35 + --warning: #772;
36 + --error: #a22;
37
38 color-scheme: dark;
39 .highlightedText { color: #fff; text-shadow: 0 0 3px #fff; }
@@ -400,6 +400,8 @@ ul.dir {
400 }
401 }
402
403 +ul { padding-left: 2em }
404 +
405 button .icon + .label {
406 cursor: inherit;
407 margin-left: .4em;
@@ -415,6 +417,7 @@ button .icon + .label {
417 .dialog-alert .dialog-content {
418 text-align: center;
419 p { text-align: left; display: inline-block; }
420 + ul { text-align: left; } // bullet points should be aligned
421 }
422
423 .dialog {
frontend/src/upload.ts
+46 -26
@@ -1,13 +1,13 @@
1 // This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
3 import { createElement as h, DragEvent, Fragment, useMemo, CSSProperties, useState, useEffect } from 'react'
4 -import { Flex, FlexV, iconBtn, Select } from './components'
4 +import { Btn, Flex, FlexV, iconBtn, Select } from './components'
5 import {
6 basename, closeDialog, formatBytes, formatPerc, hIcon, useIsMobile, newDialog, prefix, selectFiles, working,
7 HTTP_CONFLICT, HTTP_PAYLOAD_TOO_LARGE, formatSpeed, dirname, getHFS, onlyTruthy, with_, cpuSpeedIndex
8 } from './misc'
9 import _ from 'lodash'
10 -import { proxy, ref, subscribe, useSnapshot } from 'valtio'
10 +import { INTERNAL_Snapshot, proxy, ref, snapshot, subscribe, useSnapshot } from 'valtio'
11 import { alertDialog, confirmDialog, promptDialog, toast } from './dialog'
12 import { reloadList } from './useFetchList'
13 import { apiCall, getNotification } from '@hfs/shared/api'
@@ -20,10 +20,10 @@ const renameEnabled = getHFS().dontOverwriteUploading
20
21 interface ToUpload { file: File, comment?: string, name?: string, to?: string }
22 export const uploadState = proxy<{
23 - done: number
23 + done: ToUpload[]
24 doneByte: number
25 - errors: number
26 - skipped: number
25 + errors: ToUpload[]
26 + skipped: ToUpload[]
27 adding: ToUpload[]
28 qs: { to: string, entries: ToUpload[] }[]
29 paused: boolean
@@ -41,10 +41,10 @@ export const uploadState = proxy<{
41 paused: false,
42 qs: [],
43 adding: [],
44 - skipped: 0,
45 - errors: 0,
44 + skipped: [],
45 + errors: [],
46 doneByte: 0,
47 - done: 0,
47 + done: [],
48 policyForExisting: renameEnabled ? 'rename' : 'skip'
49 })
50
@@ -77,10 +77,10 @@ let everPaused = false
77
78 function resetCounters() {
79 Object.assign(uploadState, {
80 - errors: 0,
81 - done: 0,
80 + errors: [],
81 + done: [],
82 doneByte: 0,
83 - skipped: 0,
83 + skipped: [],
84 })
85 }
86
@@ -316,7 +316,7 @@ async function startUpload(toUpload: ToUpload, to: string, resume=0) {
316 const status = overrideStatus || req.status
317 closeLast?.()
318 if (!status || status === HTTP_CONFLICT) // 0 = user-aborted, HTTP_CONFLICT = skipped because existing
319 - uploadState.skipped++
319 + uploadState.skipped.push(toUpload)
320 else if (status >= 400)
321 error(status)
322 else
@@ -379,7 +379,7 @@ async function startUpload(toUpload: ToUpload, to: string, resume=0) {
379 }
380
381 function error(status: number) {
382 - if (uploadState.errors++) return
382 + if (uploadState.errors.push(toUpload)) return
383 const ERRORS = {
384 [HTTP_PAYLOAD_TOO_LARGE]: t`file too large`,
385 }
@@ -390,7 +390,7 @@ async function startUpload(toUpload: ToUpload, to: string, resume=0) {
390 }
391
392 function done() {
393 - uploadState.done++
393 + uploadState.done.push(toUpload)
394 uploadState.doneByte += toUpload!.file.size
395 reloadOnClose = true
396 }
@@ -406,21 +406,41 @@ async function startUpload(toUpload: ToUpload, to: string, resume=0) {
406 if (qs.length) return
407 setTimeout(reloadList, 500) // workaround: reloading too quickly can meet the new file still with its temp name
408 reloadOnClose = false
409 - const msg = h('div', {}, t(['upload_concluded', "Upload terminated"], "Upload concluded:"), h(UploadStatus) )
410 - if (!uploadDialogIsOpen)
411 - (uploadState.errors || uploadState.skipped ? alertDialog(msg, 'info') : toast(msg, 'success').closed)
412 - .finally(resetCounters)
409 + if (uploadDialogIsOpen) return
410 + // freeze and reset
411 + const snap = snapshot(uploadState)
412 + resetCounters()
413 + const msg = h('div', {}, t(['upload_concluded', "Upload terminated"], "Upload concluded:"),
414 + h(UploadStatus, { snapshot: snap, display: 'flex', flexDirection: 'column' }) )
415 + if (uploadState.errors.length || uploadState.skipped.length)
416 + alertDialog(msg, 'info')
417 + else
418 + toast(msg, 'success')
419 }
420 }
421
416 -function UploadStatus(props: CSSProperties) {
417 - const { done, doneByte, errors, skipped } = useSnapshot(uploadState)
418 - const s = [
419 - done && t('upload_finished', { n: done, size: formatBytes(doneByte) }, "{n} finished ({size})"),
420 - skipped && t('upload_skipped', { n: skipped }, "{n} skipped"),
421 - errors && t('upload_errors', { n: errors }, "{n} failed"),
422 - ].filter(Boolean).join(' – ')
423 - return !s ? null : h('div', { style: props }, s)
422 +function UploadStatus({ snapshot, ...props }: { snapshot?: INTERNAL_Snapshot<typeof uploadState> } & CSSProperties) {
423 + const current = useSnapshot(uploadState)
424 + const { done, doneByte, errors, skipped } = snapshot || current
425 + const msgDone = done.length > 0 && t('upload_finished', { n: done.length, size: formatBytes(doneByte) }, "{n} finished ({size})")
426 + const msgSkipped = skipped.length > 0 && t('upload_skipped', { n: skipped.length }, "{n} skipped")
427 + const msgErrors = errors.length > 0 && t('upload_errors', { n: errors.length }, "{n} failed")
428 + const s = [msgDone, msgSkipped, msgErrors].filter(Boolean).join(' – ')
429 + if (!s) return null
430 + return h('div', { style: { ...props } },
431 + s, ' – ', h(Btn, { label: t`Show details`, asText: true, onClick: showDetails }) )
432 +
433 + function showDetails() {
434 + alertDialog(h('div', {},
435 + ([
436 + [msgDone, done],
437 + [msgSkipped, skipped],
438 + [msgErrors, errors]
439 + ] as const).map(([msg, list]) =>
440 + msg && h('div', {}, msg, h('ul', {},
441 + list.map(x => h('li', {}, x.name || x.file.name)) )))
442 + ))
443 + }
444 }
445
446 function abortCurrentUpload() {