Multiple file downloads via Filesystem API, and other tuning.

This commit is contained in:
Leo Vasanko
2023-11-04 19:50:05 +00:00
parent 40a45568c1
commit 41fbd3d122
10 changed files with 203 additions and 66 deletions

View File

@@ -82,7 +82,9 @@ const props = withDefaults(
}
.breadcrumb svg {
/* FIXME: Custom positioning to align it well; needs proper solution */
transform: translate(0.3rem, -0.3rem) scale(80%);
padding-left: .6rem;
width: 1.3rem;
height: 1.3rem;
fill: var(--breadcrumb-color);
transition: fill var(--breadcrumb-transtime);
}

View File

@@ -1,16 +1,18 @@
<script setup lang="ts">
import { useDocumentStore } from '@/stores/documents'
import { ref, nextTick, watchEffect } from 'vue'
import { ref, nextTick } from 'vue'
const documentStore = useDocumentStore()
const showSearchInput = ref<boolean>(false)
const search = ref<HTMLInputElement | null>()
const searchButton = ref<HTMLButtonElement | null>()
const toggleSearchInput = () => {
showSearchInput.value = !showSearchInput.value
nextTick(() => {
const input = search.value
if (input) input.focus()
else if (searchButton.value) searchButton.value.blur()
executeSearch()
})
}
@@ -29,16 +31,7 @@ defineExpose({
<div class="buttons">
<UploadButton />
<SvgButton name="create-folder" @click="() => documentStore.fileExplorer.newFolder()"/>
<template v-if="documentStore.selected.size > 0">
<div class="smallgap"></div>
<p class="select-text">{{ documentStore.selected.size }} selected </p>
<!-- Needs better icons for copy/move/remove -->
<SvgButton name="download" />
<SvgButton name="copy" />
<SvgButton name="paste" />
<SvgButton name="trash" />
<button @click="documentStore.selected.clear()"></button>
</template>
<HeaderSelected />
<div class="spacer"></div>
<template v-if="showSearchInput">
<input
@@ -49,7 +42,7 @@ defineExpose({
@input="executeSearch"
/>
</template>
<SvgButton name="find" @click="toggleSearchInput" />
<SvgButton ref="searchButton" name="find" @click="toggleSearchInput" />
<SvgButton name="cog" @click="console.log('TODO open settings')" />
</div>
</nav>
@@ -61,6 +54,9 @@ defineExpose({
display: flex;
align-items: center;
}
.buttons > * {
flex-shrink: 1;
}
.spacer {
flex-grow: 1;
}
@@ -70,17 +66,14 @@ defineExpose({
.select-text {
color: var(--accent-color);
}
.search-widget {
display: flex;
align-items: center;
}
input[type='search'] {
background: var(--primary-background);
color: var(--text-color);
color: var(--primary-color);
border: 0;
border-radius: 0.1rem;
padding: 0.5rem;
outline: none;
font-size: 1.2rem;
font-size: 1.5rem;
max-width: 30vw;
}
</style>

View File

@@ -0,0 +1,113 @@
<template v-if="documentStore.selected.size">
<div class="smallgap"></div>
<div class="selected-actions">
<p class="select-text">{{ documentStore.selected.size }} selected </p>
<SvgButton name="download" @click="download"/>
<SvgButton name="copy" />
<SvgButton name="paste" />
<SvgButton name="trash" />
<button @click="documentStore.selected.clear()"></button>
</div>
</template>
<script setup lang="ts">
import { useDocumentStore } from '@/stores/documents'
const documentStore = useDocumentStore()
const linkdl = (href: string) => {
const a = document.createElement("a")
a.href = href
a.download = ''
a.click()
}
const filesystemdl = async (sel: SelectedItems, handle: FileSystemDirectoryHandle) => {
let hdir = ""
let h = handle
let filelist = []
for (const id of sel.ids) {
filelist.push(sel.relpath[id])
}
console.log("Downloading to filesystem", filelist)
for (const id of sel.ids) {
const rel = sel.relpath[id]
const url = sel.url[id] // Only files, not folders
// Create any missing directories
if (!rel.startsWith(hdir)) {
hdir = ""
h = handle
}
const r = rel.slice(hdir.length)
for (const dir of r.split('/').slice(0, url ? -1 : undefined)) {
hdir += `${dir}/`
try {
h = await h.getDirectoryHandle(dir.normalize("NFC"), { create: true })
} catch (error) {
console.error("Failed to create directory", hdir, error)
return
}
console.log("Created", hdir)
}
if (!url) continue // Target was a folder and was created
const name = rel.split('/').pop().normalize('NFC')
// Download file
let fileHandle
try {
fileHandle = await h.getFileHandle(name, { create: true })
} catch (error) {
console.error("Failed to create file", hdir + name, error)
return
}
const writable = await fileHandle.createWritable()
console.log("Fetching", url)
const res = await fetch(url)
if (!res.ok) throw new Error(`Failed to download ${url}: ${res.status} ${res.statusText}`)
if (res.body) await res.body.pipeTo(writable)
else {
// Zero-sized files don't have a body, so we need to create an empty file
await writable.truncate(0)
await writable.close()
}
console.log("Saved", hdir + name)
}
}
const download = async () => {
const sel = documentStore.selectedFiles
console.log("Download", sel)
if (sel.selected.size === 0) {
console.warn("Attempted download but no files found. Missing:", sel.missing)
documentStore.selected.clear()
return
}
// Plain old a href download if only one file (ignoring any folders)
const urls = Object.values(sel.url)
if (urls.length === 1) {
documentStore.selected.clear()
return linkdl(urls[0] as string)
}
// Use FileSystem API if multiple files and the browser supports it
if ("showDirectoryPicker" in window) {
try {
// @ts-ignore
const handle = await window.showDirectoryPicker({ startIn: 'downloads', mode: 'readwrite' })
filesystemdl(sel, handle, "").then(() => { documentStore.selected.clear() })
return
} catch (e) {
console.error("Download to folder aborted", e)
}
}
// Otherwise, zip and download
linkdl(`/zip/${sel.selected.join('+')}/download.zip`)
documentStore.selected.clear()
}
</script>
<style scoped>
.selected-actions {
display: flex;
align-items: center;
}
</style>

View File

@@ -22,7 +22,9 @@ button {
color: #ccc;
cursor: pointer;
transition: all 0.2s ease;
padding: 0.5rem;
padding: 0.2rem;
width: 3rem;
height: 3rem;
}
button:hover, button:focus {
color: #fff;
@@ -31,8 +33,6 @@ button:hover, button:focus {
svg {
fill: #ccc;
transform: fill 0.2s ease;
width: 1rem;
height: 1rem;
}
button:hover svg, button:focus svg {
fill: #fff;