diff --git a/frontend/src/lib/i18n/locales/de.json b/frontend/src/lib/i18n/locales/de.json index 66b80150..fa79860f 100644 --- a/frontend/src/lib/i18n/locales/de.json +++ b/frontend/src/lib/i18n/locales/de.json @@ -605,6 +605,13 @@ "allSet": "Deine Bibliothek ist in großartiger Verfassung! Alle Bücher haben vollständige Metadaten.", "allSetFiltered": "Deine Bibliothek ist in großartiger Verfassung! Alle Bücher haben vollständige Metadaten für die ausgewählten Attribute.", "tableHeaderMissing": "Fehlend", + "actions": "Aktionen", + "openDetails": "Details öffnen", + "detailsShort": "Details", + "viewCover": "Cover ansehen", + "coverShort": "Cover", + "noCover": "Kein Cover", + "loadBookDetailsFailed": "Buchdetails konnten nicht geladen werden.", "remaining": "übrig", "andXMore": "...und {count} weitere" }, diff --git a/frontend/src/lib/i18n/locales/en.json b/frontend/src/lib/i18n/locales/en.json index cd1162ad..4575a935 100644 --- a/frontend/src/lib/i18n/locales/en.json +++ b/frontend/src/lib/i18n/locales/en.json @@ -605,6 +605,13 @@ "allSet": "Your library is in great shape! All books have complete metadata.", "allSetFiltered": "Your library is in great shape! All books have complete metadata for the selected attributes.", "tableHeaderMissing": "Missing", + "actions": "Actions", + "openDetails": "Open details", + "detailsShort": "Details", + "viewCover": "View cover", + "coverShort": "Cover", + "noCover": "No cover", + "loadBookDetailsFailed": "Failed to load book details.", "remaining": "remaining", "andXMore": "...and {count} more" }, diff --git a/frontend/src/lib/i18n/locales/es.json b/frontend/src/lib/i18n/locales/es.json index d51595c3..24c6d705 100644 --- a/frontend/src/lib/i18n/locales/es.json +++ b/frontend/src/lib/i18n/locales/es.json @@ -605,6 +605,13 @@ "allSet": "¡Tu biblioteca está en excelente estado! Todos los libros tienen metadatos completos.", "allSetFiltered": "¡Tu biblioteca está en excelente estado! Todos los libros tienen metadatos completos para los atributos seleccionados.", "tableHeaderMissing": "Faltante", + "actions": "Acciones", + "openDetails": "Abrir detalles", + "detailsShort": "Detalles", + "viewCover": "Ver portada", + "coverShort": "Portada", + "noCover": "Sin portada", + "loadBookDetailsFailed": "No se pudieron cargar los detalles del libro.", "remaining": "restantes", "andXMore": "...y {count} más" }, diff --git a/frontend/src/lib/i18n/locales/fr.json b/frontend/src/lib/i18n/locales/fr.json index cfbc08c5..6cdd3610 100644 --- a/frontend/src/lib/i18n/locales/fr.json +++ b/frontend/src/lib/i18n/locales/fr.json @@ -605,6 +605,13 @@ "allSet": "Ta bibliothèque est en pleine forme ! Tous les livres ont des métadonnées complètes.", "allSetFiltered": "Ta bibliothèque est en pleine forme ! Tous les livres ont des métadonnées complètes pour les attributs sélectionnés.", "tableHeaderMissing": "Manquant", + "actions": "Actions", + "openDetails": "Ouvrir les détails", + "detailsShort": "Détails", + "viewCover": "Voir la couverture", + "coverShort": "Couverture", + "noCover": "Pas de couverture", + "loadBookDetailsFailed": "Impossible de charger les détails du livre.", "remaining": "restants", "andXMore": "...et {count} autres" }, diff --git a/frontend/src/lib/i18n/locales/zh.json b/frontend/src/lib/i18n/locales/zh.json index 6b3a8809..d47d6c47 100644 --- a/frontend/src/lib/i18n/locales/zh.json +++ b/frontend/src/lib/i18n/locales/zh.json @@ -605,6 +605,13 @@ "allSet": "你的书库状态良好!所有图书元数据完整。", "allSetFiltered": "你的书库状态良好!所选属性的所有图书元数据完整。", "tableHeaderMissing": "缺失", + "actions": "操作", + "openDetails": "打开详情", + "detailsShort": "详情", + "viewCover": "查看封面", + "coverShort": "封面", + "noCover": "无封面", + "loadBookDetailsFailed": "加载图书详情失败。", "remaining": "剩余", "andXMore": "...还有 {count} 个" }, diff --git a/frontend/src/routes/data-hygiene/+page.svelte b/frontend/src/routes/data-hygiene/+page.svelte index 6c85d276..31e7dfe8 100644 --- a/frontend/src/routes/data-hygiene/+page.svelte +++ b/frontend/src/routes/data-hygiene/+page.svelte @@ -5,8 +5,10 @@ import { toasts } from '$lib/toasts'; import { localizeError } from '$lib/errors'; import Alert from '$lib/components/Alert.svelte'; - import { LoaderCircle } from '@lucide/svelte'; - import type { HygieneAttribute, HygieneMissingBook } from '$lib/types'; + import BookDetailDialog from '$lib/components/BookDetailDialog.svelte'; + import BookDrawer from '$lib/components/BookDrawer.svelte'; + import { LoaderCircle, X } from '@lucide/svelte'; + import type { Book, HygieneAttribute, HygieneMissingBook } from '$lib/types'; const ATTRIBUTES: { key: HygieneAttribute; labelKey: string }[] = [ { key: 'author', labelKey: 'dataHygiene.attributes.author' }, @@ -32,10 +34,16 @@ let selectedBookIds = $state>(new Set()); let batchField = $state(null); let batchValue = $state(''); + let batchFieldWasAutoSelected = $state(false); let showBatchConfirm = $state(false); let batchUpdating = $state(false); let dataLoaded = $state(false); let hasMore = $state(false); + let selectedBook = $state(null); + let detailOpen = $state(false); + let drawerOpen = $state(false); + let detailLoadingBookId = $state(null); + let coverViewer = $state<{ title: string; coverUrl: string } | null>(null); const effectiveAttributes = $derived( selectedAttributes.length > 0 ? selectedAttributes : ATTRIBUTES.map(a => a.key) @@ -163,6 +171,51 @@ void loadData(false); } + async function openBookDetails(book: HygieneMissingBook) { + detailLoadingBookId = book.id; + try { + selectedBook = await api.books.get(book.id); + detailOpen = true; + drawerOpen = false; + } catch (e: unknown) { + toasts.add(localizeError(e, $_, $_('dataHygiene.loadBookDetailsFailed')), 'error'); + } finally { + detailLoadingBookId = null; + } + } + + function openEditFromDetail(book: Book) { + selectedBook = book; + detailOpen = false; + drawerOpen = true; + } + + async function handleSave(updated: Book) { + selectedBook = updated; + detailOpen = false; + drawerOpen = false; + await loadData(true); + } + + function handleDelete(id: number) { + detailOpen = false; + drawerOpen = false; + selectedBookIds = new Set([...selectedBookIds].filter(bookId => bookId !== id)); + void loadData(true); + } + + function openCoverViewer(book: HygieneMissingBook) { + if (!book.cover_url) return; + coverViewer = { + title: book.title, + coverUrl: book.cover_url, + }; + } + + function closeCoverViewer() { + coverViewer = null; + } + const allComplete = $derived( dataLoaded && total === 0 && Object.values(totalMissingPerAttribute).every(c => c === 0) ); @@ -177,6 +230,25 @@ } return [...set]; }); + + $effect(() => { + if (missingAttrsOfSelected.length === 1) { + batchField = missingAttrsOfSelected[0]; + batchFieldWasAutoSelected = true; + return; + } + + if (missingAttrsOfSelected.length > 1 && batchFieldWasAutoSelected) { + batchField = null; + batchFieldWasAutoSelected = false; + return; + } + + if (batchField && !missingAttrsOfSelected.includes(batchField)) { + batchField = null; + batchFieldWasAutoSelected = false; + } + });
@@ -242,7 +314,7 @@
{$_('dataHygiene.sectionResults')}
-
+
@@ -256,10 +328,11 @@ /> - - - + + + + @@ -277,9 +350,9 @@ - - - + + + + {/each} @@ -323,8 +431,9 @@
{$_('book.title')} {$_('dataHygiene.tableHeaderMissing')}{$_('dataHygiene.actions')}
{book.title}
{#each book.missing_attributes as attr} @@ -287,6 +360,41 @@ {/each}
+
+ + +
+