Implement ghost items reflecting pending changes from frontend (e.g. files being uploaded) until the server acknowledges them. This gives immediate UI feedback and is clearer in cases.

This commit is contained in:
Leo Vasanko
2026-02-01 03:23:43 +00:00
parent abe967de1b
commit 51bf029d2a
7 changed files with 40 additions and 7 deletions
+3
View File
@@ -280,3 +280,6 @@ header nav.headermain {
background: var(--accent-color); background: var(--accent-color);
color: #000; color: #000;
} }
.ghost {
opacity: 0.5;
}
+4 -2
View File
@@ -28,7 +28,7 @@
<tr <tr
:id="`file-${doc.key}`" :id="`file-${doc.key}`"
:class="{ file: !doc.dir, folder: doc.dir, cursor: store.cursor === doc.key }" :class="{ file: !doc.dir, folder: doc.dir, cursor: store.cursor === doc.key, ghost: doc.ghost }"
@click="store.cursor = store.cursor === doc.key ? '' : doc.key" @click="store.cursor = store.cursor === doc.key ? '' : doc.key"
@contextmenu.prevent="contextMenu($event, doc)" @contextmenu.prevent="contextMenu($event, doc)"
> >
@@ -249,9 +249,11 @@ const mkdir = (doc: Doc, name: string) => {
} }
} }
}) })
// We should get an update from watch but this is quicker
doc.name = name doc.name = name
doc.key = crypto.randomUUID() doc.key = crypto.randomUUID()
doc.ghost = true
store.document.push(doc)
editing.value = null
} }
const showFolderBreadcrumb = (i: number) => { const showFolderBreadcrumb = (i: number) => {
const docs = props.documents const docs = props.documents
+3 -1
View File
@@ -203,9 +203,11 @@ const mkdir = (doc: Doc, name: string) => {
} }
} }
}) })
// We should get an update from watch but this is quicker
doc.name = name doc.name = name
doc.key = crypto.randomUUID() doc.key = crypto.randomUUID()
doc.ghost = true
store.document.push(doc)
editing.value = null
} }
const showFolderBreadcrumb = (i: number) => { const showFolderBreadcrumb = (i: number) => {
const docs = props.documents const docs = props.documents
+1 -1
View File
@@ -1,6 +1,6 @@
<template> <template>
<a :id="`file-${doc.key}`" :href=doc.url tabindex=-1 <a :id="`file-${doc.key}`" :href=doc.url tabindex=-1
:class="{ file: !doc.dir, folder: doc.dir, cursor: store.cursor === doc.key }" :class="{ file: !doc.dir, folder: doc.dir, cursor: store.cursor === doc.key, ghost: doc.ghost }"
@contextmenu.stop @contextmenu.stop
@focus.stop="store.cursor = doc.key" @focus.stop="store.cursor = doc.key"
@click=onclick @click=onclick
+4 -2
View File
@@ -21,10 +21,10 @@ const props = defineProps({
}) })
const dst = computed(() => props.path!.join('/')) const dst = computed(() => props.path!.join('/'))
const op = (op: string, dst?: string) => { const op = (opName: string, dst?: string) => {
const sel = store.selectedFiles const sel = store.selectedFiles
const msg = { const msg = {
op, op: opName,
sel: sel.keys.map(key => { sel: sel.keys.map(key => {
const doc = sel.docs[key]! const doc = sel.docs[key]!
return doc.loc ? `${doc.loc}/${doc.name}` : doc.name return doc.loc ? `${doc.loc}/${doc.name}` : doc.name
@@ -32,6 +32,8 @@ const op = (op: string, dst?: string) => {
} }
// @ts-ignore // @ts-ignore
if (dst !== undefined) msg.dst = dst if (dst !== undefined) msg.dst = dst
if (opName === 'rm' || opName === 'mv')
for (const key of sel.keys) sel.docs[key]!.ghost = true
const control = connect(controlUrl, { const control = connect(controlUrl, {
message(ev: MessageEvent) { message(ev: MessageEvent) {
const res = JSON.parse(ev.data) const res = JSON.parse(ev.data)
+23 -1
View File
@@ -10,6 +10,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { connect, uploadUrl } from '@/repositories/WS'; import { connect, uploadUrl } from '@/repositories/WS';
import { useMainStore } from '@/stores/main' import { useMainStore } from '@/stores/main'
import { Doc } from '@/repositories/Document'
import { collator } from '@/utils'; import { collator } from '@/utils';
import { onMounted, onUnmounted, reactive, ref } from 'vue' import { onMounted, onUnmounted, reactive, ref } from 'vue'
@@ -85,11 +86,32 @@ const uploadCloudFiles = (files: CloudFile[]) => {
const dotfiles = files.filter(f => f.cloudName.includes('/.')) const dotfiles = files.filter(f => f.cloudName.includes('/.'))
if (dotfiles.length) { if (dotfiles.length) {
store.showToast("Won't upload dotfiles") store.showToast("Won't upload dotfiles")
console.log("Dotfiles omitted", dotfiles)
files = files.filter(f => !f.cloudName.includes('/.')) files = files.filter(f => !f.cloudName.includes('/.'))
} }
if (!files.length) return if (!files.length) return
files.sort((a, b) => collator.compare(a.cloudName, b.cloudName)) files.sort((a, b) => collator.compare(a.cloudName, b.cloudName))
// Optimistic update: ghost folders and files
const now = Math.floor(Date.now() / 1000)
const byPath = new Map(store.document.map(d => [d.loc ? `${d.loc}/${d.name}` : d.name, d]))
const added = new Set<string>()
for (const f of files) {
const lastSlash = f.cloudName.lastIndexOf('/')
const loc = lastSlash > 0 ? f.cloudName.slice(0, lastSlash) : ''
const name = f.cloudName.slice(lastSlash + 1)
// Ghost folders for intermediate directories
const parts = loc.split('/')
for (let i = 0; i < parts.length; i++) {
const folderPath = parts.slice(0, i + 1).join('/')
if (folderPath && !byPath.has(folderPath) && !added.has(folderPath)) {
store.document.push(new Doc({ loc: parts.slice(0, i).join('/'), name: parts[i], key: crypto.randomUUID(), size: 0, mtime: now, dir: true, ghost: true }))
added.add(folderPath)
}
}
// Ghost file or update existing
const existing = byPath.get(f.cloudName)
if (existing) { existing.size = f.file.size; existing.mtime = now; existing.ghost = true }
else store.document.push(new Doc({ loc, name, key: crypto.randomUUID(), size: f.file.size, mtime: now, dir: false, ghost: true }))
}
// @ts-ignore // @ts-ignore
upqueue = [...upqueue, ...files] upqueue = [...upqueue, ...files]
statsAdd(files) statsAdd(files)
+2
View File
@@ -9,6 +9,7 @@ export type DocProps = {
size: number size: number
mtime: number mtime: number
dir: boolean dir: boolean
ghost?: boolean
} }
export class Doc { export class Doc {
@@ -17,6 +18,7 @@ export class Doc {
public size: number = 0 public size: number = 0
public mtime: number = 0 public mtime: number = 0
public dir: boolean = false public dir: boolean = false
public ghost: boolean = false
/** @internal Use the name getter/setter instead */ /** @internal Use the name getter/setter instead */
public _name: string = "" public _name: string = ""