Disk space pie chart.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div v-if="visible" class="cursor-tooltip" :style="tooltipStyle">
|
||||
<div v-if="visible" ref="tooltipEl" class="cursor-tooltip" :style="tooltipStyle">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</Teleport>
|
||||
@@ -25,6 +25,9 @@ const props = defineProps<{
|
||||
const visible = ref(false)
|
||||
const mouseX = ref(0)
|
||||
const mouseY = ref(0)
|
||||
const tooltipWidth = ref(0)
|
||||
const tooltipHeight = ref(0)
|
||||
const tooltipEl = ref<HTMLElement | null>(null)
|
||||
let settleTimer: ReturnType<typeof setTimeout> | null = null
|
||||
let lastMoveX = 0
|
||||
let lastMoveY = 0
|
||||
@@ -32,10 +35,32 @@ let lastMoveY = 0
|
||||
// Movement threshold (pixels) - cursor must settle within this radius
|
||||
const SETTLE_THRESHOLD = 8
|
||||
|
||||
const tooltipStyle = computed(() => ({
|
||||
left: `${mouseX.value}px`,
|
||||
top: `${mouseY.value}px`,
|
||||
}))
|
||||
const tooltipStyle = computed(() => {
|
||||
// Constrain to viewport
|
||||
const pad = 8
|
||||
let x = mouseX.value
|
||||
let y = mouseY.value
|
||||
|
||||
// Only constrain if we've measured the tooltip
|
||||
if (tooltipWidth.value > 0 && tooltipHeight.value > 0) {
|
||||
// Adjust horizontal position if tooltip would overflow right edge
|
||||
if (x + tooltipWidth.value + pad > window.innerWidth) {
|
||||
x = window.innerWidth - tooltipWidth.value - pad
|
||||
}
|
||||
// Adjust vertical position if tooltip would overflow bottom edge
|
||||
if (y + tooltipHeight.value + pad > window.innerHeight) {
|
||||
y = window.innerHeight - tooltipHeight.value - pad
|
||||
}
|
||||
// Don't go past left/top edges
|
||||
x = Math.max(pad, x)
|
||||
y = Math.max(pad, y)
|
||||
}
|
||||
|
||||
return {
|
||||
left: `${x}px`,
|
||||
top: `${y}px`,
|
||||
}
|
||||
})
|
||||
|
||||
// Track touch events globally to suppress touch-simulated mouse events
|
||||
const onTouchStart = () => { lastTouchTime = Date.now() }
|
||||
@@ -48,6 +73,13 @@ const isTouchEvent = () => Date.now() - lastTouchTime < 500
|
||||
const showTooltip = () => {
|
||||
visible.value = true
|
||||
globalActive = true
|
||||
// Measure tooltip after it renders
|
||||
requestAnimationFrame(() => {
|
||||
if (tooltipEl.value) {
|
||||
tooltipWidth.value = tooltipEl.value.offsetWidth
|
||||
tooltipHeight.value = tooltipEl.value.offsetHeight
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const scheduleTooltip = () => {
|
||||
@@ -131,7 +163,6 @@ defineExpose({
|
||||
backdrop-filter: blur(4px);
|
||||
-webkit-backdrop-filter: blur(4px);
|
||||
color: #fff;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,40 @@
|
||||
<span v-if="!query" class="search-hint" @click="focusSearch">/</span>
|
||||
</div>
|
||||
<div class="spacer smallgap"></div>
|
||||
<div v-if="store.space.disk" class="disk-space"
|
||||
@mouseenter="diskTooltip?.startHover"
|
||||
@mousemove="diskTooltip?.updatePosition"
|
||||
@mouseleave="diskTooltip?.endHover"
|
||||
>
|
||||
<svg viewBox="0 0 32 32" class="pie-mini">
|
||||
<!-- Base: 'other' usage (light purple - appears on left, before 12 o'clock) -->
|
||||
<circle r="16" cx="16" cy="16" fill="#c8e" />
|
||||
<!-- Middle ring: free space (dynamic color - appears at bottom) -->
|
||||
<circle r="8" cx="16" cy="16" fill="transparent" :stroke="freeColor" stroke-width="16" :stroke-dasharray="pieFree" :stroke-dashoffset="pieFreeOffset" transform="rotate(-90 16 16)" />
|
||||
<!-- Top ring: storage (deep purple - appears on right after 12 o'clock) -->
|
||||
<circle r="8" cx="16" cy="16" fill="transparent" stroke="#82d" stroke-width="16" :stroke-dasharray="pieStorage" transform="rotate(-90 16 16)" />
|
||||
<!-- Subtle inner circle for depth -->
|
||||
<circle r="2" cx="16" cy="16" fill="rgba(255,255,255,0.2)" />
|
||||
</svg>
|
||||
<CursorTooltip ref="diskTooltip" text="Disk space">
|
||||
<div class="disk-tooltip">
|
||||
<svg viewBox="0 0 160 80" width="160" height="80" class="pie-tooltip">
|
||||
<!-- Pie chart centered at 40,40 -->
|
||||
<circle r="32" cx="40" cy="40" fill="#c8e" />
|
||||
<circle r="16" cx="40" cy="40" fill="transparent" :stroke="freeColor" stroke-width="32" :stroke-dasharray="pieFreeLg" :stroke-dashoffset="pieFreeOffsetLg" transform="rotate(-90 40 40)" />
|
||||
<circle r="16" cx="40" cy="40" fill="transparent" stroke="#82d" stroke-width="32" :stroke-dasharray="pieStorageLg" transform="rotate(-90 40 40)" />
|
||||
<circle r="4" cx="40" cy="40" fill="rgba(255,255,255,0.25)" />
|
||||
<!-- Labels on the right -->
|
||||
<rect x="78" y="10" width="10" height="10" fill="#82d" rx="2"/>
|
||||
<text x="92" y="19" class="pie-label">{{ formatSize(store.space.storage) }} stored</text>
|
||||
<rect x="78" y="30" width="10" height="10" fill="#c8e" rx="2"/>
|
||||
<text x="92" y="39" class="pie-label">{{ formatSize(store.space.usage - store.space.storage) }} other</text>
|
||||
<rect x="78" y="50" width="10" height="10" :fill="freeColor" rx="2"/>
|
||||
<text x="92" y="59" class="pie-label">{{ formatSize(store.space.free) }} free</text>
|
||||
</svg>
|
||||
</div>
|
||||
</CursorTooltip>
|
||||
</div>
|
||||
<SvgButton name="cog" @click="settingsMenu" />
|
||||
</nav>
|
||||
</template>
|
||||
@@ -27,15 +61,72 @@
|
||||
<script setup lang="ts">
|
||||
import { useMainStore } from '@/stores/main'
|
||||
import { useSsoAuthStore } from '@/stores/ssoAuth'
|
||||
import { ref, nextTick, watchEffect } from 'vue'
|
||||
import { ref, nextTick, watchEffect, computed } from 'vue'
|
||||
import ContextMenu from '@imengyu/vue3-context-menu'
|
||||
import { showAuthIframe } from 'paskia'
|
||||
import { resumeWatching } from '@/repositories/WS'
|
||||
import router from '@/router';
|
||||
import { formatSize } from '@/utils'
|
||||
import CursorTooltip from './CursorTooltip.vue'
|
||||
|
||||
const store = useMainStore()
|
||||
const ssoStore = useSsoAuthStore()
|
||||
const search = ref<HTMLInputElement | null>()
|
||||
const diskTooltip = ref<InstanceType<typeof CursorTooltip> | null>(null)
|
||||
|
||||
const CIRC = 50.27 // 2π×8
|
||||
|
||||
// Storage segment (starts at top, -90°)
|
||||
const pieStorage = computed(() => {
|
||||
const s = store.space
|
||||
if (!s.disk) return `0 ${CIRC}`
|
||||
const pct = s.storage / s.disk
|
||||
return `${pct * CIRC} ${CIRC}`
|
||||
})
|
||||
|
||||
// Free segment (starts after storage, goes clockwise to bottom area)
|
||||
const pieFree = computed(() => {
|
||||
const s = store.space
|
||||
if (!s.disk) return `0 ${CIRC}`
|
||||
const pct = s.free / s.disk
|
||||
return `${pct * CIRC} ${CIRC}`
|
||||
})
|
||||
|
||||
const pieFreeOffset = computed(() => {
|
||||
const s = store.space
|
||||
if (!s.disk) return 0
|
||||
// Start after storage segment
|
||||
const storagePct = s.storage / s.disk
|
||||
return -storagePct * CIRC
|
||||
})
|
||||
|
||||
// Free space color: green when plenty, yellow when moderate, red when low
|
||||
const freeColor = computed(() => {
|
||||
const s = store.space
|
||||
if (!s.disk) return '#6c6'
|
||||
const freePct = s.free / s.disk
|
||||
if (freePct > 0.25) return '#5b5' // Green: > 25% free
|
||||
if (freePct > 0.10) return '#db3' // Yellow: 10-25% free
|
||||
return '#d44' // Red: < 10% free
|
||||
})
|
||||
|
||||
// Large pie for tooltip (circumference = 2π×16 ≈ 100.53)
|
||||
const CIRC_LG = 100.53
|
||||
const pieStorageLg = computed(() => {
|
||||
const s = store.space
|
||||
if (!s.disk) return `0 ${CIRC_LG}`
|
||||
return `${(s.storage / s.disk) * CIRC_LG} ${CIRC_LG}`
|
||||
})
|
||||
const pieFreeLg = computed(() => {
|
||||
const s = store.space
|
||||
if (!s.disk) return `0 ${CIRC_LG}`
|
||||
return `${(s.free / s.disk) * CIRC_LG} ${CIRC_LG}`
|
||||
})
|
||||
const pieFreeOffsetLg = computed(() => {
|
||||
const s = store.space
|
||||
if (!s.disk) return 0
|
||||
return -(s.storage / s.disk) * CIRC_LG
|
||||
})
|
||||
|
||||
const props = defineProps<{
|
||||
path: Array<string>
|
||||
@@ -189,4 +280,23 @@ defineExpose({
|
||||
cursor: pointer;
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
.disk-space {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: default;
|
||||
}
|
||||
.pie-mini {
|
||||
width: 1.4em;
|
||||
height: 1.4em;
|
||||
}
|
||||
.disk-tooltip {
|
||||
line-height: 1.5;
|
||||
}
|
||||
.pie-tooltip {
|
||||
display: block;
|
||||
}
|
||||
.pie-tooltip .pie-label {
|
||||
fill: #fff;
|
||||
font-size: 9px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user