Frontend created and rewritten a few times, with some backend fixes #1
|
@ -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",
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@media screen and (orientation: landscape) and (min-width: 800px) {
|
@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;
|
||||||
|
|
|
@ -92,6 +92,7 @@
|
||||||
"
|
"
|
||||||
/></template>
|
/></template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
|
<span class="loc" v-if="doc.loc.join('/') !== props.path.join('/')">{{ doc.loc.join('/') + '/'}}</span>
|
||||||
<a
|
<a
|
||||||
:href="url_for(doc)"
|
:href="url_for(doc)"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
|
@ -136,7 +137,7 @@ import { useDocumentStore } from '@/stores/documents'
|
||||||
import type { Document, FolderDocument } from '@/repositories/Document'
|
import type { Document, FolderDocument } 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(
|
||||||
|
@ -292,8 +293,7 @@ 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: FolderDocument, b: FolderDocument) => b.mtime - a.mtime,
|
modified: (a: FolderDocument, b: FolderDocument) => b.mtime - a.mtime,
|
||||||
size: (a: FolderDocument, b: FolderDocument) => b.size - a.size
|
size: (a: FolderDocument, b: FolderDocument) => b.size - a.size
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,9 +41,10 @@ defineExpose({
|
||||||
<input
|
<input
|
||||||
ref="search"
|
ref="search"
|
||||||
type="search"
|
type="search"
|
||||||
|
v-model="documentStore.search"
|
||||||
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" />
|
||||||
|
|
|
@ -4,21 +4,19 @@ 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
|
||||||
|
@ -110,9 +108,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 +130,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) {
|
||||||
|
|
|
@ -6,10 +6,9 @@ import type {
|
||||||
DirList,
|
DirList,
|
||||||
SelectedItems
|
SelectedItems
|
||||||
} from '@/repositories/Document'
|
} from '@/repositories/Document'
|
||||||
import { formatSize, formatUnixDate } from '@/utils'
|
import { needleFormat, formatSize, formatUnixDate, haystackFormat, localeIncludes } 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 = {
|
||||||
|
@ -25,6 +24,7 @@ type User = {
|
||||||
export type DocumentStore = {
|
export type DocumentStore = {
|
||||||
root: DirEntry
|
root: DirEntry
|
||||||
document: Document[]
|
document: Document[]
|
||||||
|
search: string
|
||||||
selected: Set<FUID>
|
selected: Set<FUID>
|
||||||
uploadingDocuments: Array<{ key: number; name: string; progress: number }>
|
uploadingDocuments: Array<{ key: number; name: string; progress: number }>
|
||||||
uploadCount: number
|
uploadCount: number
|
||||||
|
@ -40,6 +40,7 @@ export const useDocumentStore = defineStore({
|
||||||
state: (): DocumentStore => ({
|
state: (): DocumentStore => ({
|
||||||
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 +57,46 @@ 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 = [] as Array<string>
|
||||||
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.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) {
|
find(query: string): Document[] {
|
||||||
if (filter === '') return this.updateTable({})
|
const needle = needleFormat(query)
|
||||||
function traverseDir(data: DirEntry | FileEntry, path: string) {
|
return this.document.filter(doc => localeIncludes(doc.haystack, needle))
|
||||||
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) {
|
directory(loc: string[]) {
|
||||||
location = decodeURIComponent(location)
|
const ret = this.document.filter(
|
||||||
let data: FileEntry | DirEntry = this.root
|
doc => doc.loc.length === loc.length && doc.loc.every((e, i) => e === loc[i])
|
||||||
const actualDirArr = []
|
)
|
||||||
try {
|
return ret
|
||||||
// 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) {
|
||||||
|
|
|
@ -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,7 +3,12 @@
|
||||||
ref="fileExplorer"
|
ref="fileExplorer"
|
||||||
:key="Router.currentRoute.value.path"
|
:key="Router.currentRoute.value.path"
|
||||||
:path="props.path"
|
:path="props.path"
|
||||||
:documents="documentStore.mainDocument"
|
:documents="
|
||||||
|
documentStore.search ?
|
||||||
|
documentStore.find(documentStore.search) :
|
||||||
|
documentStore.directory(props.path)
|
||||||
|
"
|
||||||
|
v-if="props.path"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -14,15 +19,10 @@ import Router from '@/router/index'
|
||||||
|
|
||||||
const documentStore = useDocumentStore()
|
const documentStore = useDocumentStore()
|
||||||
const fileExplorer = ref()
|
const fileExplorer = ref()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
path: Array<string>
|
path: Array<string>
|
||||||
})
|
})
|
||||||
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