Files
meetus/ui/doocus-app/src/components/ExportBar.vue
2026-07-05 11:15:57 -03:00

78 lines
3.0 KiB
Vue

<script setup lang="ts">
import { ref } from 'vue'
import JSZip from 'jszip'
import { store, TARGETS, target, packageEstimate, humanBytes } from '@/store'
const building = ref(false)
const error = ref('')
// Build a zip of the selected files: each ORIGINAL (what Gemini/NotebookLM
// actually consume) plus, for extracted formats, the content.md search text.
// Folder structure is preserved. Later this can emit Drive links instead of
// bytes once nodes carry a `url`.
async function buildPackage(): Promise<void> {
if (!store.selected.size) return
building.value = true
error.value = ''
try {
const zip = new JSZip()
for (const f of store.files.filter((x) => store.selected.has(x.path))) {
const dir = f.path.includes('/') ? f.path.slice(0, f.path.lastIndexOf('/')) : ''
const folder = dir ? (zip.folder(dir) ?? zip) : zip
const bin = await fetch(`/api/original?path=${encodeURIComponent(f.path)}`)
if (bin.ok) folder.file(f.name, await bin.blob())
if (f.mode === 'extracted') {
const detail = await fetch(`/api/detail?path=${encodeURIComponent(f.path)}`)
if (detail.ok) {
const d = await detail.json()
if (d.content) folder.file(`${f.name}.txt`, d.content)
}
}
}
const blob = await zip.generateAsync({ type: 'blob' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `doocus-package-${target.value.key}.zip`
a.click()
URL.revokeObjectURL(url)
} catch (e) {
error.value = String(e)
} finally {
building.value = false
}
}
</script>
<template>
<div class="export-bar">
<label class="target">
Target
<select v-model="store.targetKey">
<option v-for="t in TARGETS" :key="t.key" :value="t.key">{{ t.label }}</option>
</select>
</label>
<div class="estimate" :class="{ over: packageEstimate.overFiles || packageEstimate.overBytes }">
{{ packageEstimate.docCount }} sel ·
<span :class="{ bad: packageEstimate.overFiles }">{{ packageEstimate.files }}/{{ target.maxFiles }} files</span> ·
<span :class="{ bad: packageEstimate.overBytes }">{{ humanBytes(packageEstimate.bytes) }} / {{ humanBytes(target.maxBytes) }}</span>
</div>
<button :disabled="!store.selected.size" @click="store.selected.clear()">Clear</button>
<button :disabled="!store.selected.size || building" @click="buildPackage">
{{ building ? 'Building' : 'Download package' }}
</button>
<span v-if="error" class="err">{{ error }}</span>
</div>
</template>
<style scoped>
.export-bar { display: flex; align-items: center; gap: var(--space-3); font-size: var(--font-size-sm); }
.target { display: flex; align-items: center; gap: var(--space-1); color: var(--text-secondary); }
.estimate { color: var(--text-secondary); }
.estimate.over { color: var(--status-error, #e0a030); }
.estimate .bad { color: var(--status-error, #e0a030); font-weight: 600; }
.err { color: var(--status-error, #e0a030); }
</style>