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