More efficient flat file list format and various UX improvements #3
|
@ -1,13 +1,13 @@
|
||||||
<template>
|
<template>
|
||||||
<LoginModal />
|
<LoginModal />
|
||||||
<header>
|
<header>
|
||||||
<HeaderMain ref="headerMain" :path="path.pathList">
|
<HeaderMain ref="headerMain" :path="path.pathList" :query="path.query">
|
||||||
<HeaderSelected :path="path.pathList" />
|
<HeaderSelected :path="path.pathList" />
|
||||||
</HeaderMain>
|
</HeaderMain>
|
||||||
<BreadCrumb :path="path.pathList" tabindex="-1"/>
|
<BreadCrumb :path="path.pathList" tabindex="-1"/>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
<RouterView :path="path.pathList" />
|
<RouterView :path="path.pathList" :query="path.query" />
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ import { RouterView } from 'vue-router'
|
||||||
import type { ComputedRef } from 'vue'
|
import type { ComputedRef } from 'vue'
|
||||||
import type HeaderMain from '@/components/HeaderMain.vue'
|
import type HeaderMain from '@/components/HeaderMain.vue'
|
||||||
import { onMounted, onUnmounted, ref, watchEffect } from 'vue'
|
import { onMounted, onUnmounted, ref, watchEffect } from 'vue'
|
||||||
import { watchConnect, watchDisconnect } from '@/repositories/WS'
|
import { loadSession, watchConnect, watchDisconnect } from '@/repositories/WS'
|
||||||
import { useDocumentStore } from '@/stores/documents'
|
import { useDocumentStore } from '@/stores/documents'
|
||||||
|
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
@ -25,19 +25,23 @@ import Router from '@/router/index'
|
||||||
interface Path {
|
interface Path {
|
||||||
path: string
|
path: string
|
||||||
pathList: string[]
|
pathList: string[]
|
||||||
|
query: string
|
||||||
}
|
}
|
||||||
const documentStore = useDocumentStore()
|
const documentStore = useDocumentStore()
|
||||||
const path: ComputedRef<Path> = computed(() => {
|
const path: ComputedRef<Path> = computed(() => {
|
||||||
const p = decodeURIComponent(Router.currentRoute.value.path)
|
const p = decodeURIComponent(Router.currentRoute.value.path).split('//')
|
||||||
const pathList = p.split('/').filter(value => value !== '')
|
const pathList = p[0].split('/').filter(value => value !== '')
|
||||||
|
const query = p.slice(1).join('//')
|
||||||
return {
|
return {
|
||||||
path: p,
|
path: p[0],
|
||||||
pathList
|
pathList,
|
||||||
|
query
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
document.title = path.value.path.replace(/\/$/, '').split('/').pop() || documentStore.server.name || 'Cista Storage'
|
document.title = path.value.path.replace(/\/$/, '').split('/').pop() || documentStore.server.name || 'Cista Storage'
|
||||||
})
|
})
|
||||||
|
onMounted(loadSession)
|
||||||
onMounted(watchConnect)
|
onMounted(watchConnect)
|
||||||
onUnmounted(watchDisconnect)
|
onUnmounted(watchDisconnect)
|
||||||
// Update human-readable x seconds ago messages from mtimes
|
// Update human-readable x seconds ago messages from mtimes
|
||||||
|
|
|
@ -22,29 +22,16 @@ export type errorEvent = {
|
||||||
|
|
||||||
// Raw types the backend /api/watch sends us
|
// Raw types the backend /api/watch sends us
|
||||||
|
|
||||||
export type FileEntry = {
|
export type FileEntry = [
|
||||||
key: FUID
|
number, // level
|
||||||
size: number
|
string, // name
|
||||||
mtime: number
|
FUID,
|
||||||
}
|
number, //mtime
|
||||||
|
number, // size
|
||||||
|
number, // isfile
|
||||||
|
]
|
||||||
|
|
||||||
export type DirEntry = {
|
export type UpdateEntry = ['k', number] | ['d', number] | ['i', Array<FileEntry>]
|
||||||
key: FUID
|
|
||||||
size: number
|
|
||||||
mtime: number
|
|
||||||
dir: DirList
|
|
||||||
}
|
|
||||||
|
|
||||||
export type DirList = Record<string, FileEntry | DirEntry>
|
|
||||||
|
|
||||||
export type UpdateEntry = {
|
|
||||||
name: string
|
|
||||||
deleted?: boolean
|
|
||||||
key?: FUID
|
|
||||||
size?: number
|
|
||||||
mtime?: number
|
|
||||||
dir?: DirList
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper structure for selections
|
// Helper structure for selections
|
||||||
export interface SelectedItems {
|
export interface SelectedItems {
|
||||||
|
|
|
@ -1,14 +1,29 @@
|
||||||
import { useDocumentStore } from "@/stores/documents"
|
import { useDocumentStore } from "@/stores/documents"
|
||||||
import type { DirEntry, UpdateEntry, errorEvent } from "./Document"
|
import type { FileEntry, UpdateEntry, errorEvent } from "./Document"
|
||||||
|
|
||||||
export const controlUrl = '/api/control'
|
export const controlUrl = '/api/control'
|
||||||
export const uploadUrl = '/api/upload'
|
export const uploadUrl = '/api/upload'
|
||||||
export const watchUrl = '/api/watch'
|
export const watchUrl = '/api/watch'
|
||||||
|
|
||||||
let tree = null as DirEntry | null
|
let tree = [] as FileEntry[]
|
||||||
let reconnectDuration = 500
|
let reconnectDuration = 500
|
||||||
let wsWatch = null as WebSocket | null
|
let wsWatch = null as WebSocket | null
|
||||||
|
|
||||||
|
export const loadSession = () => {
|
||||||
|
const store = useDocumentStore()
|
||||||
|
try {
|
||||||
|
tree = JSON.parse(sessionStorage["cista-files"])
|
||||||
|
store.updateRoot(tree)
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveSession = () => {
|
||||||
|
sessionStorage["cista-files"] = JSON.stringify(tree)
|
||||||
|
}
|
||||||
|
|
||||||
export const connect = (path: string, handlers: Partial<Record<keyof WebSocketEventMap, any>>) => {
|
export const connect = (path: string, handlers: Partial<Record<keyof WebSocketEventMap, any>>) => {
|
||||||
const webSocket = new WebSocket(new URL(path, location.origin.replace(/^http/, 'ws')))
|
const webSocket = new WebSocket(new URL(path, location.origin.replace(/^http/, 'ws')))
|
||||||
for (const [event, handler] of Object.entries(handlers)) webSocket.addEventListener(event, handler)
|
for (const [event, handler] of Object.entries(handlers)) webSocket.addEventListener(event, handler)
|
||||||
|
@ -99,29 +114,31 @@ function handleRootMessage({ root }: { root: DirEntry }) {
|
||||||
console.log('Watch root', root)
|
console.log('Watch root', root)
|
||||||
store.updateRoot(root)
|
store.updateRoot(root)
|
||||||
tree = root
|
tree = root
|
||||||
|
saveSession()
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleUpdateMessage(updateData: { update: UpdateEntry[] }) {
|
function handleUpdateMessage(updateData: { update: UpdateEntry[] }) {
|
||||||
const store = useDocumentStore()
|
const store = useDocumentStore()
|
||||||
console.log('Watch update', updateData.update)
|
const update = updateData.update
|
||||||
|
console.log('Watch update', update)
|
||||||
if (!tree) return console.error('Watch update before root')
|
if (!tree) return console.error('Watch update before root')
|
||||||
let node: DirEntry = tree
|
let newtree = []
|
||||||
for (const elem of updateData.update) {
|
let oidx = 0
|
||||||
if (elem.deleted) {
|
|
||||||
delete node.dir[elem.name]
|
for (const [action, arg] of update) {
|
||||||
break // Deleted elements can't have further children
|
if (action === 'k') {
|
||||||
|
newtree.push(...tree.slice(oidx, oidx + arg))
|
||||||
|
oidx += arg
|
||||||
}
|
}
|
||||||
if (elem.name) {
|
else if (action === 'd') oidx += arg
|
||||||
// @ts-ignore
|
else if (action === 'i') newtree.push(...arg)
|
||||||
console.log(node, elem.name)
|
else console.log("Unknown update action", action, arg)
|
||||||
node = node.dir[elem.name] ||= {}
|
|
||||||
}
|
}
|
||||||
if (elem.key !== undefined) node.key = elem.key
|
if (oidx != tree.length)
|
||||||
if (elem.size !== undefined) node.size = elem.size
|
throw Error(`Tree update out of sync, number of entries mismatch: got ${oidx}, expected ${tree.length}`)
|
||||||
if (elem.mtime !== undefined) node.mtime = elem.mtime
|
store.updateRoot(newtree)
|
||||||
if (elem.dir !== undefined) node.dir = elem.dir
|
tree = newtree
|
||||||
}
|
saveSession()
|
||||||
store.updateRoot(tree)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleError(msg: errorEvent) {
|
function handleError(msg: errorEvent) {
|
||||||
|
|
|
@ -1,10 +1,4 @@
|
||||||
import type {
|
import type { Document, FileEntry, FUID, SelectedItems } from '@/repositories/Document'
|
||||||
Document,
|
|
||||||
DirEntry,
|
|
||||||
FileEntry,
|
|
||||||
FUID,
|
|
||||||
SelectedItems
|
|
||||||
} from '@/repositories/Document'
|
|
||||||
import { formatSize, formatUnixDate, haystackFormat } from '@/utils'
|
import { formatSize, formatUnixDate, haystackFormat } from '@/utils'
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { collator } from '@/utils'
|
import { collator } from '@/utils'
|
||||||
|
@ -26,7 +20,6 @@ export const useDocumentStore = defineStore({
|
||||||
id: 'documents',
|
id: 'documents',
|
||||||
state: () => ({
|
state: () => ({
|
||||||
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,
|
||||||
|
@ -41,46 +34,27 @@ export const useDocumentStore = defineStore({
|
||||||
isOpenLoginModal: false
|
isOpenLoginModal: false
|
||||||
} as User
|
} as User
|
||||||
}),
|
}),
|
||||||
persist: {
|
|
||||||
storage: sessionStorage,
|
|
||||||
paths: ['document'],
|
|
||||||
},
|
|
||||||
actions: {
|
actions: {
|
||||||
updateRoot(root: DirEntry | null = null) {
|
updateRoot(root: FileEntry[]) {
|
||||||
if (!root) {
|
|
||||||
this.document = []
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Transform tree data to flat documents array
|
|
||||||
let loc = ""
|
|
||||||
const mapper = ([name, attr]: [string, FileEntry | DirEntry]) => ({
|
|
||||||
...attr,
|
|
||||||
loc,
|
|
||||||
name,
|
|
||||||
sizedisp: formatSize(attr.size),
|
|
||||||
modified: formatUnixDate(attr.mtime),
|
|
||||||
haystack: haystackFormat(name),
|
|
||||||
})
|
|
||||||
const queue = [...Object.entries(root.dir ?? {}).map(mapper)]
|
|
||||||
const docs = []
|
const docs = []
|
||||||
for (let doc; (doc = queue.shift()) !== undefined;) {
|
let loc = [] as string[]
|
||||||
docs.push(doc)
|
for (const [level, name, key, mtime, size, isfile] of root) {
|
||||||
if ("dir" in doc) {
|
if (level === 0) continue
|
||||||
// Recurse but replace recursive structure with boolean
|
loc = loc.slice(0, level - 1)
|
||||||
loc = doc.loc ? `${doc.loc}/${doc.name}` : doc.name
|
docs.push({
|
||||||
queue.push(...Object.entries(doc.dir).map(mapper))
|
name,
|
||||||
// @ts-ignore
|
loc: loc.join('/'),
|
||||||
doc.dir = true
|
key,
|
||||||
|
size,
|
||||||
|
sizedisp: formatSize(size),
|
||||||
|
mtime,
|
||||||
|
modified: formatUnixDate(mtime),
|
||||||
|
haystack: haystackFormat(name),
|
||||||
|
dir: !isfile,
|
||||||
|
})
|
||||||
|
loc.push(name)
|
||||||
}
|
}
|
||||||
// @ts-ignore
|
console.log("Documents", docs)
|
||||||
else doc.dir = false
|
|
||||||
}
|
|
||||||
// Pre sort directory entries folders first then files, names in natural ordering
|
|
||||||
docs.sort((a, b) =>
|
|
||||||
// @ts-ignore
|
|
||||||
b.dir - a.dir ||
|
|
||||||
collator.compare(a.name, b.name)
|
|
||||||
)
|
|
||||||
this.document = docs as Document[]
|
this.document = docs as Document[]
|
||||||
},
|
},
|
||||||
login(username: string, privileged: boolean) {
|
login(username: string, privileged: boolean) {
|
||||||
|
|
|
@ -16,17 +16,17 @@ 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>
|
||||||
})
|
query: string
|
||||||
|
}>()
|
||||||
const documents = computed(() => {
|
const documents = computed(() => {
|
||||||
if (!props.path) return []
|
|
||||||
const loc = props.path.join('/')
|
const loc = props.path.join('/')
|
||||||
|
const query = props.query
|
||||||
// List the current location
|
// List the current location
|
||||||
if (!documentStore.search) return documentStore.document.filter(doc => doc.loc === loc)
|
if (!query) return documentStore.document.filter(doc => doc.loc === loc)
|
||||||
// Find up to 100 newest documents that match the search
|
// Find up to 100 newest documents that match the search
|
||||||
const search = documentStore.search
|
const needle = needleFormat(query)
|
||||||
const needle = needleFormat(search)
|
|
||||||
let limit = 100
|
let limit = 100
|
||||||
let docs = []
|
let docs = []
|
||||||
for (const doc of documentStore.recentDocuments) {
|
for (const doc of documentStore.recentDocuments) {
|
||||||
|
@ -46,7 +46,7 @@ const documents = computed(() => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
(a.type === 'file') - (b.type === 'file') ||
|
(a.type === 'file') - (b.type === 'file') ||
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
b.name.includes(search) - a.name.includes(search) ||
|
b.name.includes(query) - a.name.includes(query) ||
|
||||||
collator.compare(a.name, b.name)
|
collator.compare(a.name, b.name)
|
||||||
))
|
))
|
||||||
return docs
|
return docs
|
||||||
|
|
Loading…
Reference in New Issue
Block a user