Fix free/used disk space handling. Implement file disk usage tracking. Display indicators on FileExplorer for incomplete files (sparse allocation, upload in progress).
This commit is contained in:
@@ -36,9 +36,9 @@
|
||||
</g>
|
||||
|
||||
<g ref="labelsRef" class="pie-labels">
|
||||
<text :x="storageInnerPos.x" :y="storageInnerPos.y" class="pie-label-inner" :text-anchor="getSizeAnchor(sectorInfo.storage.angle)" dominant-baseline="middle" :transform="`rotate(${getSizeRotation(sectorInfo.storage.angle)} ${storageInnerPos.x} ${storageInnerPos.y})`">{{ fmtSize(store.space.storage, sectorInfo.storage.angle) }}</text>
|
||||
<text :x="storageInnerPos.x" :y="storageInnerPos.y" class="pie-label-inner" :text-anchor="getSizeAnchor(sectorInfo.storage.angle)" dominant-baseline="middle" :transform="`rotate(${getSizeRotation(sectorInfo.storage.angle)} ${storageInnerPos.x} ${storageInnerPos.y})`">{{ fmtSize(store.space.allocated, sectorInfo.storage.angle) }}</text>
|
||||
<text :x="freeInnerPos.x" :y="freeInnerPos.y" class="pie-label-inner" :text-anchor="getSizeAnchor(sectorInfo.free.angle)" dominant-baseline="middle" :transform="`rotate(${getSizeRotation(sectorInfo.free.angle)} ${freeInnerPos.x} ${freeInnerPos.y})`">{{ fmtSize(store.space.free, sectorInfo.free.angle) }}</text>
|
||||
<text :x="otherInnerPos.x" :y="otherInnerPos.y" class="pie-label-inner" :text-anchor="getSizeAnchor(sectorInfo.other.angle)" dominant-baseline="middle" :transform="`rotate(${getSizeRotation(sectorInfo.other.angle)} ${otherInnerPos.x} ${otherInnerPos.y})`">{{ fmtSize(store.space.usage - store.space.storage, sectorInfo.other.angle) }}</text>
|
||||
<text :x="otherInnerPos.x" :y="otherInnerPos.y" class="pie-label-inner" :text-anchor="getSizeAnchor(sectorInfo.other.angle)" dominant-baseline="middle" :transform="`rotate(${getSizeRotation(sectorInfo.other.angle)} ${otherInnerPos.x} ${otherInnerPos.y})`">{{ fmtSize(store.space.used - store.space.allocated, sectorInfo.other.angle) }}</text>
|
||||
|
||||
<defs>
|
||||
<path :id="storageLabelPath.id" :d="storageLabelPath.d" fill="none" />
|
||||
@@ -113,7 +113,7 @@ const CIRC = TAU * midRadius
|
||||
const pieStorageDash = computed(() => {
|
||||
const s = store.space
|
||||
if (!s.disk) return `0 ${CIRC}`
|
||||
return `${(s.storage / s.disk) * CIRC} ${CIRC}`
|
||||
return `${(s.allocated / s.disk) * CIRC} ${CIRC}`
|
||||
})
|
||||
|
||||
const pieFreeDash = computed(() => {
|
||||
@@ -125,7 +125,7 @@ const pieFreeDash = computed(() => {
|
||||
const pieFreeOffsetVal = computed(() => {
|
||||
const s = store.space
|
||||
if (!s.disk) return 0
|
||||
return -(s.storage / s.disk) * CIRC
|
||||
return -(s.allocated / s.disk) * CIRC
|
||||
})
|
||||
|
||||
const freeColor = computed(() => {
|
||||
@@ -153,9 +153,9 @@ const sectorInfo = computed(() => {
|
||||
other: { angle: 270, pct: 0.25 }
|
||||
}
|
||||
|
||||
const storagePct = s.storage / s.disk
|
||||
const storagePct = s.allocated / s.disk
|
||||
const freePct = s.free / s.disk
|
||||
const otherPct = (s.usage - s.storage) / s.disk
|
||||
const otherPct = (s.used - s.allocated) / s.disk
|
||||
|
||||
const storageAngle = storagePct * 180 // midpoint of storage sector
|
||||
const freeStart = storagePct * 360
|
||||
|
||||
@@ -124,6 +124,7 @@ defineExpose({
|
||||
dir: true,
|
||||
mtime: now,
|
||||
size: 0,
|
||||
allocated: 0,
|
||||
})
|
||||
store.cursor = editing.value.key
|
||||
},
|
||||
|
||||
@@ -1,22 +1,44 @@
|
||||
<template>
|
||||
<td class="size right" :class=sizeClass>{{ doc.sizedisp }}</td>
|
||||
<td
|
||||
class="size right"
|
||||
:class="sizeClass"
|
||||
@mouseenter="doc.sparseIndicator && tooltip?.startHover($event)"
|
||||
@mousemove="doc.sparseIndicator && tooltip?.updatePosition($event)"
|
||||
@mouseleave="doc.sparseIndicator && tooltip?.endHover()"
|
||||
>
|
||||
<SparseIndicator :doc="doc" class="before-size" />{{ doc.sizedisp }}
|
||||
<CursorTooltip v-if="doc.sparseIndicator" ref="tooltip" :text="tooltipText">{{ tooltipText }}</CursorTooltip>
|
||||
</td>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Doc } from '@/repositories/Document'
|
||||
import { computed } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import { formatSize } from '@/utils'
|
||||
import SparseIndicator from './SparseIndicator.vue'
|
||||
import CursorTooltip from './CursorTooltip.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
doc: Doc
|
||||
}>()
|
||||
|
||||
const tooltip = ref<InstanceType<typeof CursorTooltip> | null>(null)
|
||||
|
||||
const sizeClass = computed(() => {
|
||||
const unit = props.doc.sizedisp.split('\u202F').slice(-1)[0]!
|
||||
return +unit ? "bytes" : unit
|
||||
})
|
||||
|
||||
const props = defineProps<{
|
||||
doc: Doc
|
||||
}>()
|
||||
const tooltipText = computed(() => {
|
||||
const { size, allocated } = props.doc
|
||||
return `${formatSize(allocated)} allocated of ${formatSize(size)}`
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.before-size {
|
||||
margin-right: 0.2em;
|
||||
}
|
||||
.size.empty { color: #555 }
|
||||
.size.bytes { color: #77a }
|
||||
.size.kB { color: #474 }
|
||||
|
||||
@@ -67,6 +67,7 @@ defineExpose({
|
||||
dir: true,
|
||||
mtime: now,
|
||||
size: 0,
|
||||
allocated: 0,
|
||||
})
|
||||
store.cursor = editing.value.key
|
||||
},
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
</template>
|
||||
<template v-else>
|
||||
<SelectBox :doc=doc @click="store.cursor = doc.key"/>
|
||||
<span>{{ doc.name }}</span>
|
||||
<span>{{ doc.name }}<SparseIndicator :doc="doc" class="after-name" /></span>
|
||||
<div class=namespacer></div>
|
||||
</template>
|
||||
</figcaption>
|
||||
@@ -26,6 +26,7 @@
|
||||
<CursorTooltip ref="tooltip" :text="tooltipText">
|
||||
<div class="tooltip-name">{{ doc.name }}</div>
|
||||
<div class="tooltip-details">{{ doc.modified }} — {{ doc.sizedisp }}</div>
|
||||
<div v-if="doc.sparseIndicator" class="tooltip-sparse">{{ sparseText }}</div>
|
||||
</CursorTooltip>
|
||||
</a>
|
||||
</template>
|
||||
@@ -34,8 +35,10 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import { useMainStore } from '@/stores/main'
|
||||
import { Doc } from '@/repositories/Document'
|
||||
import { formatSize } from '@/utils'
|
||||
import MediaPreview from '@/components/MediaPreview.vue'
|
||||
import CursorTooltip from './CursorTooltip.vue'
|
||||
import SparseIndicator from './SparseIndicator.vue'
|
||||
|
||||
const store = useMainStore()
|
||||
type EditingProp = {
|
||||
@@ -52,6 +55,11 @@ const tooltip = ref<InstanceType<typeof CursorTooltip> | null>(null)
|
||||
|
||||
const tooltipText = computed(() => props.doc.key)
|
||||
|
||||
const sparseText = computed(() => {
|
||||
const { size, allocated } = props.doc
|
||||
return `${formatSize(allocated)} allocated of ${formatSize(size)}`
|
||||
})
|
||||
|
||||
const onclick = (ev: Event) => {
|
||||
if (m.value!.play()) ev.preventDefault()
|
||||
store.cursor = props.doc.key
|
||||
@@ -66,6 +74,13 @@ const onclick = (ev: Event) => {
|
||||
.tooltip-details {
|
||||
text-align: center;
|
||||
}
|
||||
.tooltip-sparse {
|
||||
text-align: center;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.after-name {
|
||||
margin-left: 0.3em;
|
||||
}
|
||||
figure {
|
||||
max-height: 15em;
|
||||
position: relative;
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<span v-if="doc.sparseIndicator" class="sparse-indicator">{{ doc.sparseIndicator }}</span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Doc } from '@/repositories/Document'
|
||||
|
||||
defineProps<{
|
||||
doc: Doc
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.sparse-indicator {
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
@@ -115,13 +115,13 @@ const uploadCloudFiles = (files: CloudFile[]) => {
|
||||
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.addGhost(new Doc({ loc: parts.slice(0, i).join('/'), name: parts[i], key: crypto.randomUUID(), size: 0, mtime: now, dir: true }))
|
||||
store.addGhost(new Doc({ loc: parts.slice(0, i).join('/'), name: parts[i], key: crypto.randomUUID(), size: 0, allocated: 0, mtime: now, dir: true }))
|
||||
added.add(folderPath)
|
||||
}
|
||||
}
|
||||
// Ghost file or update existing (overwrite case doesn't need ghost, file already visible)
|
||||
const existing = byPath.get(f.cloudName)
|
||||
if (!existing) store.addGhost(new Doc({ loc, name, key: crypto.randomUUID(), size: f.file.size, mtime: now, dir: false }))
|
||||
if (!existing) store.addGhost(new Doc({ loc, name, key: crypto.randomUUID(), size: f.file.size, allocated: 0, mtime: now, dir: false }))
|
||||
}
|
||||
// @ts-ignore
|
||||
upqueue = [...upqueue, ...files]
|
||||
|
||||
@@ -7,6 +7,7 @@ export type DocProps = {
|
||||
name: string
|
||||
key: FUID
|
||||
size: number
|
||||
allocated: number
|
||||
mtime: number
|
||||
dir: boolean
|
||||
ghost?: boolean
|
||||
@@ -17,6 +18,7 @@ export class Doc {
|
||||
public loc: string = ""
|
||||
public key: FUID = ""
|
||||
public size: number = 0
|
||||
public allocated: number = 0
|
||||
public mtime: number = 0
|
||||
public dir: boolean = false
|
||||
public ghost: boolean = false
|
||||
@@ -35,6 +37,15 @@ export class Doc {
|
||||
this._name = name
|
||||
}
|
||||
get sizedisp(): string { return formatSize(this.size) }
|
||||
/** Returns a sparse allocation indicator symbol, or empty string if fully allocated */
|
||||
get sparseIndicator(): string {
|
||||
if (this.dir || this.size <= this.allocated) return ''
|
||||
if (this.allocated === 0) return '⭕' // exactly zero
|
||||
const ratio = this.allocated / this.size
|
||||
// Round to nearest 25%: ◔◑◕⬤
|
||||
const rounded = Math.round(ratio * 4) // 0,1,2,3,4
|
||||
return ['◔', '◔', '◑', '◕', '⬤'][rounded]! // 0 maps to ◔ since we handled exact 0 above
|
||||
}
|
||||
get modified(): string { return formatUnixDate(this.mtime) }
|
||||
get url(): string {
|
||||
const p = this.loc ? `${this.loc}/${this.name}` : this.name
|
||||
@@ -78,9 +89,10 @@ export type FileEntry = [
|
||||
number, // level
|
||||
string, // name
|
||||
FUID,
|
||||
number, //mtime
|
||||
number, // size
|
||||
number, // isfile
|
||||
number, // mtime
|
||||
number, // size
|
||||
number, // allocated (actual disk usage)
|
||||
number, // isfile
|
||||
]
|
||||
|
||||
export type UpdateEntry = ['k', number] | ['d', number] | ['i', Array<FileEntry>]
|
||||
|
||||
@@ -96,8 +96,9 @@ export const useMainStore = defineStore('main', {
|
||||
space: {
|
||||
disk: 0,
|
||||
free: 0,
|
||||
usage: 0,
|
||||
used: 0,
|
||||
storage: 0,
|
||||
allocated: 0,
|
||||
}
|
||||
}),
|
||||
persist: {
|
||||
@@ -118,13 +119,14 @@ export const useMainStore = defineStore('main', {
|
||||
updateRoot(root: FileEntry[]) {
|
||||
const docs = []
|
||||
let loc = [] as string[]
|
||||
for (const [level, name, key, mtime, size, isfile] of root) {
|
||||
for (const [level, name, key, mtime, size, allocated, isfile] of root) {
|
||||
loc = loc.slice(0, level - 1)
|
||||
docs.push(new Doc({
|
||||
name,
|
||||
loc: level ? loc.join('/') : '/',
|
||||
key,
|
||||
size,
|
||||
allocated,
|
||||
mtime,
|
||||
dir: !isfile,
|
||||
}))
|
||||
|
||||
Reference in New Issue
Block a user