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