accept uploads via drag&drop
Massimo Melina committed
Feb 1, 2023 at 00:43 UTC
6c501a4e70497dd42e5c50e0489d829e7fbd414d
4 files changed
+35
-21
frontend/src/BrowseFiles.ts
+3
-2
@@ -9,6 +9,7 @@ import { state, useSnapState } from './state'
9
import { alertDialog } from './dialog'
10
import useFetchList from './useFetchList'
11
import useAuthorized from './useAuthorized'
12
+import { acceptDropFiles, enqueue } from './upload'
13
14
export function usePath() {
15
return decodeURI(useLocation().pathname)
@@ -29,7 +30,7 @@ export function BrowseFiles() {
30
}
31
32
function FilesList() {
32
- const { filteredList, list, loading, stoppedSearch } = useSnapState()
33
+ const { filteredList, list, loading, stoppedSearch, can_upload } = useSnapState()
34
const midnight = useMidnight() // as an optimization we calculate this only once per list and pass it down
35
const pageSize = 100
36
const [page, setPage] = useState(0)
@@ -41,7 +42,7 @@ function FilesList() {
42
useEffect(() => document.scrollingElement?.scrollTo(0,0), [page])
43
44
return h(Fragment, {},
44
- h('ul', { className: 'dir' },
45
+ h('ul', { className: 'dir', ...acceptDropFiles(can_upload && enqueue) },
46
!list.length ? (!loading && (stoppedSearch ? "Stopped before finding anything" : "Nothing here"))
47
: filteredList && !filteredList.length ? "No match for this filter"
48
: theList.slice(offset, offset + pageSize).map((entry: DirEntry) =>
frontend/src/components.ts
+4
-3
@@ -7,14 +7,15 @@ export function Spinner() {
7
return hIcon('spinner', { className:'spinner' })
8
}
9
10
-export function Flex({ gap='1em', vert=false, children=null, ...props }) {
10
+export function Flex({ gap='1em', vert=false, children=null, props={}, ...rest }) {
11
return h('div', {
12
style: {
13
display: 'flex',
14
gap,
15
flexDirection: vert ? 'column' : undefined,
16
- ...props,
17
- }
16
+ ...rest,
17
+ },
18
+ ...props
19
}, children)
20
}
21
frontend/src/upload.ts
+28
-12
@@ -79,24 +79,14 @@ export function showUpload() {
79
const { can_upload } = useSnapState()
80
const etaStr = useMemo(() => !eta ? '' : formatTime(eta*1000, 0, 2), [eta])
81
82
- return h(FlexV, {},
82
+ return h(FlexV, { props: acceptDropFiles(x => setFiles([ ...files, ...x ])) },
83
h(Flex, { gap: '.5em', flexWrap: 'wrap', justifyContent: 'center', position: 'sticky', top: -4, background: 'var(--bg)', boxShadow: '0 3px 3px #000' },
84
can_upload && h('button',{ onClick: () => selectFiles() }, "Add file(s)"),
85
can_upload && h('button',{ onClick: () => selectFiles(true) }, "Add folder"),
86
files.length > 1 && h('button', { onClick() { setFiles([]) } }, "Clear"),
87
files.length > 0 && h('button', {
88
onClick() {
89
- const to = location.pathname
90
- const ready = _.find(uploadState.qs, { to })
91
- if (!ready)
92
- uploadState.qs.push({ to, files: files.map(ref) })
93
- else {
94
- _.remove(ready.files, f => { // avoid duplicates
95
- const match = path(f)
96
- return Boolean(_.find(files, x => match === path(x)))
97
- })
98
- ready.files.push(...files.map(ref))
99
- }
89
+ enqueue(files)
90
setFiles([])
91
}
92
}, `Send ${files.length} file(s), ${formatBytes(files.reduce((a, f) => a + f.size, 0))}`),
@@ -207,6 +197,18 @@ subscribe(uploadState, () => {
197
startUpload(cur.files[0], cur.to).then()
198
})
199
200
+export function enqueue(files: File[]) {
201
+ const to = location.pathname
202
+ const ready = _.find(uploadState.qs, { to })
203
+ if (!ready)
204
+ return uploadState.qs.push({ to, files: files.map(ref) })
205
+ _.remove(ready.files, f => { // avoid duplicates
206
+ const match = path(f)
207
+ return Boolean(_.find(files, x => match === path(x)))
208
+ })
209
+ ready.files.push(...files.map(ref))
210
+}
211
+
212
let req: XMLHttpRequest | undefined
213
let overrideStatus = 0
214
let notificationChannel = ''
@@ -230,6 +232,7 @@ async function startUpload(f: File, to: string, resume=0) {
232
if (!resuming)
233
next()
234
}
235
+ req.onerror = () => alertDialog("Couldn't upload " + f.name)
236
let lastProgress = 0
237
req.upload.onprogress = (e:any) => {
238
uploadState.partial = e.loaded + resume
@@ -304,3 +307,16 @@ async function startUpload(f: File, to: string, resume=0) {
307
function abortCurrentUpload() {
308
req?.abort()
309
}
310
+
311
+export function acceptDropFiles(cb: false | ((files:File[]) => void)) {
312
+ return {
313
+ onDragOver(ev: DragEvent) {
314
+ ev.preventDefault()
315
+ ev.dataTransfer!.dropEffect = cb ? 'copy' : 'none'
316
+ },
317
+ onDrop(ev: DragEvent) {
318
+ ev.preventDefault()
319
+ cb && cb(Array.from(ev.dataTransfer!.files))
320
+ },
321
+ }
322
+}
\ No newline at end of file
todo.md
-4
@@ -1,5 +1,4 @@
1
# To do
2
-- upload via drag&drop
2
- admin/fs: check if source exist when set
3
- plugins: after installing, switch to installed (and perhaps highlight new one)
4
- plugins' log, accessible in admin
@@ -38,8 +37,6 @@
37
- publish to npm (so people can "npm install hfs")
38
- frontend search supporting masks
39
- remove seconds from time
41
-- upload
42
-- upload unzipping (while streaming?)
40
- plugin: upload quota per-account (possibly inheriting), and a default
41
- plugin: make letsencrypt easier
42
- could be just automatic detection of files by certbot
@@ -48,7 +45,6 @@
45
- updater (stop,unzip,start)
46
- node.comment
47
- config: max connections/downloads (total/per-ip)
51
-- config: min disk space
48
- thumbnails support
49
- webdav?
50
- log: ip2name