feat(ui): sort request lists and move selected requests to another library

This commit is contained in:
Steppenstreuner committed 2026-10-25 19:40:28 +01:00
1 parent 07c988c443
commit 46b88e2fd5
3 files changed
+74 -8

No files matched your search

+47 -8
View File
@@ -84,10 +84,10 @@ function libOptions(mediaType) {
// ---- missing badge ---- // ---- missing badge ----
async function refreshMissingBadge() { async function refreshMissingBadge() {
try { try {
const reqs = await api("/api/requests?status=missing"); const { count } = await api("/api/requests/count?status=missing");
const badge = $("#missing-badge"); const badge = $("#missing-badge");
badge.textContent = reqs.length; badge.textContent = count;
badge.hidden = reqs.length === 0; badge.hidden = count === 0;
} catch {} } catch {}
} }
@@ -519,6 +519,22 @@ function requestMatchesFilter(r, text) {
return hay.includes(text.toLowerCase()); 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) { async function loadRequests(status) {
const params = new URLSearchParams({ status }); const params = new URLSearchParams({ status });
if (status === "missing") { if (status === "missing") {
@@ -543,11 +559,15 @@ async function loadRequests(status) {
function renderRequests(status) { function renderRequests(status) {
const text = status === "missing" ? $("#missing-search").value : $("#imported-search").value; const text = status === "missing" ? $("#missing-search").value : $("#imported-search").value;
const reqs = currentRequests[status].filter((r) => requestMatchesFilter(r, text)); 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 box = status === "missing" ? $("#missing-list") : $("#imported-list");
const bulkBtn = status === "missing" ? $("#missing-bulk-delete") : $("#imported-bulk-delete"); const bulkBtn = status === "missing" ? $("#missing-bulk-delete") : $("#imported-bulk-delete");
const moveSel = status === "missing" ? $("#missing-move-library") : null;
if (!reqs.length) { if (!reqs.length) {
box.innerHTML = `<p class='muted'>${t("common.nothingHere")}</p>`; box.innerHTML = `<p class='muted'>${t("common.nothingHere")}</p>`;
bulkBtn.hidden = true; bulkBtn.hidden = true;
if (moveSel) moveSel.hidden = true;
return; return;
} }
box.innerHTML = reqs box.innerHTML = reqs
@@ -582,6 +602,10 @@ function renderRequests(status) {
const n = box.querySelectorAll("[data-sel-req]:checked").length; const n = box.querySelectorAll("[data-sel-req]:checked").length;
bulkBtn.hidden = n === 0; bulkBtn.hidden = n === 0;
bulkBtn.textContent = t("common.removeSelectedN", { n }); 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) => box.querySelectorAll("[data-sel-req]").forEach((c) =>
c.addEventListener("change", updateBulkBtn) c.addEventListener("change", updateBulkBtn)
@@ -596,14 +620,29 @@ async function bulkDeleteRequests(status) {
const hint = t(status === "imported" ? "list.confirmDeleteImported" : "list.confirmDeleteRequests", const hint = t(status === "imported" ? "list.confirmDeleteImported" : "list.confirmDeleteRequests",
{ n: ids.length }); { n: ids.length });
if (!confirm(hint)) return; if (!confirm(hint)) return;
for (const id of ids) { try {
try { await api("/api/requests/" + id, { method: "DELETE" }); } const res = await api("/api/requests/bulk", {
catch (err) { toast(err.message, true); } method: "DELETE", body: JSON.stringify({ ids: ids.map(Number) }),
} });
toast(t("list.deletedN", { n: ids.length })); toast(t("list.deletedN", { n: res.deleted }));
} catch (err) { toast(err.message, true); }
loadRequests(status); 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")); $("#imported-bulk-delete").addEventListener("click", () => bulkDeleteRequests("imported"));
function selectAllRequests(status) { function selectAllRequests(status) {
+14
View File
@@ -106,6 +106,13 @@ const STRINGS = {
"list.confirmDeleteRequests": "{n} Anfrage(n) entfernen?", "list.confirmDeleteRequests": "{n} Anfrage(n) entfernen?",
"list.confirmDeleteImported": "{n} Eintrag/Einträge entfernen? Die importierten Dateien in der Library bleiben erhalten.", "list.confirmDeleteImported": "{n} Eintrag/Einträge entfernen? Die importierten Dateien in der Library bleiben erhalten.",
"list.deletedN": "{n} entfernt", "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.statusMissing": "fehlt noch",
"detail.statusImported": "importiert", "detail.statusImported": "importiert",
@@ -339,6 +346,13 @@ const STRINGS = {
"list.confirmDeleteRequests": "Remove {n} request(s)?", "list.confirmDeleteRequests": "Remove {n} request(s)?",
"list.confirmDeleteImported": "Remove {n} entr(y/ies)? The imported files stay in the library.", "list.confirmDeleteImported": "Remove {n} entr(y/ies)? The imported files stay in the library.",
"list.deletedN": "{n} removed", "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.statusMissing": "still missing",
"detail.statusImported": "imported", "detail.statusImported": "imported",
+13
View File
@@ -110,7 +110,14 @@
<option value="" data-i18n="list.allLibraries"></option> <option value="" data-i18n="list.allLibraries"></option>
</select> </select>
<input id="missing-search" data-i18n-placeholder="list.filterPlaceholder" /> <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> <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> <button id="missing-bulk-delete" class="danger" hidden data-i18n="common.removeSelected"></button>
<div class="view-toggle" role="group" data-i18n-aria="list.view"> <div class="view-toggle" role="group" data-i18n-aria="list.view">
<button data-viewmode="list" data-i18n-title="list.viewList">☰</button> <button data-viewmode="list" data-i18n-title="list.viewList">☰</button>
@@ -124,6 +131,12 @@
<section id="view-imported" hidden> <section id="view-imported" hidden>
<div class="bar wrap"> <div class="bar wrap">
<input id="imported-search" data-i18n-placeholder="list.filterPlaceholder" /> <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-select-all" class="secondary" data-i18n="common.selectAll"></button>
<button id="imported-bulk-delete" class="danger" hidden data-i18n="common.removeSelected"></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"> <div class="view-toggle" role="group" data-i18n-aria="list.view">