Style tuning
@@ -21,12 +21,10 @@ interface Path {
|
||||
}
|
||||
const documentStore = useDocumentStore()
|
||||
const path: ComputedRef<Path> = computed(() => {
|
||||
const pathList = Router.currentRoute.value.path
|
||||
.split('/')
|
||||
.filter(value => value !== '')
|
||||
|
||||
const p = decodeURIComponent(Router.currentRoute.value.path)
|
||||
const pathList = p.split('/').filter(value => value !== '')
|
||||
return {
|
||||
path: Router.currentRoute.value.path,
|
||||
path: p,
|
||||
pathList
|
||||
}
|
||||
})
|
||||
@@ -54,10 +52,10 @@ const globalShortcutHandler = (event: KeyboardEvent) => {
|
||||
if (event.repeat) return
|
||||
//console.log("key pressed", event)
|
||||
const c = documentStore.fileExplorer.isCursor
|
||||
const keyup = event.type === "keyup"
|
||||
const keyup = event.type === 'keyup'
|
||||
// For up/down implement custom fast repeat
|
||||
if (event.key === "ArrowUp") vert = (keyup ? 0 : event.altKey ? -10 : -1)
|
||||
else if (event.key === "ArrowDown") vert = (keyup ? 0 : event.altKey ? 10 : 1)
|
||||
if (event.key === 'ArrowUp') vert = keyup ? 0 : event.altKey ? -10 : -1
|
||||
else if (event.key === 'ArrowDown') vert = keyup ? 0 : event.altKey ? 10 : 1
|
||||
// Find: process on keydown so that we can bypass the built-in search hotkey
|
||||
else if (!keyup && event.key === 'f' && (event.ctrlKey || event.metaKey)) {
|
||||
headerMain.value!.toggleSearchInput()
|
||||
@@ -67,28 +65,30 @@ const globalShortcutHandler = (event: KeyboardEvent) => {
|
||||
documentStore.fileExplorer.toggleSelectAll()
|
||||
}
|
||||
// Rename
|
||||
else if (c && keyup && !event.ctrlKey && (event.key === 'F2' || event.key === "r")) {
|
||||
else if (c && keyup && !event.ctrlKey && (event.key === 'F2' || event.key === 'r')) {
|
||||
documentStore.fileExplorer.cursorRename()
|
||||
}
|
||||
// Toggle selections on file explorer; ignore all spaces to prevent scrolling built-in hotkey
|
||||
else if (c && event.code === 'Space') {
|
||||
if (keyup && !event.altKey && !event.ctrlKey) documentStore.fileExplorer.cursorSelect()
|
||||
}
|
||||
else return
|
||||
if (keyup && !event.altKey && !event.ctrlKey)
|
||||
documentStore.fileExplorer.cursorSelect()
|
||||
} else return
|
||||
event.preventDefault()
|
||||
if (vertInterval !== null) clearInterval(vertInterval)
|
||||
vertInterval = null
|
||||
if (vert) {
|
||||
vertInterval = setInterval(() => { console.log("X"), documentStore.fileExplorer.cursorMove(vert) }, 30)
|
||||
vertInterval = setInterval(() => {
|
||||
console.log('X'), documentStore.fileExplorer.cursorMove(vert)
|
||||
}, 30)
|
||||
}
|
||||
}
|
||||
onMounted(() => {
|
||||
window.addEventListener("keydown", globalShortcutHandler)
|
||||
window.addEventListener("keyup", globalShortcutHandler)
|
||||
window.addEventListener('keydown', globalShortcutHandler)
|
||||
window.addEventListener('keyup', globalShortcutHandler)
|
||||
})
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener("keydown", globalShortcutHandler)
|
||||
window.removeEventListener("keyup", globalShortcutHandler)
|
||||
window.removeEventListener('keydown', globalShortcutHandler)
|
||||
window.removeEventListener('keyup', globalShortcutHandler)
|
||||
})
|
||||
export type { Path }
|
||||
</script>
|
||||
@@ -96,11 +96,11 @@ export type { Path }
|
||||
<template>
|
||||
<LoginModal />
|
||||
<header>
|
||||
<HeaderMain ref="headerMain"/>
|
||||
<HeaderMain ref="headerMain"><HeaderSelected :path="path.pathList" /></HeaderMain>
|
||||
<BreadCrumb :path="path.pathList" />
|
||||
</header>
|
||||
<main>
|
||||
<RouterView />
|
||||
<RouterView :path="path.pathList" />
|
||||
</main>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -19,10 +19,8 @@
|
||||
}
|
||||
}
|
||||
@media screen and (orientation: portrait) {
|
||||
html {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
.size, .modified {
|
||||
.size,
|
||||
.modified {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -30,10 +28,10 @@
|
||||
html {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
header .buttons:has(input[type=search]) > div {
|
||||
header .buttons:has(input[type='search']) > div {
|
||||
display: none;
|
||||
}
|
||||
header .buttons > div:has(input[type=search]) {
|
||||
header .buttons > div:has(input[type='search']) {
|
||||
display: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="M19.2 2.6H6.1V29h19.8V9.3l-6.7-6.7zM18.5 16v7.1h-5.3V16H8.7l7.1-7.1L23 16h-4.5z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 28 28"><path d="M19.2 2.6H6.1V29h19.8V9.3l-6.7-6.7zM18.5 16v7.1h-5.3V16H8.7l7.1-7.1L23 16h-4.5z"/></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 158 B After Width: | Height: | Size: 158 B |
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="32" viewBox="0 0 448 512"><path d="M223.97 175A81 81 0 0 0 143 256c0 44.7 36.27 81.03 80.97 81.03 44.72 0 80.72-36.34 80.72-81.03 0-44.73-36-81-80.8-81zM386.3 302.53l-14.58 35.16 29.47 57.8-36.1 36.1-59.3-28-35.2 14.4-17.87 54.6-2.28 7.24h-51L177.4 418.2l-35.17-14.5-57.9 29.4-36.1-36.1 27.97-59.2-14.47-35.12L0 282.6v-51l61.7-22.1 14.5-35.1-25.96-51.23-3.43-6.72 36.1-36.03 59.3 27.92 35.1-14.5 17.9-54.6 2.3-7.24h51l22.1 61.73 35.07 14.52 58.04-29.4 36.06 36.03-27.96 59.2 14.42 35.17 61.8 20.13v50.97l-61.67 22.18z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path d="M223.97 175A81 81 0 0 0 143 256c0 44.7 36.27 81.03 80.97 81.03 44.72 0 80.72-36.34 80.72-81.03 0-44.73-36-81-80.8-81zM386.3 302.53l-14.58 35.16 29.47 57.8-36.1 36.1-59.3-28-35.2 14.4-17.87 54.6-2.28 7.24h-51L177.4 418.2l-35.17-14.5-57.9 29.4-36.1-36.1 27.97-59.2-14.47-35.12L0 282.6v-51l61.7-22.1 14.5-35.1-25.96-51.23-3.43-6.72 36.1-36.03 59.3 27.92 35.1-14.5 17.9-54.6 2.3-7.24h51l22.1 61.73 35.07 14.52 58.04-29.4 36.06 36.03-27.96 59.2 14.42 35.17 61.8 20.13v50.97l-61.67 22.18z"/></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 586 B After Width: | Height: | Size: 563 B |
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-3 0 34 34"><path d="M23 25.9c0-.3-.1-.6-.3-.8s-.5-.3-.8-.3c-.3 0-.6.1-.8.3-.2.2-.3.5-.3.8s.1.6.3.8c.2.2.5.3.8.3.3 0 .6-.1.8-.3.2-.3.3-.5.3-.8zm4.6 0c0-.3-.1-.6-.3-.8s-.5-.3-.8-.3c-.3 0-.6.1-.8.3-.2.2-.3.5-.3.8s.1.6.3.8c.2.2.5.3.8.3.3 0 .6-.1.8-.3.2-.3.3-.5.3-.8zm2.3-4v5.7c0 .5-.2.9-.5 1.2-.3.3-.7.5-1.2.5H1.9c-.5 0-.9-.2-1.2-.5s-.5-.7-.5-1.2v-5.7c0-.5.2-.9.5-1.2.3-.3.7-.5 1.2-.5h8.3l2.4 2.4c.7.7 1.5 1 2.4 1 .9 0 1.7-.3 2.4-1l2.4-2.4h8.3c.5 0 .9.2 1.2.5.4.3.6.7.6 1.2zm-5.8-10.2c.2.5.1.9-.3 1.3l-8 8c-.2.2-.5.3-.8.3-.3 0-.6-.1-.8-.3l-8-8c-.4-.3-.5-.8-.3-1.3S6.5 11 7 11h4.6V3c0-.3.1-.6.3-.8s.5-.3.8-.3h4.6c.3 0 .6.1.8.3s.3.5.3.8v8H23c.5 0 .8.2 1.1.7z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30"><path d="M23 25.9c0-.3-.1-.6-.3-.8s-.5-.3-.8-.3c-.3 0-.6.1-.8.3-.2.2-.3.5-.3.8s.1.6.3.8c.2.2.5.3.8.3.3 0 .6-.1.8-.3.2-.3.3-.5.3-.8zm4.6 0c0-.3-.1-.6-.3-.8s-.5-.3-.8-.3c-.3 0-.6.1-.8.3-.2.2-.3.5-.3.8s.1.6.3.8c.2.2.5.3.8.3.3 0 .6-.1.8-.3.2-.3.3-.5.3-.8zm2.3-4v5.7c0 .5-.2.9-.5 1.2-.3.3-.7.5-1.2.5H1.9c-.5 0-.9-.2-1.2-.5s-.5-.7-.5-1.2v-5.7c0-.5.2-.9.5-1.2.3-.3.7-.5 1.2-.5h8.3l2.4 2.4c.7.7 1.5 1 2.4 1 .9 0 1.7-.3 2.4-1l2.4-2.4h8.3c.5 0 .9.2 1.2.5.4.3.6.7.6 1.2zm-5.8-10.2c.2.5.1.9-.3 1.3l-8 8c-.2.2-.5.3-.8.3-.3 0-.6-.1-.8-.3l-8-8c-.4-.3-.5-.8-.3-1.3S6.5 11 7 11h4.6V3c0-.3.1-.6.3-.8s.5-.3.8-.3h4.6c.3 0 .6.1.8.3s.3.5.3.8v8H23c.5 0 .8.2 1.1.7z"/></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 711 B After Width: | Height: | Size: 711 B |
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="10 10 372 468"><path d="M128 344V168c0-4.5-3.5-8-8-8h-16c-4.5 0-8 3.5-8 8v176c0 4.5 3.5 8 8 8h16c4.5 0 8-3.5 8-8zm64 0V168c0-4.5-3.5-8-8-8h-16c-4.5 0-8 3.5-8 8v176c0 4.5 3.5 8 8 8h16c4.5 0 8-3.5 8-8zm64 0V168c0-4.5-3.5-8-8-8h-16c-4.5 0-8 3.5-8 8v176c0 4.5 3.5 8 8 8h16c4.5 0 8-3.5 8-8zM120 96h112l-12-29.25c-.75-1-3-2.5-4.25-2.75H136.5c-1.5.25-3.5 1.75-4.25 2.75zm232 8v16c0 4.5-3.5 8-8 8h-24v237c0 27.5-18 51-40 51H72c-22 0-40-22.5-40-50V128H8c-4.5 0-8-3.5-8-8v-16c0-4.5 3.5-8 8-8h77.25l17.5-41.75C107.75 42 122.75 32 136 32h80c13.25 0 28.25 10 33.25 22.25L266.75 96H344c4.5 0 8 3.5 8 8z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="10 40 372 490"><path d="M128 344V168c0-4.5-3.5-8-8-8h-16c-4.5 0-8 3.5-8 8v176c0 4.5 3.5 8 8 8h16c4.5 0 8-3.5 8-8zm64 0V168c0-4.5-3.5-8-8-8h-16c-4.5 0-8 3.5-8 8v176c0 4.5 3.5 8 8 8h16c4.5 0 8-3.5 8-8zm64 0V168c0-4.5-3.5-8-8-8h-16c-4.5 0-8 3.5-8 8v176c0 4.5 3.5 8 8 8h16c4.5 0 8-3.5 8-8zM120 96h112l-12-29.25c-.75-1-3-2.5-4.25-2.75H136.5c-1.5.25-3.5 1.75-4.25 2.75zm232 8v16c0 4.5-3.5 8-8 8h-24v237c0 27.5-18 51-40 51H72c-22 0-40-22.5-40-50V128H8c-4.5 0-8-3.5-8-8v-16c0-4.5 3.5-8 8-8h77.25l17.5-41.75C107.75 42 122.75 32 136 32h80c13.25 0 28.25 10 33.25 22.25L266.75 96H344c4.5 0 8 3.5 8 8z"/></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 647 B After Width: | Height: | Size: 647 B |
@@ -2,25 +2,20 @@
|
||||
<div class="breadcrumb">
|
||||
<a href="#/"><component :is="home" /></a>
|
||||
<template v-for="(location, index) in props.path" :key="index">
|
||||
<a :href="`/#/${props.path.slice(0, index + 1).join('/')}/`">{{
|
||||
decodeURIComponent(location)
|
||||
}}</a>
|
||||
<a :href="`/#/${props.path.slice(0, index + 1).join('/')}/`">{{ location }}</a>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
//import home from '@/assets/svg/home.svg'
|
||||
import { withDefaults, defineProps, defineAsyncComponent } from 'vue'
|
||||
import { defineProps, defineAsyncComponent } from 'vue'
|
||||
|
||||
const home = defineAsyncComponent(() => import(`@/assets/svg/home.svg`))
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
path: Array<string>
|
||||
}>(),
|
||||
{}
|
||||
)
|
||||
const props = defineProps<{
|
||||
path: Array<string>
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@@ -82,7 +77,7 @@ const props = withDefaults(
|
||||
}
|
||||
.breadcrumb svg {
|
||||
/* FIXME: Custom positioning to align it well; needs proper solution */
|
||||
padding-left: .6rem;
|
||||
padding-left: 0.6rem;
|
||||
width: 1.3rem;
|
||||
height: 1.3rem;
|
||||
fill: var(--breadcrumb-color);
|
||||
@@ -94,10 +89,12 @@ const props = withDefaults(
|
||||
.breadcrumb a:nth-child(even) {
|
||||
background: var(--breadcrumb-background-even);
|
||||
}
|
||||
.breadcrumb a:nth-child(odd):hover, .breadcrumb a:focus:nth-child(odd) {
|
||||
.breadcrumb a:nth-child(odd):hover,
|
||||
.breadcrumb a:focus:nth-child(odd) {
|
||||
background: var(--breadcrumb-hover-background-odd);
|
||||
}
|
||||
.breadcrumb a:nth-child(even):hover, .breadcrumb a:focus:nth-child(even) {
|
||||
.breadcrumb a:nth-child(even):hover,
|
||||
.breadcrumb a:focus:nth-child(even) {
|
||||
background: var(--breadcrumb-hover-background-even);
|
||||
}
|
||||
.breadcrumb a:hover {
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
Modified
|
||||
</th>
|
||||
<th
|
||||
class="sortcolumn modified right"
|
||||
class="sortcolumn size right"
|
||||
:class="{ sortactive: sort === 'size' }"
|
||||
@click="toggleSort('size')"
|
||||
>
|
||||
@@ -69,7 +69,11 @@
|
||||
type="checkbox"
|
||||
tabindex="-1"
|
||||
:checked="documentStore.selected.has(doc.key)"
|
||||
@change="($event.target as HTMLInputElement).checked ? documentStore.selected.add(doc.key) : documentStore.selected.delete(doc.key)"
|
||||
@change="
|
||||
($event.target as HTMLInputElement).checked
|
||||
? documentStore.selected.add(doc.key)
|
||||
: documentStore.selected.delete(doc.key)
|
||||
"
|
||||
/>
|
||||
</td>
|
||||
<td class="name">
|
||||
@@ -84,13 +88,24 @@
|
||||
"
|
||||
/></template>
|
||||
<template v-else>
|
||||
<a :href="url_for(doc)" tabindex="-1" @contextmenu.stop @click.stop @focus.stop="cursor = doc">{{ doc.name }}</a>
|
||||
<a
|
||||
:href="url_for(doc)"
|
||||
tabindex="-1"
|
||||
@contextmenu.stop
|
||||
@click.stop
|
||||
@focus.stop="cursor = doc"
|
||||
>{{ doc.name }}</a
|
||||
>
|
||||
<button @click="() => (editing = doc)">🖊️</button>
|
||||
</template>
|
||||
</td>
|
||||
<td class="modified right">{{ doc.modified }}</td>
|
||||
<td class="size right">{{ doc.sizedisp }}</td>
|
||||
<td class="menu"><button tabindex="-1" @click.stop="cursor = doc; contextMenu($event, doc)">⋮</button></td>
|
||||
<td class="menu">
|
||||
<button tabindex="-1" @click.stop="cursor = doc; contextMenu($event, doc)">
|
||||
⋮
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -107,18 +122,15 @@ import { formatSize, formatUnixDate } from '@/utils'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
path: string
|
||||
path: Array<string>
|
||||
documents: Document[]
|
||||
}>(),
|
||||
{}
|
||||
)
|
||||
|
||||
const documentStore = useDocumentStore()
|
||||
const linkBasePath = computed(() => {
|
||||
const path = props.path
|
||||
return path === '/' ? '' : path
|
||||
})
|
||||
const filesBasePath = computed(() => `/files${linkBasePath.value}`)
|
||||
const linkBasePath = computed(() => props.path.join('/'))
|
||||
const filesBasePath = computed(() => `/files/${linkBasePath.value}`)
|
||||
const url_for = (doc: FolderDocument) =>
|
||||
doc.type === 'folder'
|
||||
? `#${linkBasePath.value}/${doc.name}/`
|
||||
@@ -162,7 +174,7 @@ defineExpose({
|
||||
}
|
||||
},
|
||||
toggleSelectAll() {
|
||||
console.log("Select")
|
||||
console.log('Select')
|
||||
allSelected.value = !allSelected.value
|
||||
},
|
||||
isCursor() {
|
||||
@@ -172,7 +184,7 @@ defineExpose({
|
||||
editing.value = cursor.value
|
||||
},
|
||||
cursorSelect() {
|
||||
console.log("select", documentStore.selected)
|
||||
console.log('select', documentStore.selected)
|
||||
const doc = cursor.value
|
||||
if (!doc) return
|
||||
if (documentStore.selected.has(doc.key)) {
|
||||
@@ -191,14 +203,18 @@ defineExpose({
|
||||
const mod = (a: number, b: number) => ((a % b) + b) % b
|
||||
const index = cursor.value !== null ? documents.indexOf(cursor.value) : -1
|
||||
cursor.value = documents[mod(index + d, documents.length + 1)] ?? null
|
||||
const tr = document.getElementById(`file-${cursor.value.key}`) as HTMLTableRowElement | null
|
||||
const tr = document.getElementById(
|
||||
`file-${cursor.value.key}`
|
||||
) as HTMLTableRowElement | null
|
||||
// @ts-ignore
|
||||
if (tr) tr.scrollIntoView({ block: 'center' })
|
||||
}
|
||||
})
|
||||
watchEffect(() => {
|
||||
if (cursor.value) {
|
||||
const a = document.querySelector(`#file-${cursor.value.key} .name a`) as HTMLAnchorElement | null
|
||||
const a = document.querySelector(
|
||||
`#file-${cursor.value.key} .name a`
|
||||
) as HTMLAnchorElement | null
|
||||
if (a) a.focus()
|
||||
}
|
||||
})
|
||||
@@ -229,7 +245,8 @@ const toggleSort = (name: string) => {
|
||||
}
|
||||
const sort = ref<string>('')
|
||||
const sortCompare = {
|
||||
name: (a: Document, b: Document) => a.name.localeCompare(b.name, undefined, {numeric: true, sensitivity: 'base'}),
|
||||
name: (a: Document, b: Document) =>
|
||||
a.name.localeCompare(b.name, undefined, { numeric: true, sensitivity: 'base' }),
|
||||
modified: (a: FolderDocument, b: FolderDocument) => b.mtime - a.mtime,
|
||||
size: (a: FolderDocument, b: FolderDocument) => b.size - a.size
|
||||
}
|
||||
@@ -258,7 +275,7 @@ const allSelected = computed({
|
||||
)
|
||||
},
|
||||
set: (value: boolean) => {
|
||||
console.log("Setting allSelected", value)
|
||||
console.log('Setting allSelected', value)
|
||||
for (const doc of props.documents) {
|
||||
if (value) {
|
||||
documentStore.selected.add(doc.key)
|
||||
@@ -287,10 +304,10 @@ table .selection {
|
||||
width: 1rem;
|
||||
}
|
||||
table .modified {
|
||||
width: 10rem;
|
||||
width: 9rem;
|
||||
}
|
||||
table .size {
|
||||
width: 5rem;
|
||||
width: 4rem;
|
||||
}
|
||||
table .menu {
|
||||
width: 1rem;
|
||||
|
||||
@@ -30,8 +30,11 @@ defineExpose({
|
||||
<nav>
|
||||
<div class="buttons">
|
||||
<UploadButton />
|
||||
<SvgButton name="create-folder" @click="() => documentStore.fileExplorer.newFolder()"/>
|
||||
<HeaderSelected />
|
||||
<SvgButton
|
||||
name="create-folder"
|
||||
@click="() => documentStore.fileExplorer.newFolder()"
|
||||
/>
|
||||
<slot></slot>
|
||||
<div class="spacer"></div>
|
||||
<template v-if="showSearchInput">
|
||||
<input
|
||||
@@ -60,12 +63,6 @@ defineExpose({
|
||||
.spacer {
|
||||
flex-grow: 1;
|
||||
}
|
||||
.smallgap {
|
||||
margin-left: 2em;
|
||||
}
|
||||
.select-text {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
input[type='search'] {
|
||||
background: var(--primary-background);
|
||||
color: var(--primary-color);
|
||||
|
||||
@@ -1,84 +1,114 @@
|
||||
<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>
|
||||
<p class="select-text">{{ documentStore.selected.size }} selected ➤</p>
|
||||
<SvgButton name="download" @click="download" />
|
||||
<SvgButton name="copy" @click="op('cp', dst)" />
|
||||
<SvgButton name="paste" @click="op('mv', dst)" />
|
||||
<SvgButton name="trash" @click="op('rm')" />
|
||||
<button @click="documentStore.selected.clear()">❌</button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import createWebSocket from '@/repositories/WS'
|
||||
import { useDocumentStore } from '@/stores/documents'
|
||||
import { computed } from 'vue'
|
||||
import type { SelectedItems } from '@/repositories/Document'
|
||||
|
||||
const documentStore = useDocumentStore()
|
||||
const props = defineProps({
|
||||
path: Array<string>
|
||||
})
|
||||
|
||||
const dst = computed(() => props.path!.join('/'))
|
||||
const op = (op: string, dst?: string) => {
|
||||
const sel = documentStore.selectedFiles
|
||||
const msg = {
|
||||
op,
|
||||
sel: sel.ids.filter(id => sel.selected.has(id)).map(id => sel.fullpath[id])
|
||||
}
|
||||
// @ts-ignore
|
||||
if (dst !== undefined) msg.dst = dst
|
||||
const control = createWebSocket('/api/control', ev => {
|
||||
const res = JSON.parse(ev.data)
|
||||
if ('error' in res) {
|
||||
console.error('Control socket error', msg, res.error)
|
||||
return
|
||||
} else if (res.status === 'ack') {
|
||||
console.log('Control ack OK', res)
|
||||
control.close()
|
||||
documentStore.selected.clear()
|
||||
return
|
||||
} else console.log('Unknown control respons', msg, res)
|
||||
})
|
||||
control.onopen = () => {
|
||||
control.send(JSON.stringify(msg))
|
||||
}
|
||||
}
|
||||
|
||||
const linkdl = (href: string) => {
|
||||
const a = document.createElement("a")
|
||||
const a = document.createElement('a')
|
||||
a.href = href
|
||||
a.download = ''
|
||||
a.click()
|
||||
}
|
||||
|
||||
const filesystemdl = async (sel: SelectedItems, handle: FileSystemDirectoryHandle) => {
|
||||
let hdir = ""
|
||||
let hdir = ''
|
||||
let h = handle
|
||||
let filelist = []
|
||||
for (const id of sel.ids) {
|
||||
filelist.push(sel.relpath[id])
|
||||
}
|
||||
console.log("Downloading to filesystem", filelist)
|
||||
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
|
||||
const url = sel.url[id] // Only files, not folders
|
||||
// Create any missing directories
|
||||
if (!rel.startsWith(hdir)) {
|
||||
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 })
|
||||
h = await h.getDirectoryHandle(dir.normalize('NFC'), { create: true })
|
||||
} catch (error) {
|
||||
console.error("Failed to create directory", hdir, error)
|
||||
console.error('Failed to create directory', hdir, error)
|
||||
return
|
||||
}
|
||||
console.log("Created", hdir)
|
||||
console.log('Created', hdir)
|
||||
}
|
||||
if (!url) continue // Target was a folder and was created
|
||||
const name = rel.split('/').pop().normalize('NFC')
|
||||
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)
|
||||
console.error('Failed to create file', hdir + name, error)
|
||||
return
|
||||
}
|
||||
const writable = await fileHandle.createWritable()
|
||||
console.log("Fetching", url)
|
||||
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.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)
|
||||
console.log('Saved', hdir + name)
|
||||
}
|
||||
}
|
||||
|
||||
const download = async () => {
|
||||
const sel = documentStore.selectedFiles
|
||||
console.log("Download", sel)
|
||||
console.log('Download', sel)
|
||||
if (sel.selected.size === 0) {
|
||||
console.warn("Attempted download but no files found. Missing:", sel.missing)
|
||||
console.warn('Attempted download but no files found. Missing:', sel.missing)
|
||||
documentStore.selected.clear()
|
||||
return
|
||||
}
|
||||
@@ -89,25 +119,35 @@ const download = async () => {
|
||||
return linkdl(urls[0] as string)
|
||||
}
|
||||
// Use FileSystem API if multiple files and the browser supports it
|
||||
if ("showDirectoryPicker" in window) {
|
||||
if ('showDirectoryPicker' in window) {
|
||||
try {
|
||||
// @ts-ignore
|
||||
const handle = await window.showDirectoryPicker({ startIn: 'downloads', mode: 'readwrite' })
|
||||
filesystemdl(sel, handle, "").then(() => { documentStore.selected.clear() })
|
||||
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)
|
||||
console.error('Download to folder aborted', e)
|
||||
}
|
||||
}
|
||||
// Otherwise, zip and download
|
||||
linkdl(`/zip/${sel.selected.join('+')}/download.zip`)
|
||||
linkdl(`/zip/${Array.from(sel.selected).join('+')}/download.zip`)
|
||||
documentStore.selected.clear()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.selected-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.smallgap {
|
||||
margin-left: 2em;
|
||||
}
|
||||
.select-text {
|
||||
color: var(--accent-color);
|
||||
text-wrap: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -26,7 +26,8 @@ button {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
}
|
||||
button:hover, button:focus {
|
||||
button:hover,
|
||||
button:focus {
|
||||
color: #fff;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
@@ -34,7 +35,8 @@ svg {
|
||||
fill: #ccc;
|
||||
transform: fill 0.2s ease;
|
||||
}
|
||||
button:hover svg, button:focus svg {
|
||||
button:hover svg,
|
||||
button:focus svg {
|
||||
fill: #fff;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -65,7 +65,6 @@ export interface SelectedItems {
|
||||
ids: FUID[]
|
||||
}
|
||||
|
||||
|
||||
export const url_document_watch_ws = '/api/watch'
|
||||
export const url_document_upload_ws = '/api/upload'
|
||||
export const url_document_get = '/files'
|
||||
|
||||
@@ -4,7 +4,7 @@ import type {
|
||||
FileEntry,
|
||||
FUID,
|
||||
DirList,
|
||||
SelectedItems,
|
||||
SelectedItems
|
||||
} from '@/repositories/Document'
|
||||
import { formatSize, formatUnixDate } from '@/utils'
|
||||
import { defineStore } from 'pinia'
|
||||
@@ -74,7 +74,14 @@ export const useDocumentStore = defineStore({
|
||||
}
|
||||
// Pre sort directory entries folders first then files, names in natural ordering
|
||||
dataMapped.sort((a, b) =>
|
||||
a.type === b.type ? a.name.localeCompare(b.name, undefined, {numeric: true, sensitivity: 'base'}) : a.type === 'folder' ? -1 : 1
|
||||
a.type === b.type
|
||||
? a.name.localeCompare(b.name, undefined, {
|
||||
numeric: true,
|
||||
sensitivity: 'base'
|
||||
})
|
||||
: a.type === 'folder'
|
||||
? -1
|
||||
: 1
|
||||
)
|
||||
this.document = dataMapped
|
||||
},
|
||||
@@ -183,7 +190,7 @@ export const useDocumentStore = defineStore({
|
||||
ret.fullpath[attr.id] = fullname
|
||||
ret.relpath[attr.id] = r
|
||||
ret.ids.push(attr.id)
|
||||
if (!("dir" in attr)) ret.url[attr.id] = `/files/${fullname}`
|
||||
if (!('dir' in attr)) ret.url[attr.id] = `/files/${fullname}`
|
||||
}
|
||||
traverseDir(attr, fullname, r)
|
||||
}
|
||||
@@ -205,7 +212,12 @@ export const useDocumentStore = defineStore({
|
||||
if (!ret.selected.has(id)) ret.missing.add(id)
|
||||
}
|
||||
// Sorted array of FUIDs for easy traversal
|
||||
ret.ids.sort((a, b) => ret.relpath[a].localeCompare(ret.relpath[b] , undefined, {numeric: true, sensitivity: 'base'}))
|
||||
ret.ids.sort((a, b) =>
|
||||
ret.relpath[a].localeCompare(ret.relpath[b], undefined, {
|
||||
numeric: true,
|
||||
sensitivity: 'base'
|
||||
})
|
||||
)
|
||||
return ret
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<FileExplorer
|
||||
ref="fileExplorer"
|
||||
:key="Router.currentRoute.value.path"
|
||||
:path="Router.currentRoute.value.path"
|
||||
:path="props.path"
|
||||
:documents="documentStore.mainDocument"
|
||||
/>
|
||||
</template>
|
||||
@@ -15,7 +15,12 @@ import Router from '@/router/index'
|
||||
const documentStore = useDocumentStore()
|
||||
const fileExplorer = ref()
|
||||
|
||||
watchEffect(() => { documentStore.fileExplorer = fileExplorer.value })
|
||||
const props = defineProps({
|
||||
path: Array<string>
|
||||
})
|
||||
watchEffect(() => {
|
||||
documentStore.fileExplorer = fileExplorer.value
|
||||
})
|
||||
watchEffect(async () => {
|
||||
const path = new String(Router.currentRoute.value.path) as string
|
||||
documentStore.setActualDocument(path.toString())
|
||||
|
||||