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:
@@ -280,3 +280,6 @@ header nav.headermain {
|
||||
background: var(--accent-color);
|
||||
color: #000;
|
||||
}
|
||||
.ghost {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 = ""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user