Compare commits
6 Commits
b25d0fc14b
...
32fa005c62
Author | SHA1 | Date | |
---|---|---|---|
32fa005c62 | |||
fabec4dd7e | |||
ece64f48be | |||
1f24313d23 | |||
e3af21af91 | |||
6938740b0f |
|
@ -15,7 +15,6 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vueuse/core": "^10.4.1",
|
"@vueuse/core": "^10.4.1",
|
||||||
"esbuild": "^0.19.5",
|
"esbuild": "^0.19.5",
|
||||||
"locale-includes": "^1.0.5",
|
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"pinia": "^2.1.6",
|
"pinia": "^2.1.6",
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
<HeaderMain ref="headerMain">
|
<HeaderMain ref="headerMain">
|
||||||
<HeaderSelected :path="path.pathList" />
|
<HeaderSelected :path="path.pathList" />
|
||||||
</HeaderMain>
|
</HeaderMain>
|
||||||
<BreadCrumb :path="path.pathList" />
|
<BreadCrumb :path="path.pathList" tabindex="-1"/>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
<RouterView :path="path.pathList" />
|
<RouterView :path="path.pathList" />
|
||||||
|
@ -62,7 +62,9 @@ const headerMain = ref<typeof HeaderMain | null>(null)
|
||||||
let vert = 0
|
let vert = 0
|
||||||
let timer: any = null
|
let timer: any = null
|
||||||
const globalShortcutHandler = (event: KeyboardEvent) => {
|
const globalShortcutHandler = (event: KeyboardEvent) => {
|
||||||
const c = documentStore.fileExplorer.isCursor()
|
const fileExplorer = documentStore.fileExplorer as any
|
||||||
|
if (!fileExplorer) return
|
||||||
|
const c = fileExplorer.isCursor()
|
||||||
const keyup = event.type === 'keyup'
|
const keyup = event.type === 'keyup'
|
||||||
if (event.repeat) {
|
if (event.repeat) {
|
||||||
if (
|
if (
|
||||||
|
@ -84,7 +86,7 @@ const globalShortcutHandler = (event: KeyboardEvent) => {
|
||||||
}
|
}
|
||||||
// Select all (toggle); keydown to prevent builtin
|
// Select all (toggle); keydown to prevent builtin
|
||||||
else if (!keyup && event.key === 'a' && (event.ctrlKey || event.metaKey)) {
|
else if (!keyup && event.key === 'a' && (event.ctrlKey || event.metaKey)) {
|
||||||
documentStore.fileExplorer.toggleSelectAll()
|
fileExplorer.toggleSelectAll()
|
||||||
}
|
}
|
||||||
// Keys 1-3 to sort columns
|
// Keys 1-3 to sort columns
|
||||||
else if (
|
else if (
|
||||||
|
@ -92,16 +94,16 @@ const globalShortcutHandler = (event: KeyboardEvent) => {
|
||||||
keyup &&
|
keyup &&
|
||||||
(event.key === '1' || event.key === '2' || event.key === '3')
|
(event.key === '1' || event.key === '2' || event.key === '3')
|
||||||
) {
|
) {
|
||||||
documentStore.fileExplorer.toggleSortColumn(+event.key)
|
fileExplorer.toggleSortColumn(+event.key)
|
||||||
}
|
}
|
||||||
// Rename
|
// 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()
|
fileExplorer.cursorRename()
|
||||||
}
|
}
|
||||||
// Toggle selections on file explorer; ignore all spaces to prevent scrolling built-in hotkey
|
// Toggle selections on file explorer; ignore all spaces to prevent scrolling built-in hotkey
|
||||||
else if (c && event.code === 'Space') {
|
else if (c && event.code === 'Space') {
|
||||||
if (keyup && !event.altKey && !event.ctrlKey)
|
if (keyup && !event.altKey && !event.ctrlKey)
|
||||||
documentStore.fileExplorer.cursorSelect()
|
fileExplorer.cursorSelect()
|
||||||
} else return
|
} else return
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
if (!vert) {
|
if (!vert) {
|
||||||
|
@ -114,13 +116,13 @@ const globalShortcutHandler = (event: KeyboardEvent) => {
|
||||||
if (!timer) {
|
if (!timer) {
|
||||||
// Initial move, then t0 delay until repeats at tr intervals
|
// Initial move, then t0 delay until repeats at tr intervals
|
||||||
const select = event.shiftKey
|
const select = event.shiftKey
|
||||||
documentStore.fileExplorer.cursorMove(vert, select)
|
fileExplorer.cursorMove(vert, select)
|
||||||
const t0 = 200,
|
const t0 = 200,
|
||||||
tr = 30
|
tr = 30
|
||||||
timer = setTimeout(
|
timer = setTimeout(
|
||||||
() =>
|
() =>
|
||||||
(timer = setInterval(() => {
|
(timer = setInterval(() => {
|
||||||
documentStore.fileExplorer.cursorMove(vert, select)
|
fileExplorer.cursorMove(vert, select)
|
||||||
}, tr)),
|
}, tr)),
|
||||||
t0 - tr
|
t0 - tr
|
||||||
)
|
)
|
||||||
|
|
|
@ -8,6 +8,8 @@
|
||||||
--primary-color: #000;
|
--primary-color: #000;
|
||||||
--accent-color: #f80;
|
--accent-color: #f80;
|
||||||
--transition-time: 0.2s;
|
--transition-time: 0.2s;
|
||||||
|
/* The following are overridden by responsive layouts */
|
||||||
|
--root-font-size: 1rem;
|
||||||
--header-font-size: 1rem;
|
--header-font-size: 1rem;
|
||||||
--header-height: calc(8 * var(--header-font-size));
|
--header-height: calc(8 * var(--header-font-size));
|
||||||
}
|
}
|
||||||
|
@ -19,13 +21,13 @@
|
||||||
--header-color: #ccc;
|
--header-color: #ccc;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@media screen and (orientation: portrait) and (max-width: 600px) {
|
@media screen and (max-width: 600px) {
|
||||||
.size,
|
.size,
|
||||||
.modified {
|
.modified {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@media screen and (orientation: landscape) and (min-width: 600px) {
|
@media screen and (orientation: landscape) and (min-width: 1200px) {
|
||||||
/* Breadcrumbs and buttons side by side */
|
/* Breadcrumbs and buttons side by side */
|
||||||
header {
|
header {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -33,14 +35,14 @@
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: end;
|
align-items: end;
|
||||||
}
|
}
|
||||||
.breadcrumb {
|
header .breadcrumb {
|
||||||
font-size: 1.7em;
|
font-size: 1.7em;
|
||||||
flex-shrink: 10;
|
flex-shrink: 10;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@media screen and (min-width: 800px) and (--webkit-min-device-pixel-ratio: 2) {
|
@media screen and (min-width: 800px) and (--webkit-min-device-pixel-ratio: 2) {
|
||||||
html {
|
:root {
|
||||||
font-size: 1.5rem;
|
--root-font-size: calc(16 * 100vw / 800);
|
||||||
}
|
}
|
||||||
header .buttons:has(input[type='search']) > div {
|
header .buttons:has(input[type='search']) > div {
|
||||||
display: none;
|
display: none;
|
||||||
|
@ -49,9 +51,9 @@
|
||||||
display: inherit;
|
display: inherit;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@media screen and (min-width: 1400px) and (--webkit-min-device-pixel-ratio: 3) {
|
@media screen and (min-width: 1600px) and (--webkit-min-device-pixel-ratio: 3) {
|
||||||
html {
|
:root {
|
||||||
font-size: 2rem;
|
--root-font-size: 2rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@media screen and (max-height: 600px) {
|
@media screen and (max-height: 600px) {
|
||||||
|
@ -185,10 +187,10 @@ table {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
nav {
|
header nav.headermain {
|
||||||
/* Position so that tooltips can appear on top of other positioned elements */
|
/* Position so that tooltips can appear on top of other positioned elements */
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 10;
|
z-index: 100;
|
||||||
}
|
}
|
||||||
main {
|
main {
|
||||||
height: calc(100svh - var(--header-height));
|
height: calc(100svh - var(--header-height));
|
||||||
|
@ -196,7 +198,7 @@ main {
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
}
|
}
|
||||||
[data-tooltip]:hover:after {
|
[data-tooltip]:hover:after {
|
||||||
z-index: 1000;
|
z-index: 101;
|
||||||
content: attr(data-tooltip);
|
content: attr(data-tooltip);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
<nav
|
<nav
|
||||||
class="breadcrumb"
|
class="breadcrumb"
|
||||||
aria-label="Breadcrumb"
|
aria-label="Breadcrumb"
|
||||||
tabindex="0"
|
|
||||||
@keyup.left.stop="move(-1)"
|
@keyup.left.stop="move(-1)"
|
||||||
@keyup.right.stop="move(1)"
|
@keyup.right.stop="move(1)"
|
||||||
@focus="move(0)"
|
@focus="move(0)"
|
||||||
|
@ -42,19 +41,12 @@ const props = defineProps<{
|
||||||
|
|
||||||
const longest = ref<Array<string>>([])
|
const longest = ref<Array<string>>([])
|
||||||
|
|
||||||
watchEffect(() => {
|
|
||||||
const longcut = longest.value.slice(0, props.path.length)
|
|
||||||
const same = longcut.every((value, index) => value === props.path[index])
|
|
||||||
if (!same) longest.value = props.path
|
|
||||||
else if (props.path.length > longcut.length) {
|
|
||||||
longest.value = longcut.concat(props.path.slice(longcut.length))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const isCurrent = (index: number) => index == props.path.length ? 'location' : undefined
|
const isCurrent = (index: number) => index == props.path.length ? 'location' : undefined
|
||||||
|
|
||||||
const navigate = (index: number) => {
|
const navigate = (index: number) => {
|
||||||
links[index].focus()
|
const link = links[index]
|
||||||
|
if (!link) throw Error(`No link at index ${index} (path: ${props.path})`)
|
||||||
|
link.focus()
|
||||||
router.replace(`/${longest.value.slice(0, index).join('/')}`)
|
router.replace(`/${longest.value.slice(0, index).join('/')}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,6 +55,18 @@ const move = (dir: number) => {
|
||||||
if (index < 0 || index > longest.value.length) return
|
if (index < 0 || index > longest.value.length) return
|
||||||
navigate(index)
|
navigate(index)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
const longcut = longest.value.slice(0, props.path.length)
|
||||||
|
const same = longcut.every((value, index) => value === props.path[index])
|
||||||
|
if (!same) longest.value = props.path
|
||||||
|
else if (props.path.length > longcut.length) {
|
||||||
|
longest.value = longcut.concat(props.path.slice(longcut.length))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
watchEffect(() => {
|
||||||
|
if (links.length) navigate(props.path.length)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<table v-if="props.documents.length || editing" @blur="cursor = null">
|
<table v-if="props.documents.length || editing">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="selection">
|
<th class="selection">
|
||||||
|
@ -56,9 +56,14 @@
|
||||||
<td class="size right">{{ editing.sizedisp }}</td>
|
<td class="size right">{{ editing.sizedisp }}</td>
|
||||||
<td class="menu"></td>
|
<td class="menu"></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<template
|
||||||
|
v-for="doc of sorted(props.documents as Document[])"
|
||||||
|
:key="doc.key">
|
||||||
|
<tr v-if="doc.loc !== prevloc && ((prevloc = doc.loc) || true)">
|
||||||
|
<th colspan="5"><BreadCrumb :path="doc.loc ? doc.loc.split('/') : []" /></th>
|
||||||
|
</tr>
|
||||||
|
|
||||||
<tr
|
<tr
|
||||||
v-for="doc of sorted(props.documents as FolderDocument[])"
|
|
||||||
:key="doc.key"
|
|
||||||
:id="`file-${doc.key}`"
|
:id="`file-${doc.key}`"
|
||||||
:class="{
|
:class="{
|
||||||
file: doc.type === 'file',
|
file: doc.type === 'file',
|
||||||
|
@ -95,8 +100,11 @@
|
||||||
<a
|
<a
|
||||||
:href="url_for(doc)"
|
:href="url_for(doc)"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
@contextmenu.stop
|
@contextmenu.prevent
|
||||||
@focus.stop="cursor = doc"
|
@focus.stop="cursor = doc"
|
||||||
|
@blur="ev => { if (!editing) cursor = null }"
|
||||||
|
@keyup.left="router.back()"
|
||||||
|
@keyup.right.stop="ev => { if (doc.type === 'folder') (ev.target as HTMLElement).click() }"
|
||||||
>{{ doc.name }}</a
|
>{{ doc.name }}</a
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
|
@ -124,18 +132,24 @@
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
</template>
|
||||||
|
<tr>
|
||||||
|
<td colspan="3" class="right">{{props.documents.length}} items shown:</td>
|
||||||
|
<td class="size right">{{ formatSize(props.documents.reduce((a, b) => a + b.size, 0)) }}</td>
|
||||||
|
<td class="menu"></td>
|
||||||
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<div v-else class="empty-container">Nothing to see here</div>
|
<div v-else class="empty-container">Nothing to see here</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watchEffect } from 'vue'
|
import { ref, computed, watchEffect, onBeforeUpdate } from 'vue'
|
||||||
import { useDocumentStore } from '@/stores/documents'
|
import { useDocumentStore } from '@/stores/documents'
|
||||||
import type { Document, FolderDocument } from '@/repositories/Document'
|
import type { Document } from '@/repositories/Document'
|
||||||
import FileRenameInput from './FileRenameInput.vue'
|
import FileRenameInput from './FileRenameInput.vue'
|
||||||
import createWebSocket from '@/repositories/WS'
|
import createWebSocket from '@/repositories/WS'
|
||||||
import { formatSize, formatUnixDate } from '@/utils'
|
import { collator, formatSize, formatUnixDate } from '@/utils'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
|
@ -145,19 +159,16 @@ const props = withDefaults(
|
||||||
}>(),
|
}>(),
|
||||||
{}
|
{}
|
||||||
)
|
)
|
||||||
|
|
||||||
const documentStore = useDocumentStore()
|
const documentStore = useDocumentStore()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const linkBasePath = computed(() => props.path.join('/'))
|
const url_for = (doc: Document) => {
|
||||||
const filesBasePath = computed(() => `/files/${linkBasePath.value}`)
|
const p = doc.loc ? `${doc.loc}/${doc.name}` : doc.name
|
||||||
const url_for = (doc: FolderDocument) =>
|
return doc.type === 'folder' ? `#/${p}/` : `/files/${p}`
|
||||||
doc.type === 'folder'
|
}
|
||||||
? `#${linkBasePath.value}/${doc.name}/`
|
const cursor = ref<Document | null>(null)
|
||||||
: `${filesBasePath.value}/${doc.name}`
|
|
||||||
const cursor = ref<FolderDocument | null>(null)
|
|
||||||
// File rename
|
// File rename
|
||||||
const editing = ref<FolderDocument | null>(null)
|
const editing = ref<Document | null>(null)
|
||||||
const rename = (doc: FolderDocument, newName: string) => {
|
const rename = (doc: Document, newName: string) => {
|
||||||
const oldName = doc.name
|
const oldName = doc.name
|
||||||
const control = createWebSocket('/api/control', (ev: MessageEvent) => {
|
const control = createWebSocket('/api/control', (ev: MessageEvent) => {
|
||||||
const msg = JSON.parse(ev.data)
|
const msg = JSON.parse(ev.data)
|
||||||
|
@ -172,7 +183,7 @@ const rename = (doc: FolderDocument, newName: string) => {
|
||||||
control.send(
|
control.send(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
op: 'rename',
|
op: 'rename',
|
||||||
path: `${decodeURIComponent(linkBasePath.value)}/${oldName}`,
|
path: `${doc.loc}/${oldName}`,
|
||||||
to: newName
|
to: newName
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
@ -183,13 +194,15 @@ defineExpose({
|
||||||
newFolder() {
|
newFolder() {
|
||||||
const now = Date.now() / 1000
|
const now = Date.now() / 1000
|
||||||
editing.value = {
|
editing.value = {
|
||||||
|
loc: loc.value,
|
||||||
key: 'new',
|
key: 'new',
|
||||||
name: 'New Folder',
|
name: 'New Folder',
|
||||||
type: 'folder',
|
type: 'folder',
|
||||||
mtime: now,
|
mtime: now,
|
||||||
size: 0,
|
size: 0,
|
||||||
sizedisp: formatSize(0),
|
sizedisp: formatSize(0),
|
||||||
modified: formatUnixDate(now)
|
modified: formatUnixDate(now),
|
||||||
|
haystack: '',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
toggleSelectAll() {
|
toggleSelectAll() {
|
||||||
|
@ -218,7 +231,7 @@ defineExpose({
|
||||||
},
|
},
|
||||||
cursorMove(d: number, select = false) {
|
cursorMove(d: number, select = false) {
|
||||||
// Move cursor up or down (keyboard navigation)
|
// Move cursor up or down (keyboard navigation)
|
||||||
const documents = sorted(props.documents as FolderDocument[])
|
const documents = sorted(props.documents as Document[])
|
||||||
if (documents.length === 0) {
|
if (documents.length === 0) {
|
||||||
cursor.value = null
|
cursor.value = null
|
||||||
return
|
return
|
||||||
|
@ -245,16 +258,23 @@ defineExpose({
|
||||||
scrolltr = tr
|
scrolltr = tr
|
||||||
if (!scrolltimer) {
|
if (!scrolltimer) {
|
||||||
scrolltimer = setTimeout(() => {
|
scrolltimer = setTimeout(() => {
|
||||||
|
if (scrolltr)
|
||||||
scrolltr.scrollIntoView({ block: 'center', behavior: 'smooth' })
|
scrolltr.scrollIntoView({ block: 'center', behavior: 'smooth' })
|
||||||
scrolltimer = null
|
scrolltimer = null
|
||||||
}, 300)
|
}, 300)
|
||||||
}
|
}
|
||||||
|
if (moveto === N) focusBreadcrumb()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
const focusBreadcrumb = () => {
|
||||||
|
const el = document.querySelector('.breadcrumb') as HTMLElement | null
|
||||||
|
if (el) el.focus()
|
||||||
|
}
|
||||||
let scrolltimer: any = null
|
let scrolltimer: any = null
|
||||||
let scrolltr: any = null
|
let scrolltr: any = null
|
||||||
|
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
|
if (cursor.value && cursor.value !== editing.value) editing.value = null
|
||||||
|
if (editing.value) cursor.value = editing.value
|
||||||
if (cursor.value) {
|
if (cursor.value) {
|
||||||
const a = document.querySelector(
|
const a = document.querySelector(
|
||||||
`#file-${cursor.value.key} .name a`
|
`#file-${cursor.value.key} .name a`
|
||||||
|
@ -262,7 +282,13 @@ watchEffect(() => {
|
||||||
if (a) a.focus()
|
if (a) a.focus()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const mkdir = (doc: FolderDocument, name: string) => {
|
watchEffect(() => {
|
||||||
|
if (!props.documents.length && cursor.value) {
|
||||||
|
cursor.value = null
|
||||||
|
focusBreadcrumb()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const mkdir = (doc: Document, name: string) => {
|
||||||
const control = createWebSocket('/api/control', (ev: MessageEvent) => {
|
const control = createWebSocket('/api/control', (ev: MessageEvent) => {
|
||||||
const msg = JSON.parse(ev.data)
|
const msg = JSON.parse(ev.data)
|
||||||
if ('error' in msg) {
|
if ('error' in msg) {
|
||||||
|
@ -270,14 +296,14 @@ const mkdir = (doc: FolderDocument, name: string) => {
|
||||||
editing.value = null
|
editing.value = null
|
||||||
} else {
|
} else {
|
||||||
console.log('mkdir', msg)
|
console.log('mkdir', msg)
|
||||||
router.push(`/${linkBasePath.value}/${name}/`)
|
router.push(`/${doc.loc}/${name}/`)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
control.onopen = () => {
|
control.onopen = () => {
|
||||||
control.send(
|
control.send(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
op: 'mkdir',
|
op: 'mkdir',
|
||||||
path: `${decodeURIComponent(linkBasePath.value)}/${name}`
|
path: `${doc.loc}/${name}`
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -290,12 +316,11 @@ const toggleSort = (name: string) => {
|
||||||
}
|
}
|
||||||
const sort = ref<string>('')
|
const sort = ref<string>('')
|
||||||
const sortCompare = {
|
const sortCompare = {
|
||||||
name: (a: Document, b: Document) =>
|
name: (a: Document, b: Document) => collator.compare(a.name, b.name),
|
||||||
a.name.localeCompare(b.name, undefined, { numeric: true, sensitivity: 'base' }),
|
modified: (a: Document, b: Document) => b.mtime - a.mtime,
|
||||||
modified: (a: FolderDocument, b: FolderDocument) => b.mtime - a.mtime,
|
size: (a: Document, b: Document) => b.size - a.size
|
||||||
size: (a: FolderDocument, b: FolderDocument) => b.size - a.size
|
|
||||||
}
|
}
|
||||||
const sorted = (documents: FolderDocument[]) => {
|
const sorted = (documents: Document[]) => {
|
||||||
const cmp = sortCompare[sort.value as keyof typeof sortCompare]
|
const cmp = sortCompare[sort.value as keyof typeof sortCompare]
|
||||||
const sorted = [...documents]
|
const sorted = [...documents]
|
||||||
if (cmp) sorted.sort(cmp)
|
if (cmp) sorted.sort(cmp)
|
||||||
|
@ -330,10 +355,11 @@ const allSelected = computed({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
watchEffect(() => {
|
|
||||||
if (cursor.value && cursor.value !== editing.value) editing.value = null
|
const loc = computed(() => props.path.join('/'))
|
||||||
if (editing.value) cursor.value = editing.value
|
let prevloc = ''
|
||||||
})
|
onBeforeUpdate(() => { prevloc = loc.value })
|
||||||
|
|
||||||
const contextMenu = (ev: Event, doc: Document) => {
|
const contextMenu = (ev: Event, doc: Document) => {
|
||||||
cursor.value = doc
|
cursor.value = doc
|
||||||
console.log('Context menu', ev, doc)
|
console.log('Context menu', ev, doc)
|
||||||
|
@ -370,10 +396,10 @@ table .selection {
|
||||||
text-overflow: clip;
|
text-overflow: clip;
|
||||||
}
|
}
|
||||||
table .modified {
|
table .modified {
|
||||||
width: 8rem;
|
width: 8em;
|
||||||
}
|
}
|
||||||
table .size {
|
table .size {
|
||||||
width: 4rem;
|
width: 5em;
|
||||||
}
|
}
|
||||||
table .menu {
|
table .menu {
|
||||||
width: 1rem;
|
width: 1rem;
|
||||||
|
@ -473,4 +499,7 @@ tbody .selection input {
|
||||||
font-size: 3rem;
|
font-size: 3rem;
|
||||||
color: var(--accent-color);
|
color: var(--accent-color);
|
||||||
}
|
}
|
||||||
|
.loc {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { FolderDocument } from '@/repositories/Document'
|
import type { Document } from '@/repositories/Document'
|
||||||
import { ref, onMounted, nextTick } from 'vue'
|
import { ref, onMounted, nextTick } from 'vue'
|
||||||
|
|
||||||
const input = ref<HTMLInputElement | null>(null)
|
const input = ref<HTMLInputElement | null>(null)
|
||||||
|
@ -28,8 +28,8 @@ onMounted(() => {
|
||||||
})
|
})
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
doc: FolderDocument
|
doc: Document
|
||||||
rename: (doc: FolderDocument, newName: string) => void
|
rename: (doc: Document, newName: string) => void
|
||||||
exit: () => void
|
exit: () => void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
|
|
@ -12,22 +12,17 @@ const toggleSearchInput = () => {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
const input = search.value
|
const input = search.value
|
||||||
if (input) input.focus()
|
if (input) input.focus()
|
||||||
else if (searchButton.value) searchButton.value.blur()
|
//else if (searchButton.value) document.querySelector('.breadcrumb')!.focus()
|
||||||
executeSearch()
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const executeSearch = () => {
|
|
||||||
documentStore.setFilter(search.value?.value ?? '')
|
|
||||||
}
|
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
toggleSearchInput
|
toggleSearchInput
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<nav>
|
<nav class="headermain">
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<UploadButton />
|
<UploadButton />
|
||||||
<SvgButton
|
<SvgButton
|
||||||
|
@ -41,9 +36,11 @@ defineExpose({
|
||||||
<input
|
<input
|
||||||
ref="search"
|
ref="search"
|
||||||
type="search"
|
type="search"
|
||||||
|
v-model="documentStore.search"
|
||||||
|
placeholder="Search words"
|
||||||
class="margin-input"
|
class="margin-input"
|
||||||
|
@blur="() => { if (documentStore.search === '') toggleSearchInput() }"
|
||||||
@keyup.esc="toggleSearchInput"
|
@keyup.esc="toggleSearchInput"
|
||||||
@input="executeSearch"
|
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<SvgButton ref="searchButton" name="find" @click="toggleSearchInput" />
|
<SvgButton ref="searchButton" name="find" @click="toggleSearchInput" />
|
||||||
|
|
|
@ -1,24 +1,21 @@
|
||||||
import type { DocumentStore } from '@/stores/documents'
|
|
||||||
import { useDocumentStore } from '@/stores/documents'
|
import { useDocumentStore } from '@/stores/documents'
|
||||||
import createWebSocket from './WS'
|
import createWebSocket from './WS'
|
||||||
|
|
||||||
export type FUID = string
|
export type FUID = string
|
||||||
|
|
||||||
type BaseDocument = {
|
export type Document = {
|
||||||
|
loc: string
|
||||||
name: string
|
name: string
|
||||||
key: FUID
|
key: FUID
|
||||||
}
|
|
||||||
|
|
||||||
export type FolderDocument = BaseDocument & {
|
|
||||||
type: 'folder' | 'file'
|
type: 'folder' | 'file'
|
||||||
size: number
|
size: number
|
||||||
sizedisp: string
|
sizedisp: string
|
||||||
mtime: number
|
mtime: number
|
||||||
modified: string
|
modified: string
|
||||||
|
haystack: string
|
||||||
|
dir?: DirList
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Document = FolderDocument
|
|
||||||
|
|
||||||
export type errorEvent = {
|
export type errorEvent = {
|
||||||
error: {
|
error: {
|
||||||
code: number
|
code: number
|
||||||
|
@ -70,7 +67,7 @@ export const url_document_upload_ws = '/api/upload'
|
||||||
export const url_document_get = '/files'
|
export const url_document_get = '/files'
|
||||||
|
|
||||||
export class DocumentHandler {
|
export class DocumentHandler {
|
||||||
constructor(private store: DocumentStore = useDocumentStore()) {
|
constructor(private store = useDocumentStore()) {
|
||||||
this.handleWebSocketMessage = this.handleWebSocketMessage.bind(this)
|
this.handleWebSocketMessage = this.handleWebSocketMessage.bind(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,9 +107,9 @@ export class DocumentHandler {
|
||||||
|
|
||||||
private handleRootMessage({ root }: { root: DirEntry }) {
|
private handleRootMessage({ root }: { root: DirEntry }) {
|
||||||
console.log('Watch root', root)
|
console.log('Watch root', root)
|
||||||
if (this.store && this.store.root) {
|
if (this.store) {
|
||||||
this.store.user.isLoggedIn = true
|
this.store.user.isLoggedIn = true
|
||||||
this.store.root = root
|
this.store.updateRoot(root)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private handleUpdateMessage(updateData: { update: UpdateEntry[] }) {
|
private handleUpdateMessage(updateData: { update: UpdateEntry[] }) {
|
||||||
|
@ -132,6 +129,7 @@ export class DocumentHandler {
|
||||||
if (elem.mtime !== undefined) node.mtime = elem.mtime
|
if (elem.mtime !== undefined) node.mtime = elem.mtime
|
||||||
if (elem.dir !== undefined) node.dir = elem.dir
|
if (elem.dir !== undefined) node.dir = elem.dir
|
||||||
}
|
}
|
||||||
|
this.store.updateRoot()
|
||||||
}
|
}
|
||||||
private handleError(msg: errorEvent) {
|
private handleError(msg: errorEvent) {
|
||||||
if (msg.error.code === 401) {
|
if (msg.error.code === 401) {
|
||||||
|
@ -143,7 +141,7 @@ export class DocumentHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DocumentUploadHandler {
|
export class DocumentUploadHandler {
|
||||||
constructor(private store: DocumentStore = useDocumentStore()) {
|
constructor(private store = useDocumentStore()) {
|
||||||
this.handleWebSocketMessage = this.handleWebSocketMessage.bind(this)
|
this.handleWebSocketMessage = this.handleWebSocketMessage.bind(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,10 +6,9 @@ import type {
|
||||||
DirList,
|
DirList,
|
||||||
SelectedItems
|
SelectedItems
|
||||||
} from '@/repositories/Document'
|
} from '@/repositories/Document'
|
||||||
import { formatSize, formatUnixDate } from '@/utils'
|
import { formatSize, formatUnixDate, haystackFormat } from '@/utils'
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
// @ts-ignore
|
import { collator } from '@/utils'
|
||||||
import { localeIncludes } from 'locale-includes'
|
|
||||||
|
|
||||||
type FileData = { id: string; mtime: number; size: number; dir: DirectoryData }
|
type FileData = { id: string; mtime: number; size: number; dir: DirectoryData }
|
||||||
type DirectoryData = {
|
type DirectoryData = {
|
||||||
|
@ -22,24 +21,12 @@ type User = {
|
||||||
isLoggedIn: boolean
|
isLoggedIn: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DocumentStore = {
|
|
||||||
root: DirEntry
|
|
||||||
document: Document[]
|
|
||||||
selected: Set<FUID>
|
|
||||||
uploadingDocuments: Array<{ key: number; name: string; progress: number }>
|
|
||||||
uploadCount: number
|
|
||||||
wsWatch: WebSocket | undefined
|
|
||||||
wsUpload: WebSocket | undefined
|
|
||||||
fileExplorer: any
|
|
||||||
user: User
|
|
||||||
error: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useDocumentStore = defineStore({
|
export const useDocumentStore = defineStore({
|
||||||
id: 'documents',
|
id: 'documents',
|
||||||
state: (): DocumentStore => ({
|
state: () => ({
|
||||||
root: {} as DirEntry,
|
root: {} as DirEntry,
|
||||||
document: [] as Document[],
|
document: [] as Document[],
|
||||||
|
search: "" as string,
|
||||||
selected: new Set<FUID>(),
|
selected: new Set<FUID>(),
|
||||||
uploadingDocuments: [],
|
uploadingDocuments: [],
|
||||||
uploadCount: 0 as number,
|
uploadCount: 0 as number,
|
||||||
|
@ -56,89 +43,36 @@ export const useDocumentStore = defineStore({
|
||||||
}),
|
}),
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
updateTable(matched: DirList) {
|
updateRoot(root: DirEntry | null = null) {
|
||||||
// Transform data
|
root ??= this.root
|
||||||
const dataMapped = []
|
// Transform tree data to flat documents array
|
||||||
for (const [name, attr] of Object.entries(matched)) {
|
let loc = ""
|
||||||
const { key, size, mtime } = attr
|
const mapper = ([name, attr]: [string, FileEntry | DirEntry]) => ({
|
||||||
const element: Document = {
|
loc,
|
||||||
name,
|
name,
|
||||||
key,
|
type: 'dir' in attr ? 'folder' : 'file' as 'folder' | 'file',
|
||||||
size,
|
...attr,
|
||||||
sizedisp: formatSize(size),
|
sizedisp: formatSize(attr.size),
|
||||||
mtime,
|
modified: formatUnixDate(attr.mtime),
|
||||||
modified: formatUnixDate(mtime),
|
haystack: haystackFormat(name),
|
||||||
type: 'dir' in attr ? 'folder' : 'file'
|
})
|
||||||
|
const queue = [...Object.entries(root.dir ?? {}).map(mapper)]
|
||||||
|
const docs = []
|
||||||
|
for (let doc; (doc = queue.shift()) !== undefined;) {
|
||||||
|
docs.push(doc)
|
||||||
|
if ("dir" in doc) {
|
||||||
|
loc = doc.loc ? `${doc.loc}/${doc.name}` : doc.name
|
||||||
|
queue.push(...Object.entries(doc.dir).map(mapper))
|
||||||
}
|
}
|
||||||
dataMapped.push(element)
|
|
||||||
}
|
}
|
||||||
// Pre sort directory entries folders first then files, names in natural ordering
|
// Pre sort directory entries folders first then files, names in natural ordering
|
||||||
dataMapped.sort((a, b) =>
|
docs.sort((a, b) =>
|
||||||
a.type === b.type
|
// @ts-ignore
|
||||||
? a.name.localeCompare(b.name, undefined, {
|
(a.type === "file") - (b.type === "file") ||
|
||||||
numeric: true,
|
collator.compare(a.name, b.name)
|
||||||
sensitivity: 'base'
|
|
||||||
})
|
|
||||||
: a.type === 'folder'
|
|
||||||
? -1
|
|
||||||
: 1
|
|
||||||
)
|
)
|
||||||
this.document = dataMapped
|
this.root = root
|
||||||
},
|
this.document = docs
|
||||||
setFilter(filter: string) {
|
|
||||||
if (filter === '') return this.updateTable({})
|
|
||||||
function traverseDir(data: DirEntry | FileEntry, path: string) {
|
|
||||||
if (!('dir' in data)) return
|
|
||||||
for (const [name, attr] of Object.entries(data.dir)) {
|
|
||||||
const fullname = `${path}/${name}`
|
|
||||||
if (
|
|
||||||
localeIncludes(name, filter, {
|
|
||||||
usage: 'search',
|
|
||||||
numeric: true,
|
|
||||||
sensitivity: 'base'
|
|
||||||
})
|
|
||||||
) {
|
|
||||||
matched[fullname.slice(1)] = attr // No initial slash on name
|
|
||||||
if (!--count) throw Error('Too many matches')
|
|
||||||
}
|
|
||||||
traverseDir(attr, fullname)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let count = 100
|
|
||||||
const matched: any = {}
|
|
||||||
try {
|
|
||||||
traverseDir(this.root, '')
|
|
||||||
} catch (error: any) {
|
|
||||||
if (error.message !== 'Too many matches') throw error
|
|
||||||
}
|
|
||||||
this.updateTable(matched)
|
|
||||||
},
|
|
||||||
setActualDocument(location: string) {
|
|
||||||
location = decodeURIComponent(location)
|
|
||||||
let data: FileEntry | DirEntry = this.root
|
|
||||||
const actualDirArr = []
|
|
||||||
try {
|
|
||||||
// Navigate to target folder
|
|
||||||
for (const dirname of location.split('/').slice(1)) {
|
|
||||||
if (!dirname) continue
|
|
||||||
if (!('dir' in data)) throw Error('Target folder not available')
|
|
||||||
actualDirArr.push(dirname)
|
|
||||||
data = data.dir[dirname]
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(
|
|
||||||
'Cannot show requested folder',
|
|
||||||
location,
|
|
||||||
actualDirArr.join('/'),
|
|
||||||
error
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (!('dir' in data)) {
|
|
||||||
// Target folder not available
|
|
||||||
this.document = []
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.updateTable(data.dir)
|
|
||||||
},
|
},
|
||||||
updateUploadingDocuments(key: number, progress: number) {
|
updateUploadingDocuments(key: number, progress: number) {
|
||||||
for (const d of this.uploadingDocuments) {
|
for (const d of this.uploadingDocuments) {
|
||||||
|
@ -171,12 +105,19 @@ export const useDocumentStore = defineStore({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
mainDocument(): Document[] {
|
|
||||||
return this.document
|
|
||||||
},
|
|
||||||
isUserLogged(): boolean {
|
isUserLogged(): boolean {
|
||||||
return this.user.isLoggedIn
|
return this.user.isLoggedIn
|
||||||
},
|
},
|
||||||
|
recentDocuments(): Document[] {
|
||||||
|
const ret = [...this.document]
|
||||||
|
ret.sort((a, b) => b.mtime - a.mtime)
|
||||||
|
return ret
|
||||||
|
},
|
||||||
|
largeDocuments(): Document[] {
|
||||||
|
const ret = [...this.document]
|
||||||
|
ret.sort((a, b) => b.size - a.size)
|
||||||
|
return ret
|
||||||
|
},
|
||||||
selectedFiles(): SelectedItems {
|
selectedFiles(): SelectedItems {
|
||||||
function traverseDir(data: DirEntry | FileEntry, path: string, relpath: string) {
|
function traverseDir(data: DirEntry | FileEntry, path: string, relpath: string) {
|
||||||
if (!('dir' in data)) return
|
if (!('dir' in data)) return
|
||||||
|
|
|
@ -57,44 +57,40 @@ export function getFileExtension(filename: string) {
|
||||||
return '' // No hay extensión
|
return '' // No hay extensión
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export function getFileType(extension: string): string {
|
interface FileTypes {
|
||||||
const videoExtensions = ['mp4', 'avi', 'mkv', 'mov']
|
[key: string]: string[]
|
||||||
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif']
|
|
||||||
const pdfExtensions = ['pdf']
|
|
||||||
|
|
||||||
if (videoExtensions.includes(extension)) {
|
|
||||||
return 'video'
|
|
||||||
} else if (imageExtensions.includes(extension)) {
|
|
||||||
return 'image'
|
|
||||||
} else if (pdfExtensions.includes(extension)) {
|
|
||||||
return 'pdf'
|
|
||||||
} else {
|
|
||||||
return 'unknown'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const collator = new Intl.Collator('en', { sensitivity: 'base', numeric: true, usage: 'search' })
|
const filetypes: FileTypes = {
|
||||||
|
video: ['avi', 'mkv', 'mov', 'mp4', 'webm'],
|
||||||
|
image: ['avif', 'gif', 'jpg', 'jpeg', 'png', 'webp', 'svg'],
|
||||||
|
pdf: ['pdf'],
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFileType(name: string): string {
|
||||||
|
const ext = name.split('.').pop()?.toLowerCase()
|
||||||
|
if (!ext || ext.length === name.length) return 'unknown'
|
||||||
|
return Object.keys(filetypes).find(type => filetypes[type].includes(ext)) || 'unknown'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prebuilt for fast & consistent sorting
|
||||||
|
export const collator = new Intl.Collator('en', { sensitivity: 'base', numeric: true, usage: 'search' })
|
||||||
|
|
||||||
|
// Preformat document names for faster search
|
||||||
export function haystackFormat(str: string) {
|
export function haystackFormat(str: string) {
|
||||||
const based = str.normalize('NFKD').replace(/[\u0300-\u036f]/g, '').toLowerCase()
|
const based = str.normalize('NFKD').replace(/[\u0300-\u036f]/g, '').toLowerCase()
|
||||||
return '^' + based + '$'
|
return '^' + based + '$'
|
||||||
}
|
}
|
||||||
|
|
||||||
export function localeIncludes(haystack: string, based: string, words: string[]) {
|
|
||||||
|
// Preformat search string for faster search
|
||||||
|
export function needleFormat(query: string) {
|
||||||
|
const based = query.normalize('NFKD').replace(/[\u0300-\u036f]/g, '').toLowerCase()
|
||||||
|
return {based, words: based.split(/\W+/)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test if haystack includes needle
|
||||||
|
export function localeIncludes(haystack: string, filter: { based: string, words: string[] }) {
|
||||||
|
const {based, words} = filter
|
||||||
return haystack.includes(based) || words && words.every(word => haystack.includes(word))
|
return haystack.includes(based) || words && words.every(word => haystack.includes(word))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildCorpus(data: any[]) {
|
|
||||||
return data.map(item => [haystackFormat(item.name), item])
|
|
||||||
}
|
|
||||||
|
|
||||||
export function search(corpus: [string, any][], search: string) {
|
|
||||||
const based = search.normalize('NFKD').replace(/[\u0300-\u036f]/g, '').toLowerCase()
|
|
||||||
const words = based.split(/\W+/)
|
|
||||||
const ret = []
|
|
||||||
for (const [haystack, item] of corpus) {
|
|
||||||
if (localeIncludes(haystack, based, words))
|
|
||||||
ret.push(item)
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
|
@ -3,26 +3,56 @@
|
||||||
ref="fileExplorer"
|
ref="fileExplorer"
|
||||||
:key="Router.currentRoute.value.path"
|
:key="Router.currentRoute.value.path"
|
||||||
:path="props.path"
|
:path="props.path"
|
||||||
:documents="documentStore.mainDocument"
|
:documents="documents"
|
||||||
|
v-if="props.path"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { watchEffect, ref } from 'vue'
|
import { watchEffect, ref, computed } from 'vue'
|
||||||
import { useDocumentStore } from '@/stores/documents'
|
import { useDocumentStore } from '@/stores/documents'
|
||||||
import Router from '@/router/index'
|
import Router from '@/router/index'
|
||||||
|
import { needleFormat, localeIncludes, collator } from '@/utils';
|
||||||
|
|
||||||
const documentStore = useDocumentStore()
|
const documentStore = useDocumentStore()
|
||||||
const fileExplorer = ref()
|
const fileExplorer = ref()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
path: Array<string>
|
path: Array<string>
|
||||||
})
|
})
|
||||||
|
const documents = computed(() => {
|
||||||
|
if (!props.path) return []
|
||||||
|
const loc = props.path.join('/')
|
||||||
|
// List the current location
|
||||||
|
if (!documentStore.search) return documentStore.document.filter(doc => doc.loc === loc)
|
||||||
|
// Find up to 100 newest documents that match the search
|
||||||
|
const search = documentStore.search
|
||||||
|
const needle = needleFormat(search)
|
||||||
|
let limit = 100
|
||||||
|
let docs = []
|
||||||
|
for (const doc of documentStore.recentDocuments) {
|
||||||
|
if (localeIncludes(doc.haystack, needle)) {
|
||||||
|
docs.push(doc)
|
||||||
|
if (--limit === 0) break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Organize by folder, by relevance
|
||||||
|
const locsub = loc + '/'
|
||||||
|
docs.sort((a, b) => (
|
||||||
|
// @ts-ignore
|
||||||
|
(b.loc === loc) - (a.loc === loc) ||
|
||||||
|
// @ts-ignore
|
||||||
|
(b.loc.slice(0, locsub.length) === locsub) - (a.loc.slice(0, locsub.length) === locsub) ||
|
||||||
|
collator.compare(a.loc, b.loc) ||
|
||||||
|
// @ts-ignore
|
||||||
|
(a.type === 'file') - (b.type === 'file') ||
|
||||||
|
// @ts-ignore
|
||||||
|
b.name.includes(search) - a.name.includes(search) ||
|
||||||
|
collator.compare(a.name, b.name)
|
||||||
|
))
|
||||||
|
return docs
|
||||||
|
})
|
||||||
|
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
documentStore.fileExplorer = fileExplorer.value
|
documentStore.fileExplorer = fileExplorer.value
|
||||||
})
|
})
|
||||||
watchEffect(async () => {
|
|
||||||
const path = new String(Router.currentRoute.value.path) as string
|
|
||||||
documentStore.setActualDocument(path.toString())
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user