Feature/library adopt #5
No files matched your search
+75
-1
@@ -126,6 +126,7 @@ async function loadLibraries() {
|
||||
<td>${l.language ? esc(languageName(l.language)) : "—"}</td>
|
||||
<td class="row">
|
||||
<button class="secondary" data-edit-lib="${l.id}">${t("common.edit")}</button>
|
||||
<button class="secondary" data-adopt-lib="${l.id}" title="${esc(t("adopt.buttonTitle"))}">${t("adopt.button")}</button>
|
||||
<button class="danger" data-del-lib="${l.id}">${t("common.delete")}</button>
|
||||
</td>
|
||||
</tr>`
|
||||
@@ -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 = `<span class="spinner"></span> ${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) => `<label>
|
||||
<input type="checkbox" checked data-adopt-path="${esc(item.path)}" />
|
||||
<span>${esc(item.name)} <span class="muted">${esc(item.rel_dir)}</span></span>
|
||||
<select>${options.map(([id, label]) =>
|
||||
`<option value="${id}"${id === item.suggested_request_id ? " selected" : ""}>${esc(label)}</option>`
|
||||
).join("")}</select>
|
||||
</label>`)
|
||||
.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 ----
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -379,6 +379,16 @@
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<dialog id="adopt-dialog">
|
||||
<h3 id="adopt-heading"></h3>
|
||||
<p class="muted" id="adopt-intro"></p>
|
||||
<div id="adopt-list" class="series-list"></div>
|
||||
<div class="row">
|
||||
<button type="button" id="adopt-cancel" class="secondary" data-i18n="common.cancel"></button>
|
||||
<button type="button" id="adopt-submit" data-i18n="adopt.submit"></button>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<div id="toast" hidden></div>
|
||||
<script src="/i18n.js"></script>
|
||||
<script src="/app.js"></script>
|
||||
|
||||
Reference in new issue
Block a user