diff --git a/cista-front/package.json b/cista-front/package.json index 3d4afba..3d14f6e 100644 --- a/cista-front/package.json +++ b/cista-front/package.json @@ -15,7 +15,6 @@ "dependencies": { "@vueuse/core": "^10.4.1", "esbuild": "^0.19.5", - "locale-includes": "^1.0.5", "lodash": "^4.17.21", "lodash-es": "^4.17.21", "pinia": "^2.1.6", diff --git a/cista-front/src/assets/main.css b/cista-front/src/assets/main.css index c8add70..2d1719e 100644 --- a/cista-front/src/assets/main.css +++ b/cista-front/src/assets/main.css @@ -27,7 +27,7 @@ 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 */ header { display: flex; diff --git a/cista-front/src/components/FileExplorer.vue b/cista-front/src/components/FileExplorer.vue index 1c865a2..843b1e3 100644 --- a/cista-front/src/components/FileExplorer.vue +++ b/cista-front/src/components/FileExplorer.vue @@ -92,6 +92,7 @@ " /> diff --git a/cista-front/src/repositories/Document.ts b/cista-front/src/repositories/Document.ts index 461ca4b..83b2770 100644 --- a/cista-front/src/repositories/Document.ts +++ b/cista-front/src/repositories/Document.ts @@ -4,21 +4,19 @@ import createWebSocket from './WS' export type FUID = string -type BaseDocument = { +export type Document = { + loc: string[] name: string key: FUID -} - -export type FolderDocument = BaseDocument & { type: 'folder' | 'file' size: number sizedisp: string mtime: number modified: string + haystack: string + dir?: DirList } -export type Document = FolderDocument - export type errorEvent = { error: { code: number @@ -110,9 +108,9 @@ export class DocumentHandler { private handleRootMessage({ root }: { root: DirEntry }) { console.log('Watch root', root) - if (this.store && this.store.root) { + if (this.store) { this.store.user.isLoggedIn = true - this.store.root = root + this.store.updateRoot(root) } } private handleUpdateMessage(updateData: { update: UpdateEntry[] }) { @@ -132,6 +130,7 @@ export class DocumentHandler { if (elem.mtime !== undefined) node.mtime = elem.mtime if (elem.dir !== undefined) node.dir = elem.dir } + this.store.updateRoot() } private handleError(msg: errorEvent) { if (msg.error.code === 401) { diff --git a/cista-front/src/stores/documents.ts b/cista-front/src/stores/documents.ts index 2c65231..dda72af 100644 --- a/cista-front/src/stores/documents.ts +++ b/cista-front/src/stores/documents.ts @@ -6,10 +6,9 @@ import type { DirList, SelectedItems } from '@/repositories/Document' -import { formatSize, formatUnixDate } from '@/utils' +import { needleFormat, formatSize, formatUnixDate, haystackFormat, localeIncludes } from '@/utils' import { defineStore } from 'pinia' -// @ts-ignore -import { localeIncludes } from 'locale-includes' +import { collator } from '@/utils' type FileData = { id: string; mtime: number; size: number; dir: DirectoryData } type DirectoryData = { @@ -25,6 +24,7 @@ type User = { export type DocumentStore = { root: DirEntry document: Document[] + search: string selected: Set uploadingDocuments: Array<{ key: number; name: string; progress: number }> uploadCount: number @@ -40,6 +40,7 @@ export const useDocumentStore = defineStore({ state: (): DocumentStore => ({ root: {} as DirEntry, document: [] as Document[], + search: "" as string, selected: new Set(), uploadingDocuments: [], uploadCount: 0 as number, @@ -56,89 +57,46 @@ export const useDocumentStore = defineStore({ }), actions: { - updateTable(matched: DirList) { - // Transform data - const dataMapped = [] - for (const [name, attr] of Object.entries(matched)) { - const { key, size, mtime } = attr - const element: Document = { - name, - key, - size, - sizedisp: formatSize(size), - mtime, - modified: formatUnixDate(mtime), - type: 'dir' in attr ? 'folder' : 'file' + updateRoot(root: DirEntry | null = null) { + root ??= this.root + // Transform tree data to flat documents array + let loc = [] as Array + const mapper = ([name, attr]: [string, FileEntry | DirEntry]) => ({ + loc, + name, + type: 'dir' in attr ? 'folder' : 'file' as 'folder' | 'file', + ...attr, + sizedisp: formatSize(attr.size), + modified: formatUnixDate(attr.mtime), + haystack: haystackFormat(name), + }) + 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 - dataMapped.sort((a, b) => - a.type === b.type - ? a.name.localeCompare(b.name, undefined, { - numeric: true, - sensitivity: 'base' - }) - : a.type === 'folder' - ? -1 - : 1 + docs.sort((a, b) => + // @ts-ignore + (a.type === "file") - (b.type === "file") || + collator.compare(a.name, b.name) ) - 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) + find(query: string): Document[] { + const needle = needleFormat(query) + return this.document.filter(doc => localeIncludes(doc.haystack, needle)) }, - 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) + directory(loc: string[]) { + const ret = this.document.filter( + doc => doc.loc.length === loc.length && doc.loc.every((e, i) => e === loc[i]) + ) + return ret }, updateUploadingDocuments(key: number, progress: number) { for (const d of this.uploadingDocuments) { diff --git a/cista-front/src/utils/index.ts b/cista-front/src/utils/index.ts index 6954948..e8cf947 100644 --- a/cista-front/src/utils/index.ts +++ b/cista-front/src/utils/index.ts @@ -57,44 +57,40 @@ export function getFileExtension(filename: string) { return '' // No hay extensión } } -export function getFileType(extension: string): string { - const videoExtensions = ['mp4', 'avi', 'mkv', 'mov'] - 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' - } +interface FileTypes { + [key: string]: string[] } -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) { const based = str.normalize('NFKD').replace(/[\u0300-\u036f]/g, '').toLowerCase() 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)) } - -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 -} diff --git a/cista-front/src/views/ExplorerView.vue b/cista-front/src/views/ExplorerView.vue index 1d3812c..1aca104 100644 --- a/cista-front/src/views/ExplorerView.vue +++ b/cista-front/src/views/ExplorerView.vue @@ -3,7 +3,12 @@ ref="fileExplorer" :key="Router.currentRoute.value.path" :path="props.path" - :documents="documentStore.mainDocument" + :documents=" + documentStore.search ? + documentStore.find(documentStore.search) : + documentStore.directory(props.path) + " + v-if="props.path" /> @@ -14,15 +19,10 @@ import Router from '@/router/index' const documentStore = useDocumentStore() const fileExplorer = ref() - const props = defineProps({ path: Array }) watchEffect(() => { documentStore.fileExplorer = fileExplorer.value }) -watchEffect(async () => { - const path = new String(Router.currentRoute.value.path) as string - documentStore.setActualDocument(path.toString()) -})