feat(ui): sort request lists and move selected requests to another library
This commit is contained in:
1 parent
07c988c443
commit
46b88e2fd5
3 files changed
+74
-8
No files matched your search
+47
-8
@@ -84,10 +84,10 @@ function libOptions(mediaType) {
|
||||
// ---- missing badge ----
|
||||
async function refreshMissingBadge() {
|
||||
try {
|
||||
const reqs = await api("/api/requests?status=missing");
|
||||
const { count } = await api("/api/requests/count?status=missing");
|
||||
const badge = $("#missing-badge");
|
||||
badge.textContent = reqs.length;
|
||||
badge.hidden = reqs.length === 0;
|
||||
badge.textContent = count;
|
||||
badge.hidden = count === 0;
|
||||
} catch {}
|
||||
}
|
||||
|
||||
@@ -519,6 +519,22 @@ function requestMatchesFilter(r, text) {
|
||||
return hay.includes(text.toLowerCase());
|
||||
}
|
||||
|
||||
// the server already sends newest first
|
||||
const SORTS = {
|
||||
title: (a, b) => a.title.localeCompare(b.title, undefined, { numeric: true }),
|
||||
author: (a, b) => a.authors.localeCompare(b.authors) || SORTS.title(a, b),
|
||||
series: (a, b) => (a.series || a.title).localeCompare(b.series || b.title)
|
||||
|| (a.volume ?? 0) - (b.volume ?? 0) || SORTS.title(a, b),
|
||||
};
|
||||
for (const status of ["missing", "imported"]) {
|
||||
const sel = $(`#${status}-sort`);
|
||||
try { sel.value = localStorage.getItem("wordarr.sort." + status) || "newest"; } catch {}
|
||||
sel.addEventListener("change", () => {
|
||||
try { localStorage.setItem("wordarr.sort." + status, sel.value); } catch {}
|
||||
renderRequests(status);
|
||||
});
|
||||
}
|
||||
|
||||
async function loadRequests(status) {
|
||||
const params = new URLSearchParams({ status });
|
||||
if (status === "missing") {
|
||||
@@ -543,11 +559,15 @@ async function loadRequests(status) {
|
||||
function renderRequests(status) {
|
||||
const text = status === "missing" ? $("#missing-search").value : $("#imported-search").value;
|
||||
const reqs = currentRequests[status].filter((r) => requestMatchesFilter(r, text));
|
||||
const sort = SORTS[$(`#${status}-sort`).value];
|
||||
if (sort) reqs.sort(sort);
|
||||
const box = status === "missing" ? $("#missing-list") : $("#imported-list");
|
||||
const bulkBtn = status === "missing" ? $("#missing-bulk-delete") : $("#imported-bulk-delete");
|
||||
const moveSel = status === "missing" ? $("#missing-move-library") : null;
|
||||
if (!reqs.length) {
|
||||
box.innerHTML = `<p class='muted'>${t("common.nothingHere")}</p>`;
|
||||
bulkBtn.hidden = true;
|
||||
if (moveSel) moveSel.hidden = true;
|
||||
return;
|
||||
}
|
||||
box.innerHTML = reqs
|
||||
@@ -582,6 +602,10 @@ function renderRequests(status) {
|
||||
const n = box.querySelectorAll("[data-sel-req]:checked").length;
|
||||
bulkBtn.hidden = n === 0;
|
||||
bulkBtn.textContent = t("common.removeSelectedN", { n });
|
||||
if (!moveSel) return;
|
||||
moveSel.hidden = n === 0;
|
||||
moveSel.innerHTML = `<option value="">${esc(t("list.moveTo", { n }))}</option>` +
|
||||
libraries.map((l) => `<option value="${l.id}">${esc(l.name)} (${esc(l.media_type)})</option>`).join("");
|
||||
};
|
||||
box.querySelectorAll("[data-sel-req]").forEach((c) =>
|
||||
c.addEventListener("change", updateBulkBtn)
|
||||
@@ -596,14 +620,29 @@ async function bulkDeleteRequests(status) {
|
||||
const hint = t(status === "imported" ? "list.confirmDeleteImported" : "list.confirmDeleteRequests",
|
||||
{ n: ids.length });
|
||||
if (!confirm(hint)) return;
|
||||
for (const id of ids) {
|
||||
try { await api("/api/requests/" + id, { method: "DELETE" }); }
|
||||
catch (err) { toast(err.message, true); }
|
||||
}
|
||||
toast(t("list.deletedN", { n: ids.length }));
|
||||
try {
|
||||
const res = await api("/api/requests/bulk", {
|
||||
method: "DELETE", body: JSON.stringify({ ids: ids.map(Number) }),
|
||||
});
|
||||
toast(t("list.deletedN", { n: res.deleted }));
|
||||
} catch (err) { toast(err.message, true); }
|
||||
loadRequests(status);
|
||||
}
|
||||
|
||||
$("#missing-move-library").addEventListener("change", async (ev) => {
|
||||
const lib = libraries.find((l) => String(l.id) === ev.target.value);
|
||||
if (!lib) return;
|
||||
const ids = [...$("#missing-list").querySelectorAll("[data-sel-req]:checked")]
|
||||
.map((c) => Number(c.dataset.selReq));
|
||||
try {
|
||||
const res = await api("/api/requests/bulk", {
|
||||
method: "PUT", body: JSON.stringify({ ids, library_id: lib.id }),
|
||||
});
|
||||
toast(t("list.movedN", { n: res.updated, library: lib.name }));
|
||||
} catch (err) { toast(err.message, true); }
|
||||
loadRequests("missing");
|
||||
});
|
||||
|
||||
$("#imported-bulk-delete").addEventListener("click", () => bulkDeleteRequests("imported"));
|
||||
|
||||
function selectAllRequests(status) {
|
||||
|
||||
@@ -106,6 +106,13 @@ const STRINGS = {
|
||||
"list.confirmDeleteRequests": "{n} Anfrage(n) entfernen?",
|
||||
"list.confirmDeleteImported": "{n} Eintrag/Einträge entfernen? Die importierten Dateien in der Library bleiben erhalten.",
|
||||
"list.deletedN": "{n} entfernt",
|
||||
"list.sort": "Sortierung",
|
||||
"list.sortNewest": "Neueste zuerst",
|
||||
"list.sortTitle": "Titel",
|
||||
"list.sortAuthor": "Autor",
|
||||
"list.sortSeries": "Serie und Band",
|
||||
"list.moveTo": "Bibliothek ändern ({n})…",
|
||||
"list.movedN": "{n} nach {library} verschoben",
|
||||
|
||||
"detail.statusMissing": "fehlt noch",
|
||||
"detail.statusImported": "importiert",
|
||||
@@ -339,6 +346,13 @@ const STRINGS = {
|
||||
"list.confirmDeleteRequests": "Remove {n} request(s)?",
|
||||
"list.confirmDeleteImported": "Remove {n} entr(y/ies)? The imported files stay in the library.",
|
||||
"list.deletedN": "{n} removed",
|
||||
"list.sort": "Sort order",
|
||||
"list.sortNewest": "Newest first",
|
||||
"list.sortTitle": "Title",
|
||||
"list.sortAuthor": "Author",
|
||||
"list.sortSeries": "Series and volume",
|
||||
"list.moveTo": "Change library ({n})…",
|
||||
"list.movedN": "{n} moved to {library}",
|
||||
|
||||
"detail.statusMissing": "still missing",
|
||||
"detail.statusImported": "imported",
|
||||
|
||||
@@ -110,7 +110,14 @@
|
||||
<option value="" data-i18n="list.allLibraries"></option>
|
||||
</select>
|
||||
<input id="missing-search" data-i18n-placeholder="list.filterPlaceholder" />
|
||||
<select id="missing-sort" data-i18n-title="list.sort">
|
||||
<option value="newest" data-i18n="list.sortNewest"></option>
|
||||
<option value="title" data-i18n="list.sortTitle"></option>
|
||||
<option value="author" data-i18n="list.sortAuthor"></option>
|
||||
<option value="series" data-i18n="list.sortSeries"></option>
|
||||
</select>
|
||||
<button id="missing-select-all" class="secondary" data-i18n="common.selectAll"></button>
|
||||
<select id="missing-move-library" hidden></select>
|
||||
<button id="missing-bulk-delete" class="danger" hidden data-i18n="common.removeSelected"></button>
|
||||
<div class="view-toggle" role="group" data-i18n-aria="list.view">
|
||||
<button data-viewmode="list" data-i18n-title="list.viewList">☰</button>
|
||||
@@ -124,6 +131,12 @@
|
||||
<section id="view-imported" hidden>
|
||||
<div class="bar wrap">
|
||||
<input id="imported-search" data-i18n-placeholder="list.filterPlaceholder" />
|
||||
<select id="imported-sort" data-i18n-title="list.sort">
|
||||
<option value="newest" data-i18n="list.sortNewest"></option>
|
||||
<option value="title" data-i18n="list.sortTitle"></option>
|
||||
<option value="author" data-i18n="list.sortAuthor"></option>
|
||||
<option value="series" data-i18n="list.sortSeries"></option>
|
||||
</select>
|
||||
<button id="imported-select-all" class="secondary" data-i18n="common.selectAll"></button>
|
||||
<button id="imported-bulk-delete" class="danger" hidden data-i18n="common.removeSelected"></button>
|
||||
<div class="view-toggle" role="group" data-i18n-aria="list.view">
|
||||
|
||||
Reference in new issue
Block a user