Use the parent clip last frame and new-segment join for extensions, not the stitched file's first frame. Show this account's library storage in Settings.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -5,11 +5,24 @@
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<h2 class="font-display text-2xl font-bold">Settings</h2>
|
||||
<p class="mt-1 text-sm text-zinc-400">Library folders, passwords, and prompt-helper instruction sets for this account.</p>
|
||||
<p class="mt-1 text-sm text-zinc-400">Library storage, folders, passwords, and prompt-helper instruction sets for this account.</p>
|
||||
</div>
|
||||
<button type="button" class="rounded-full border border-white/10 px-3 py-1 text-sm text-zinc-400 hover:text-white" @click="emit('close')">Close</button>
|
||||
</div>
|
||||
|
||||
<section class="mt-6">
|
||||
<h3 class="text-sm font-semibold uppercase tracking-wider text-zinc-500">Storage</h3>
|
||||
<div class="mt-2 rounded-2xl border border-white/10 bg-zinc-950/50 px-4 py-3">
|
||||
<p v-if="storageLoading" class="text-sm text-zinc-500">Measuring…</p>
|
||||
<p v-else-if="storageError" class="text-sm text-zinc-500">{{ storageError }}</p>
|
||||
<template v-else-if="storage">
|
||||
<p class="text-sm text-zinc-200">{{ storageLine }}</p>
|
||||
<p v-if="storageBreakdown" class="mt-1 text-xs text-zinc-500">{{ storageBreakdown }}</p>
|
||||
<p class="mt-1 text-xs text-zinc-600">{{ storageNote }}</p>
|
||||
</template>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="authMode === 'password'" class="mt-6 space-y-3">
|
||||
<h3 class="text-sm font-semibold uppercase tracking-wider text-zinc-500">Site login</h3>
|
||||
<p class="text-sm text-zinc-400">Username and password are stored encrypted on this server. Changing them does not move the library.</p>
|
||||
@@ -207,6 +220,56 @@ const loginUser = ref(props.username || '')
|
||||
const loginCurrent = ref('')
|
||||
const loginNext = ref('')
|
||||
const drafts = reactive<Record<string, { name: string; protect: boolean; password: string; currentPassword: string }>>({})
|
||||
const storageLoading = ref(true)
|
||||
const storageError = ref('')
|
||||
const storage = ref<{
|
||||
libraryBytes: number
|
||||
breakdown: { clips: number; stills: number; tmp: number; other: number }
|
||||
volume: { ok: boolean; totalBytes: number | null; usedBytes: number | null; freeBytes: number | null; overlay: boolean }
|
||||
} | null>(null)
|
||||
|
||||
function formatBytes(bytes: number) {
|
||||
const n = Math.max(0, Number(bytes) || 0)
|
||||
if (n < 1024) return `${Math.round(n)} B`
|
||||
const units = ['KB', 'MB', 'GB', 'TB']
|
||||
let value = n / 1024
|
||||
let unit = 0
|
||||
while (value >= 1024 && unit < units.length - 1) {
|
||||
value /= 1024
|
||||
unit += 1
|
||||
}
|
||||
const digits = value >= 10 || units[unit] === 'KB' ? 0 : 1
|
||||
return `${value.toFixed(digits)} ${units[unit]}`
|
||||
}
|
||||
|
||||
const storageLine = computed(() => {
|
||||
if (!storage.value) return ''
|
||||
const library = `Library ${formatBytes(storage.value.libraryBytes)}`
|
||||
const volume = storage.value.volume
|
||||
if (!volume.ok || volume.freeBytes == null || volume.totalBytes == null) {
|
||||
return `${library} · free space unavailable`
|
||||
}
|
||||
const free = `${formatBytes(volume.freeBytes)} free of ${formatBytes(volume.totalBytes)}`
|
||||
return volume.overlay ? `${library} · ${free} on the container disk` : `${library} · ${free}`
|
||||
})
|
||||
|
||||
const storageBreakdown = computed(() => {
|
||||
if (!storage.value) return ''
|
||||
const { clips, stills, tmp } = storage.value.breakdown
|
||||
const parts = [
|
||||
clips ? `Clips ${formatBytes(clips)}` : '',
|
||||
stills ? `Stills ${formatBytes(stills)}` : '',
|
||||
tmp ? `Temp ${formatBytes(tmp)}` : ''
|
||||
].filter(Boolean)
|
||||
return parts.join(' · ')
|
||||
})
|
||||
|
||||
const storageNote = computed(() => {
|
||||
if (!storage.value) return ''
|
||||
return storage.value.volume.overlay
|
||||
? "This account's clips, stills, thumbs, and temp. Free space is the container disk, not a mounted volume."
|
||||
: "This account's clips, stills, thumbs, and temp. Volume free space is shared."
|
||||
})
|
||||
|
||||
function onKey(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape') emit('close')
|
||||
@@ -215,6 +278,7 @@ function onKey(event: KeyboardEvent) {
|
||||
onMounted(() => {
|
||||
window.addEventListener('keydown', onKey)
|
||||
void loadPresets()
|
||||
void loadStorage()
|
||||
})
|
||||
onBeforeUnmount(() => window.removeEventListener('keydown', onKey))
|
||||
|
||||
@@ -280,6 +344,19 @@ async function loadPresets() {
|
||||
promptPresets.value = data.presets || []
|
||||
}
|
||||
|
||||
async function loadStorage() {
|
||||
storageLoading.value = true
|
||||
storageError.value = ''
|
||||
try {
|
||||
storage.value = await $fetch('/api/settings/storage')
|
||||
} catch (error: any) {
|
||||
storage.value = null
|
||||
storageError.value = error?.data?.statusMessage || error?.statusMessage || 'Could not read storage'
|
||||
} finally {
|
||||
storageLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function addPreset() {
|
||||
const name = newPresetName.value.trim()
|
||||
const systemPrompt = newPresetPrompt.value.trim()
|
||||
|
||||
Reference in New Issue
Block a user