add(ui/ux): bulk delete
This commit is contained in:
+51
-26
@@ -261,6 +261,26 @@ $("#manual-form").addEventListener("submit", async (e) => {
|
|||||||
// ---- missing / imported lists ----
|
// ---- missing / imported lists ----
|
||||||
let currentRequests = { missing: [], imported: [] };
|
let currentRequests = { missing: [], imported: [] };
|
||||||
|
|
||||||
|
// view mode (Liste / kleine / große Symbole), shared by both lists
|
||||||
|
function getViewMode() {
|
||||||
|
try { return localStorage.getItem("wordarr.viewMode") || "small"; } catch { return "small"; }
|
||||||
|
}
|
||||||
|
function applyViewMode(mode) {
|
||||||
|
try { localStorage.setItem("wordarr.viewMode", mode); } catch {}
|
||||||
|
["#missing-list", "#imported-list"].forEach((sel) => {
|
||||||
|
const box = $(sel);
|
||||||
|
box.classList.remove("view-list", "view-small", "view-large");
|
||||||
|
box.classList.add("view-" + mode);
|
||||||
|
});
|
||||||
|
$$(".view-toggle button").forEach((b) =>
|
||||||
|
b.classList.toggle("active", b.dataset.viewmode === mode)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$$(".view-toggle button").forEach((b) =>
|
||||||
|
b.addEventListener("click", () => applyViewMode(b.dataset.viewmode))
|
||||||
|
);
|
||||||
|
applyViewMode(getViewMode());
|
||||||
|
|
||||||
function requestMatchesFilter(r, text) {
|
function requestMatchesFilter(r, text) {
|
||||||
if (!text) return true;
|
if (!text) return true;
|
||||||
const hay = `${r.title} ${r.authors} ${r.series} ${r.narrator}`.toLowerCase();
|
const hay = `${r.title} ${r.authors} ${r.series} ${r.narrator}`.toLowerCase();
|
||||||
@@ -284,9 +304,10 @@ 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 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");
|
||||||
if (!reqs.length) {
|
if (!reqs.length) {
|
||||||
box.innerHTML = "<p class='muted'>Nichts hier.</p>";
|
box.innerHTML = "<p class='muted'>Nichts hier.</p>";
|
||||||
if (status === "missing") $("#missing-bulk-delete").hidden = true;
|
bulkBtn.hidden = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
box.innerHTML = reqs
|
box.innerHTML = reqs
|
||||||
@@ -300,7 +321,7 @@ function renderRequests(status) {
|
|||||||
<span class="muted">${esc(r.media_type)} → ${esc(r.library_name)}</span>
|
<span class="muted">${esc(r.media_type)} → ${esc(r.library_name)}</span>
|
||||||
${status === "imported" ? `<span class="muted mono">${esc(r.imported_path)}</span>` : ""}
|
${status === "imported" ? `<span class="muted mono">${esc(r.imported_path)}</span>` : ""}
|
||||||
</div>
|
</div>
|
||||||
${status === "missing" ? `<input type="checkbox" class="card-check" data-sel-req="${r.id}">` : ""}
|
<input type="checkbox" class="card-check" data-sel-req="${r.id}">
|
||||||
</div>`
|
</div>`
|
||||||
)
|
)
|
||||||
.join("");
|
.join("");
|
||||||
@@ -310,37 +331,41 @@ function renderRequests(status) {
|
|||||||
openDetail(parseInt(card.dataset.detail), status);
|
openDetail(parseInt(card.dataset.detail), status);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
if (status === "missing") {
|
const updateBulkBtn = () => {
|
||||||
const updateBulkBtn = () => {
|
const n = box.querySelectorAll("[data-sel-req]:checked").length;
|
||||||
const n = box.querySelectorAll("[data-sel-req]:checked").length;
|
bulkBtn.hidden = n === 0;
|
||||||
const btn = $("#missing-bulk-delete");
|
bulkBtn.textContent = `Ausgewählte entfernen (${n})`;
|
||||||
btn.hidden = n === 0;
|
};
|
||||||
btn.textContent = `Ausgewählte entfernen (${n})`;
|
box.querySelectorAll("[data-sel-req]").forEach((c) =>
|
||||||
};
|
c.addEventListener("change", updateBulkBtn)
|
||||||
box.querySelectorAll("[data-sel-req]").forEach((c) =>
|
);
|
||||||
c.addEventListener("change", updateBulkBtn)
|
updateBulkBtn();
|
||||||
);
|
|
||||||
updateBulkBtn();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function bulkDeleteRequests(status) {
|
||||||
|
const box = status === "missing" ? $("#missing-list") : $("#imported-list");
|
||||||
|
const ids = [...box.querySelectorAll("[data-sel-req]:checked")].map((c) => c.dataset.selReq);
|
||||||
|
if (!ids.length) return;
|
||||||
|
const hint = status === "imported"
|
||||||
|
? `${ids.length} Eintrag/Einträge entfernen? Die importierten Dateien in der Library bleiben erhalten.`
|
||||||
|
: `${ids.length} Anfrage(n) entfernen?`;
|
||||||
|
if (!confirm(hint)) return;
|
||||||
|
for (const id of ids) {
|
||||||
|
try { await api("/api/requests/" + id, { method: "DELETE" }); }
|
||||||
|
catch (err) { toast(err.message, true); }
|
||||||
|
}
|
||||||
|
toast(`${ids.length} entfernt`);
|
||||||
|
loadRequests(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
$("#imported-bulk-delete").addEventListener("click", () => bulkDeleteRequests("imported"));
|
||||||
|
|
||||||
$("#missing-filter-type").addEventListener("change", () => loadRequests("missing"));
|
$("#missing-filter-type").addEventListener("change", () => loadRequests("missing"));
|
||||||
$("#missing-filter-library").addEventListener("change", () => loadRequests("missing"));
|
$("#missing-filter-library").addEventListener("change", () => loadRequests("missing"));
|
||||||
$("#missing-search").addEventListener("input", () => renderRequests("missing"));
|
$("#missing-search").addEventListener("input", () => renderRequests("missing"));
|
||||||
$("#imported-search").addEventListener("input", () => renderRequests("imported"));
|
$("#imported-search").addEventListener("input", () => renderRequests("imported"));
|
||||||
|
|
||||||
$("#missing-bulk-delete").addEventListener("click", async () => {
|
$("#missing-bulk-delete").addEventListener("click", () => bulkDeleteRequests("missing"));
|
||||||
const ids = [...$("#missing-list").querySelectorAll("[data-sel-req]:checked")]
|
|
||||||
.map((c) => c.dataset.selReq);
|
|
||||||
if (!ids.length) return;
|
|
||||||
if (!confirm(`${ids.length} Anfrage(n) entfernen?`)) return;
|
|
||||||
for (const id of ids) {
|
|
||||||
try { await api("/api/requests/" + id, { method: "DELETE" }); }
|
|
||||||
catch (err) { toast(err.message, true); }
|
|
||||||
}
|
|
||||||
toast(`${ids.length} Anfrage(n) entfernt`);
|
|
||||||
loadRequests("missing");
|
|
||||||
});
|
|
||||||
|
|
||||||
// ---- request detail dialog ----
|
// ---- request detail dialog ----
|
||||||
let detailRequest = null;
|
let detailRequest = null;
|
||||||
|
|||||||
@@ -90,6 +90,11 @@
|
|||||||
<button id="missing-bulk-delete" class="danger" hidden>
|
<button id="missing-bulk-delete" class="danger" hidden>
|
||||||
Ausgewählte entfernen
|
Ausgewählte entfernen
|
||||||
</button>
|
</button>
|
||||||
|
<div class="view-toggle" role="group" aria-label="Ansicht">
|
||||||
|
<button data-viewmode="list" title="Liste">☰</button>
|
||||||
|
<button data-viewmode="small" title="Kleine Symbole">▦</button>
|
||||||
|
<button data-viewmode="large" title="Große Symbole">⊞</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="missing-list" class="cards"></div>
|
<div id="missing-list" class="cards"></div>
|
||||||
</section>
|
</section>
|
||||||
@@ -97,6 +102,14 @@
|
|||||||
<section id="view-imported" hidden>
|
<section id="view-imported" hidden>
|
||||||
<div class="bar wrap">
|
<div class="bar wrap">
|
||||||
<input id="imported-search" placeholder="Filtern (Titel/Autor/Serie)…" />
|
<input id="imported-search" placeholder="Filtern (Titel/Autor/Serie)…" />
|
||||||
|
<button id="imported-bulk-delete" class="danger" hidden>
|
||||||
|
Ausgewählte entfernen
|
||||||
|
</button>
|
||||||
|
<div class="view-toggle" role="group" aria-label="Ansicht">
|
||||||
|
<button data-viewmode="list" title="Liste">☰</button>
|
||||||
|
<button data-viewmode="small" title="Kleine Symbole">▦</button>
|
||||||
|
<button data-viewmode="large" title="Große Symbole">⊞</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="imported-list" class="cards"></div>
|
<div id="imported-list" class="cards"></div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -55,6 +55,22 @@ button:disabled { opacity: 0.6; cursor: default; }
|
|||||||
.mono { font-family: ui-monospace, monospace; font-size: 0.85rem; word-break: break-all; }
|
.mono { font-family: ui-monospace, monospace; font-size: 0.85rem; word-break: break-all; }
|
||||||
.subpath { color: var(--muted); font-size: 0.75rem; font-family: ui-monospace, monospace; }
|
.subpath { color: var(--muted); font-size: 0.75rem; font-family: ui-monospace, monospace; }
|
||||||
|
|
||||||
|
/* view modes (Missing/Importiert) */
|
||||||
|
.view-toggle { display: flex; gap: 0; border: 1px solid var(--border); border-radius: 4px; overflow: hidden; margin-left: auto; }
|
||||||
|
.view-toggle button { background: var(--panel); color: var(--muted); padding: 0.45rem 0.7rem; border-radius: 0; }
|
||||||
|
.view-toggle button.active { background: var(--accent); color: #fff; }
|
||||||
|
.cards.view-large { grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); }
|
||||||
|
.cards.view-large .card img, .cards.view-large .nocover { width: 90px; height: 135px; }
|
||||||
|
.cards.view-small { grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); }
|
||||||
|
.cards.view-small .card img, .cards.view-small .nocover { width: 45px; height: 68px; }
|
||||||
|
.cards.view-small .card { padding: 0.5rem; gap: 0.6rem; }
|
||||||
|
.cards.view-small .card-body { font-size: 0.88rem; gap: 0.15rem; }
|
||||||
|
.cards.view-list { grid-template-columns: 1fr; gap: 0.25rem; }
|
||||||
|
.cards.view-list .card { padding: 0.35rem 0.6rem; gap: 0.6rem; align-items: center; border-radius: 4px; }
|
||||||
|
.cards.view-list .card img, .cards.view-list .nocover { width: 28px; height: 42px; font-size: 0.9rem; }
|
||||||
|
.cards.view-list .card-body { flex-direction: row; align-items: baseline; gap: 0.7rem; flex-wrap: wrap; }
|
||||||
|
.cards.view-list .card-check { position: static; margin-left: auto; flex-shrink: 0; }
|
||||||
|
|
||||||
/* tables */
|
/* tables */
|
||||||
.table-scroll { overflow: auto; max-height: 70vh; margin-bottom: 1rem; border: 1px solid var(--border); border-radius: 6px; }
|
.table-scroll { overflow: auto; max-height: 70vh; margin-bottom: 1rem; border: 1px solid var(--border); border-radius: 6px; }
|
||||||
table { width: 100%; border-collapse: collapse; background: var(--panel); }
|
table { width: 100%; border-collapse: collapse; background: var(--panel); }
|
||||||
|
|||||||
Reference in New Issue
Block a user