feat(ui): keep the current view in the url hash
This commit is contained in:
1 parent
5f4b3cb564
commit
adc708ae29
4 files changed
+66
-10
No files matched your search
+39
-10
@@ -39,12 +39,16 @@ async function createRequest(body) {
|
||||
}
|
||||
}
|
||||
|
||||
let toastTimer;
|
||||
|
||||
function toast(msg, isError = false) {
|
||||
const t = $("#toast");
|
||||
t.textContent = msg;
|
||||
t.className = isError ? "error" : "";
|
||||
t.hidden = false;
|
||||
setTimeout(() => (t.hidden = true), 4000);
|
||||
// an older toast's timer would otherwise hide this one early
|
||||
clearTimeout(toastTimer);
|
||||
toastTimer = setTimeout(() => (t.hidden = true), 4000);
|
||||
}
|
||||
|
||||
// quotes included: the result also lands inside attributes (value="${esc(...)}")
|
||||
@@ -88,17 +92,26 @@ async function refreshMissingBadge() {
|
||||
}
|
||||
|
||||
// ---- navigation ----
|
||||
// the view lives in the hash so reload, back button and links keep it
|
||||
const VIEWS = ["search", "missing", "imported", "import", "settings"];
|
||||
|
||||
function showView() {
|
||||
const hash = location.hash.slice(1);
|
||||
const name = VIEWS.includes(hash) ? hash : "search";
|
||||
$$("nav button[data-view]").forEach((b) => b.classList.toggle("active", b.dataset.view === name));
|
||||
$$("main > section").forEach((s) => (s.hidden = s.id !== "view-" + name));
|
||||
if (name === "missing" || name === "imported") loadRequests(name);
|
||||
if (name === "settings") loadLibraries().catch((err) => toast(err.message, true));
|
||||
}
|
||||
|
||||
$$("nav button[data-view]").forEach((btn) =>
|
||||
btn.addEventListener("click", () => {
|
||||
$$("nav button[data-view]").forEach((b) => b.classList.remove("active"));
|
||||
btn.classList.add("active");
|
||||
$$("main > section").forEach((s) => (s.hidden = true));
|
||||
$("#view-" + btn.dataset.view).hidden = false;
|
||||
if (btn.dataset.view === "missing") loadRequests("missing");
|
||||
if (btn.dataset.view === "imported") loadRequests("imported");
|
||||
if (btn.dataset.view === "settings") loadLibraries();
|
||||
// same hash fires no hashchange, but a click should still reload the list
|
||||
if (location.hash === "#" + btn.dataset.view) showView();
|
||||
else location.hash = btn.dataset.view;
|
||||
})
|
||||
);
|
||||
window.addEventListener("hashchange", showView);
|
||||
|
||||
// ---- libraries ----
|
||||
async function loadLibraries() {
|
||||
@@ -513,7 +526,15 @@ async function loadRequests(status) {
|
||||
if (t) params.set("media_type", t);
|
||||
if (l) params.set("library_id", l);
|
||||
}
|
||||
currentRequests[status] = await api("/api/requests?" + params);
|
||||
const box = status === "missing" ? $("#missing-list") : $("#imported-list");
|
||||
if (!currentRequests[status].length) {
|
||||
box.innerHTML = `<p class="muted"><span class="spinner"></span> ${t("common.loading")}</p>`;
|
||||
}
|
||||
try {
|
||||
currentRequests[status] = await api("/api/requests?" + params);
|
||||
} catch (err) {
|
||||
toast(err.message, true);
|
||||
}
|
||||
renderRequests(status);
|
||||
if (status === "missing") refreshMissingBadge();
|
||||
}
|
||||
@@ -530,7 +551,7 @@ function renderRequests(status) {
|
||||
}
|
||||
box.innerHTML = reqs
|
||||
.map(
|
||||
(r) => `<div class="card clickable" data-detail="${r.id}">
|
||||
(r) => `<div class="card clickable" data-detail="${r.id}" tabindex="0">
|
||||
${r.cover_url ? `<img src="${esc(r.cover_url)}" alt="" loading="lazy">` : '<div class="nocover">?</div>'}
|
||||
<div class="card-body">
|
||||
<strong>${esc(r.title)}${r.volume != null ? ` · ${t("common.volume")} ${r.volume}` : ""}</strong>
|
||||
@@ -549,6 +570,13 @@ function renderRequests(status) {
|
||||
openDetail(parseInt(card.dataset.detail), status);
|
||||
})
|
||||
);
|
||||
box.querySelectorAll("[data-detail]").forEach((card) =>
|
||||
card.addEventListener("keydown", (ev) => {
|
||||
if (ev.target !== card || (ev.key !== "Enter" && ev.key !== " ")) return;
|
||||
ev.preventDefault();
|
||||
openDetail(parseInt(card.dataset.detail), status);
|
||||
})
|
||||
);
|
||||
const updateBulkBtn = () => {
|
||||
const n = box.querySelectorAll("[data-sel-req]:checked").length;
|
||||
bulkBtn.hidden = n === 0;
|
||||
@@ -1754,3 +1782,4 @@ endLibraryEdit();
|
||||
loadLibraries();
|
||||
refreshMissingBadge();
|
||||
showVersion();
|
||||
showView();
|
||||
@@ -33,6 +33,7 @@ const STRINGS = {
|
||||
"common.status": "Status",
|
||||
"common.noLibrary": "— keine Library —",
|
||||
"common.nothingHere": "Nichts hier.",
|
||||
"common.loading": "Wird geladen…",
|
||||
"common.optionalForAll": "optional, für alle",
|
||||
"common.optional": "optional",
|
||||
"common.needLibraryFirst": "Erst eine Library für diesen Typ anlegen (Tab Libraries)",
|
||||
@@ -256,6 +257,7 @@ const STRINGS = {
|
||||
"common.status": "Status",
|
||||
"common.noLibrary": "— no library —",
|
||||
"common.nothingHere": "Nothing here.",
|
||||
"common.loading": "Loading…",
|
||||
"common.optionalForAll": "optional, for all",
|
||||
"common.optional": "optional",
|
||||
"common.needLibraryFirst": "Create a library for this type first (Libraries tab)",
|
||||
|
||||
@@ -48,6 +48,7 @@ button:disabled { opacity: 0.6; cursor: default; }
|
||||
}
|
||||
.card.clickable { cursor: pointer; transition: border-color 0.1s, background 0.1s; }
|
||||
.card.clickable:hover { border-color: var(--accent); background: #262b35; }
|
||||
.card.clickable:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
|
||||
.card img, .nocover { width: 60px; height: 90px; object-fit: cover; border-radius: 4px; flex-shrink: 0; }
|
||||
.nocover { background: var(--bg); display: flex; align-items: center; justify-content: center; color: var(--muted); font-size: 1.5rem; }
|
||||
.card-body { display: flex; flex-direction: column; gap: 0.25rem; min-width: 0; }
|
||||
|
||||
Reference in new issue
Block a user