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 @@
"
/>
+ {{ doc.loc.join('/') + '/'}}
{
}
const sort = ref('')
const sortCompare = {
- name: (a: Document, b: Document) =>
- a.name.localeCompare(b.name, undefined, { numeric: true, sensitivity: 'base' }),
+ name: (a: Document, b: Document) => collator.compare(a.name, b.name),
modified: (a: FolderDocument, b: FolderDocument) => b.mtime - a.mtime,
size: (a: FolderDocument, b: FolderDocument) => b.size - a.size
}
diff --git a/cista-front/src/components/HeaderMain.vue b/cista-front/src/components/HeaderMain.vue
index 02e7754..1461b69 100644
--- a/cista-front/src/components/HeaderMain.vue
+++ b/cista-front/src/components/HeaderMain.vue
@@ -41,9 +41,10 @@ defineExpose({
{ if (documentStore.search === '') toggleSearchInput() }"
@keyup.esc="toggleSearchInput"
- @input="executeSearch"
/>
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())
-})