More efficient flat file list format and various UX improvements #3

Merged
leo merged 19 commits from major-upgrade into main 2023-11-12 23:20:40 +00:00
5 changed files with 82 additions and 100 deletions
Showing only changes of commit 00a4297c0b - Show all commits

View File

@ -1,13 +1,13 @@
<template>
<LoginModal />
<header>
<HeaderMain ref="headerMain" :path="path.pathList">
<HeaderMain ref="headerMain" :path="path.pathList" :query="path.query">
<HeaderSelected :path="path.pathList" />
</HeaderMain>
<BreadCrumb :path="path.pathList" tabindex="-1"/>
</header>
<main>
<RouterView :path="path.pathList" />
<RouterView :path="path.pathList" :query="path.query" />
</main>
</template>
@ -16,7 +16,7 @@ import { RouterView } from 'vue-router'
import type { ComputedRef } from 'vue'
import type HeaderMain from '@/components/HeaderMain.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 { computed } from 'vue'
@ -25,19 +25,23 @@ import Router from '@/router/index'
interface Path {
path: string
pathList: string[]
query: string
}
const documentStore = useDocumentStore()
const path: ComputedRef<Path> = computed(() => {
const p = decodeURIComponent(Router.currentRoute.value.path)
const pathList = p.split('/').filter(value => value !== '')
const p = decodeURIComponent(Router.currentRoute.value.path).split('//')
const pathList = p[0].split('/').filter(value => value !== '')
const query = p.slice(1).join('//')
return {
path: p,
pathList
path: p[0],
pathList,
query
}
})
watchEffect(() => {
document.title = path.value.path.replace(/\/$/, '').split('/').pop() || documentStore.server.name || 'Cista Storage'
})
onMounted(loadSession)
onMounted(watchConnect)
onUnmounted(watchDisconnect)
// Update human-readable x seconds ago messages from mtimes

View File

@ -22,29 +22,16 @@ export type errorEvent = {
// Raw types the backend /api/watch sends us
export type FileEntry = {
key: FUID
size: number
mtime: number
}
export type FileEntry = [
number, // level
string, // name
FUID,
number, //mtime
number, // size
number, // isfile
]
export type DirEntry = {
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
}
export type UpdateEntry = ['k', number] | ['d', number] | ['i', Array<FileEntry>]
// Helper structure for selections
export interface SelectedItems {

View File

@ -1,14 +1,29 @@
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 uploadUrl = '/api/upload'
export const watchUrl = '/api/watch'
let tree = null as DirEntry | null
let tree = [] as FileEntry[]
let reconnectDuration = 500
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>>) => {
const webSocket = new WebSocket(new URL(path, location.origin.replace(/^http/, 'ws')))
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)
store.updateRoot(root)
tree = root
saveSession()
}
function handleUpdateMessage(updateData: { update: UpdateEntry[] }) {
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')
let node: DirEntry = tree
for (const elem of updateData.update) {
if (elem.deleted) {
delete node.dir[elem.name]
break // Deleted elements can't have further children
}
if (elem.name) {
// @ts-ignore
console.log(node, elem.name)
node = node.dir[elem.name] ||= {}
}
if (elem.key !== undefined) node.key = elem.key
if (elem.size !== undefined) node.size = elem.size
if (elem.mtime !== undefined) node.mtime = elem.mtime
if (elem.dir !== undefined) node.dir = elem.dir
let newtree = []
let oidx = 0
for (const [action, arg] of update) {
if (action === 'k') {
newtree.push(...tree.slice(oidx, oidx + arg))
oidx += arg
}
else if (action === 'd') oidx += arg
else if (action === 'i') newtree.push(...arg)
else console.log("Unknown update action", action, arg)
}
store.updateRoot(tree)
if (oidx != tree.length)
throw Error(`Tree update out of sync, number of entries mismatch: got ${oidx}, expected ${tree.length}`)
store.updateRoot(newtree)
tree = newtree
saveSession()
}
function handleError(msg: errorEvent) {

View File

@ -1,10 +1,4 @@
import type {
Document,
DirEntry,
FileEntry,
FUID,
SelectedItems
} from '@/repositories/Document'
import type { Document, FileEntry, FUID, SelectedItems } from '@/repositories/Document'
import { formatSize, formatUnixDate, haystackFormat } from '@/utils'
import { defineStore } from 'pinia'
import { collator } from '@/utils'
@ -26,7 +20,6 @@ export const useDocumentStore = defineStore({
id: 'documents',
state: () => ({
document: [] as Document[],
search: "" as string,
selected: new Set<FUID>(),
uploadingDocuments: [],
uploadCount: 0 as number,
@ -41,46 +34,27 @@ export const useDocumentStore = defineStore({
isOpenLoginModal: false
} as User
}),
persist: {
storage: sessionStorage,
paths: ['document'],
},
actions: {
updateRoot(root: DirEntry | null = null) {
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)]
updateRoot(root: FileEntry[]) {
const docs = []
for (let doc; (doc = queue.shift()) !== undefined;) {
docs.push(doc)
if ("dir" in doc) {
// Recurse but replace recursive structure with boolean
loc = doc.loc ? `${doc.loc}/${doc.name}` : doc.name
queue.push(...Object.entries(doc.dir).map(mapper))
// @ts-ignore
doc.dir = true
}
// @ts-ignore
else doc.dir = false
let loc = [] as string[]
for (const [level, name, key, mtime, size, isfile] of root) {
if (level === 0) continue
loc = loc.slice(0, level - 1)
docs.push({
name,
loc: loc.join('/'),
key,
size,
sizedisp: formatSize(size),
mtime,
modified: formatUnixDate(mtime),
haystack: haystackFormat(name),
dir: !isfile,
})
loc.push(name)
}
// 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)
)
console.log("Documents", docs)
this.document = docs as Document[]
},
login(username: string, privileged: boolean) {

View File

@ -16,17 +16,17 @@ import { needleFormat, localeIncludes, collator } from '@/utils';
const documentStore = useDocumentStore()
const fileExplorer = ref()
const props = defineProps({
const props = defineProps<{
path: Array<string>
})
query: string
}>()
const documents = computed(() => {
if (!props.path) return []
const loc = props.path.join('/')
const query = props.query
// 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
const search = documentStore.search
const needle = needleFormat(search)
const needle = needleFormat(query)
let limit = 100
let docs = []
for (const doc of documentStore.recentDocuments) {
@ -46,7 +46,7 @@ const documents = computed(() => {
// @ts-ignore
(a.type === 'file') - (b.type === 'file') ||
// @ts-ignore
b.name.includes(search) - a.name.includes(search) ||
b.name.includes(query) - a.name.includes(query) ||
collator.compare(a.name, b.name)
))
return docs