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 @@
+
+