108 lines
3.4 KiB
Vue
108 lines
3.4 KiB
Vue
<template>
|
|
<main class="library">
|
|
<header class="library-header">
|
|
<h1>Library</h1>
|
|
<button class="btn-primary" @click="scan" :disabled="scanning">
|
|
{{ scanning ? "Scanning..." : "Scan for PDFs" }}
|
|
</button>
|
|
</header>
|
|
|
|
<p class="error-msg" v-if="error">{{ error }}</p>
|
|
|
|
<p class="empty-state" v-if="!loading && docs.length === 0">
|
|
No books indexed yet. Click "Scan for PDFs" to discover PDFs in your books directory.<br>
|
|
Make sure your PDF directory is mounted at <code>/books</code> inside the container.
|
|
</p>
|
|
|
|
<div class="doc-grid" v-else>
|
|
<DocumentCard
|
|
v-for="doc in docs"
|
|
:key="doc.id"
|
|
:doc="doc"
|
|
@reingest="reingest"
|
|
@delete="remove"
|
|
@refresh="load"
|
|
/>
|
|
</div>
|
|
|
|
<p class="scan-result" v-if="scanResult">
|
|
Found {{ scanResult.discovered }} PDFs, queued {{ scanResult.queued }} for indexing.
|
|
</p>
|
|
</main>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { onMounted, ref } from "vue"
|
|
import { api, type Document } from "@/api"
|
|
import DocumentCard from "@/components/DocumentCard.vue"
|
|
|
|
const docs = ref<Document[]>([])
|
|
const loading = ref(true)
|
|
const scanning = ref(false)
|
|
const error = ref<string | null>(null)
|
|
const scanResult = ref<{ discovered: number; queued: number } | null>(null)
|
|
|
|
async function load() {
|
|
loading.value = true
|
|
error.value = null
|
|
try {
|
|
docs.value = await api.getLibrary()
|
|
} catch (e) {
|
|
error.value = e instanceof Error ? e.message : "Failed to load library"
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
async function scan() {
|
|
scanning.value = true
|
|
error.value = null
|
|
try {
|
|
scanResult.value = await api.scanLibrary()
|
|
await load()
|
|
} catch (e) {
|
|
error.value = e instanceof Error ? e.message : "Scan failed"
|
|
} finally {
|
|
scanning.value = false
|
|
}
|
|
}
|
|
|
|
async function reingest(id: string) {
|
|
error.value = null
|
|
try {
|
|
await api.reingestDocument(id)
|
|
await load()
|
|
} catch (e) {
|
|
error.value = e instanceof Error ? e.message : "Re-index failed"
|
|
}
|
|
}
|
|
|
|
async function remove(id: string) {
|
|
if (!confirm("Remove this book from the library? The PDF file is not deleted.")) return
|
|
error.value = null
|
|
try {
|
|
await api.deleteDocument(id)
|
|
await load()
|
|
} catch (e) {
|
|
error.value = e instanceof Error ? e.message : "Remove failed"
|
|
}
|
|
}
|
|
|
|
onMounted(load)
|
|
</script>
|
|
|
|
<style scoped>
|
|
.library { padding: 1.5rem; max-width: 1200px; margin: 0 auto; }
|
|
.library-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 1.5rem; flex-wrap: wrap; gap: 1rem; }
|
|
h1 { font-size: 1.5rem; }
|
|
.btn-primary {
|
|
background: var(--color-accent); color: #fff; border: none; padding: 0.6rem 1.2rem;
|
|
border-radius: var(--radius-sm); cursor: pointer; font-size: 0.95rem;
|
|
}
|
|
.btn-primary:disabled { opacity: 0.5; cursor: default; }
|
|
.doc-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 1rem; }
|
|
.empty-state { color: var(--color-text-muted); line-height: 1.8; }
|
|
.empty-state code { font-family: var(--font-mono); background: var(--color-surface-alt); padding: 2px 6px; border-radius: 3px; }
|
|
.scan-result { margin-top: 1rem; color: var(--color-text-muted); font-size: 0.9rem; }
|
|
.error-msg { color: var(--color-error); margin-bottom: 1rem; font-size: 0.9rem; }
|
|
</style>
|