From d0b12d5d0974a49924b20ef4d2ee1c543cab84e1 Mon Sep 17 00:00:00 2001 From: Steppenstreuner Date: Sun, 25 Oct 2026 19:55:22 +0100 Subject: [PATCH] feat(ui): scan a library and link its files to open requests --- static/app.js | 76 ++++++++++++++++++++++++++++++++++++++++++++++- static/i18n.js | 16 ++++++++++ static/index.html | 10 +++++++ 3 files changed, 101 insertions(+), 1 deletion(-) diff --git a/static/app.js b/static/app.js index 7bcadde..3dbe386 100644 --- a/static/app.js +++ b/static/app.js @@ -126,6 +126,7 @@ async function loadLibraries() { ${l.language ? esc(languageName(l.language)) : "—"} + ` @@ -134,6 +135,9 @@ async function loadLibraries() { tbody.querySelectorAll("[data-edit-lib]").forEach((b) => b.addEventListener("click", () => startLibraryEdit(parseInt(b.dataset.editLib))) ); + tbody.querySelectorAll("[data-adopt-lib]").forEach((b) => + b.addEventListener("click", () => openAdopt(parseInt(b.dataset.adoptLib))) + ); tbody.querySelectorAll("[data-del-lib]").forEach((b) => b.addEventListener("click", async () => { if (!confirm(t("lib.confirmDelete"))) return; @@ -664,7 +668,7 @@ $("#missing-bulk-delete").addEventListener("click", () => bulkDeleteRequests("mi // ---- activity ---- let historyRows = []; -const HISTORY_STATE = { imported: "done", appended: "done", failed: "failed" }; +const HISTORY_STATE = { imported: "done", appended: "done", adopted: "done", failed: "failed" }; async function loadHistory() { try { @@ -1818,6 +1822,75 @@ function setImportProgress(done, total) { `${done}/${total} · ${Math.round((done / total) * 100)} %`; } +// ---- adopt files already in a library ---- +let adoptLib = null; +let adoptScan = null; + +async function openAdopt(id) { + adoptLib = libraries.find((l) => l.id === id); + adoptScan = null; + $("#adopt-heading").textContent = t("adopt.heading", { name: adoptLib.name }); + $("#adopt-intro").innerHTML = ` ${t("import.scanning")}`; + $("#adopt-list").innerHTML = ""; + $("#adopt-submit").disabled = true; + $("#adopt-dialog").showModal(); + try { + adoptScan = await api("/api/import/library/" + id); + } catch (err) { + $("#adopt-intro").textContent = ""; + toast(err.message, true); + return; + } + renderAdopt(); +} + +function renderAdopt() { + if (!adoptScan) return; + // only files with a suggestion: a select per unmatched file would bury them + const matched = adoptScan.items.filter((i) => i.suggested_request_id); + $("#adopt-intro").textContent = t("adopt.intro", { + n: matched.length, rest: adoptScan.items.length - matched.length, + }); + const options = adoptScan.requests + .map((r) => [r.id, r.authors ? `${r.authors}: ${r.title}` : r.title]); + $("#adopt-list").innerHTML = matched + .map((item) => ``) + .join(""); + $("#adopt-submit").disabled = matched.length === 0; +} + +$("#adopt-cancel").addEventListener("click", () => $("#adopt-dialog").close()); + +$("#adopt-submit").addEventListener("click", async () => { + const items = [...$$("#adopt-list [data-adopt-path]:checked")].map((box) => ({ + path: box.dataset.adoptPath, + request_id: parseInt(box.closest("label").querySelector("select").value), + })); + if (!items.length) return; + $("#adopt-submit").disabled = true; + try { + const { results } = await api("/api/import/library/" + adoptLib.id, { + method: "POST", body: JSON.stringify({ items }), + }); + const failed = results.filter((r) => !r.ok); + toast(failed.length + ? t("adopt.partial", { n: results.length - failed.length, failed: failed.map((r) => `${r.path}: ${r.error}`).join("; ") }) + : t("adopt.done", { n: results.length }), failed.length > 0); + $("#adopt-dialog").close(); + refreshMissingBadge(); + } catch (err) { + toast(err.message, true); + } finally { + $("#adopt-submit").disabled = false; + } +}); + // ---- language ---- function updateLangButton() { $("#lang-toggle").textContent = lang === "de" ? "EN" : "DE"; @@ -1836,6 +1909,7 @@ function onLangChange() { const editId = $("#lib-form").dataset.editId; editId ? startLibraryEdit(parseInt(editId)) : endLibraryEdit(); if (scanItems.length) renderImportTable(); + renderAdopt(); } // ---- version ---- diff --git a/static/i18n.js b/static/i18n.js index 745278b..14bb8dc 100644 --- a/static/i18n.js +++ b/static/i18n.js @@ -56,6 +56,14 @@ const STRINGS = { "history.retagged": "neu getaggt", "history.deleted": "gelöscht", "history.failed": "fehlgeschlagen", + "history.adopted": "übernommen", + "adopt.button": "Einlesen", + "adopt.buttonTitle": "Dateien, die schon in der Library liegen, offenen Anfragen zuordnen", + "adopt.heading": "„{name}“ einlesen", + "adopt.intro": "{n} Dateien passen zu offenen Anfragen dieser Library und bleiben, wo sie sind. {rest} weitere ohne Treffer.", + "adopt.submit": "Übernehmen", + "adopt.done": "{n} übernommen", + "adopt.partial": "{n} übernommen, Fehler: {failed}", "search.languageTitle": "Sprache der Ausgabe — filtert Ebook- und Audiobook-Suche", "search.allLanguages": "Alle Sprachen", @@ -296,6 +304,14 @@ const STRINGS = { "history.retagged": "retagged", "history.deleted": "deleted", "history.failed": "failed", + "history.adopted": "adopted", + "adopt.button": "Scan", + "adopt.buttonTitle": "Link files already in the library to open requests", + "adopt.heading": "Scan “{name}”", + "adopt.intro": "{n} files match open requests of this library and stay where they are. {rest} more without a match.", + "adopt.submit": "Adopt", + "adopt.done": "{n} adopted", + "adopt.partial": "{n} adopted, errors: {failed}", "search.languageTitle": "Edition language — filters the ebook and audiobook search", "search.allLanguages": "All languages", diff --git a/static/index.html b/static/index.html index 73331d6..b92f1ca 100644 --- a/static/index.html +++ b/static/index.html @@ -379,6 +379,16 @@ + +

+

+
+
+ + +
+
+