doocus update and meeting list export
This commit is contained in:
@@ -1,36 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import JSZip from 'jszip'
|
||||
import {
|
||||
store, TARGETS, target, packageEstimate, humanBytes,
|
||||
} from '@/store'
|
||||
import { store, TARGETS, target, packageEstimate, humanBytes } from '@/store'
|
||||
|
||||
const building = ref(false)
|
||||
const error = ref('')
|
||||
|
||||
// Build a zip: per selected doc, a folder holding the original source file plus
|
||||
// the textified content.md and meta.json. Originals are included because the
|
||||
// permitted services (Gemini web / NotebookLM) receive them directly; content.md
|
||||
// keeps each doc greppable and small.
|
||||
// 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 id of store.selected) {
|
||||
const summary = store.docs.find((d) => d.id === id)
|
||||
const res = await fetch(`/api/docs/${encodeURIComponent(id)}`)
|
||||
if (!res.ok) throw new Error(`fetch ${id} failed`)
|
||||
const detail = await res.json()
|
||||
const folder = zip.folder(id.replace(/[\\/]+/g, '__')) ?? zip
|
||||
folder.file('content.md', detail.content ?? '')
|
||||
folder.file('meta.json', JSON.stringify(detail.meta ?? {}, null, 2))
|
||||
if (detail.originalUrl) {
|
||||
const bin = await fetch(detail.originalUrl)
|
||||
if (bin.ok) {
|
||||
const name = (summary?.name) || 'original'
|
||||
folder.file(name, await bin.blob())
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,10 +42,6 @@ async function buildPackage(): Promise<void> {
|
||||
building.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function clearSelection(): void {
|
||||
store.selected.clear()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -63,17 +54,12 @@ function clearSelection(): void {
|
||||
</label>
|
||||
|
||||
<div class="estimate" :class="{ over: packageEstimate.overFiles || packageEstimate.overBytes }">
|
||||
{{ packageEstimate.docCount }} docs ·
|
||||
<span :class="{ bad: packageEstimate.overFiles }">
|
||||
{{ packageEstimate.files }}/{{ target.maxFiles }} files
|
||||
</span>
|
||||
·
|
||||
<span :class="{ bad: packageEstimate.overBytes }">
|
||||
{{ humanBytes(packageEstimate.bytes) }} / {{ humanBytes(target.maxBytes) }}
|
||||
</span>
|
||||
{{ 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="clearSelection">Clear</button>
|
||||
<button :disabled="!store.selected.size" @click="store.selected.clear()">Clear</button>
|
||||
<button :disabled="!store.selected.size || building" @click="buildPackage">
|
||||
{{ building ? 'Building…' : 'Download package' }}
|
||||
</button>
|
||||
@@ -82,29 +68,10 @@ function clearSelection(): void {
|
||||
</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);
|
||||
}
|
||||
.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>
|
||||
|
||||
Reference in New Issue
Block a user