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);
color: #000;
}
.ghost {
opacity: 0.5;
}
+4 -2
View File
@@ -28,7 +28,7 @@
<tr
: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"
@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.key = crypto.randomUUID()
doc.ghost = true
store.document.push(doc)
editing.value = null
}
const showFolderBreadcrumb = (i: number) => {
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.key = crypto.randomUUID()
doc.ghost = true
store.document.push(doc)
editing.value = null
}
const showFolderBreadcrumb = (i: number) => {
const docs = props.documents
+1 -1
View File
@@ -1,6 +1,6 @@
<template>
<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
@focus.stop="store.cursor = doc.key"
@click=onclick
+4 -2
View File
@@ -21,10 +21,10 @@ const props = defineProps({
})
const dst = computed(() => props.path!.join('/'))
const op = (op: string, dst?: string) => {
const op = (opName: string, dst?: string) => {
const sel = store.selectedFiles
const msg = {
op,
op: opName,
sel: sel.keys.map(key => {
const doc = sel.docs[key]!
return doc.loc ? `${doc.loc}/${doc.name}` : doc.name
@@ -32,6 +32,8 @@ const op = (op: string, dst?: string) => {
}
// @ts-ignore
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, {
message(ev: MessageEvent) {
const res = JSON.parse(ev.data)
+23 -1
View File
@@ -10,6 +10,7 @@
<script setup lang="ts">
import { connect, uploadUrl } from '@/repositories/WS';
import { useMainStore } from '@/stores/main'
import { Doc } from '@/repositories/Document'
import { collator } from '@/utils';
import { onMounted, onUnmounted, reactive, ref } from 'vue'
@@ -85,11 +86,32 @@ const uploadCloudFiles = (files: CloudFile[]) => {
const dotfiles = files.filter(f => f.cloudName.includes('/.'))
if (dotfiles.length) {
store.showToast("Won't upload dotfiles")
console.log("Dotfiles omitted", dotfiles)
files = files.filter(f => !f.cloudName.includes('/.'))
}
if (!files.length) return
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
upqueue = [...upqueue, ...files]
statsAdd(files)
+2
View File
@@ -9,6 +9,7 @@ export type DocProps = {
size: number
mtime: number
dir: boolean
ghost?: boolean
}
export class Doc {
@@ -17,6 +18,7 @@ export class Doc {
public size: number = 0
public mtime: number = 0
public dir: boolean = false
public ghost: boolean = false
/** @internal Use the name getter/setter instead */
public _name: string = ""