78 lines
3.0 KiB
Vue
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>
|