feat(ui): keep the current view in the url hash

This commit is contained in:
Steppenstreuner committed 2026-10-25 19:16:48 +01:00
1 parent 5f4b3cb564
commit adc708ae29
4 files changed
+65 -9

No files matched your search

+38 -9
View File
@@ -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);
}
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();
+2
View File
@@ -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)",
+1
View File
@@ -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; }
+24
View File
@@ -159,3 +159,27 @@ def test_the_header_shows_the_running_version(page):
stamp = page.locator("#app-version").text_content()
assert stamp.startswith("v")
assert len(stamp) > 3 # version plus the build stamp behind it
def test_the_view_survives_reload_and_back(page, server):
page.click("nav button[data-view=missing]")
assert page.url.endswith("#missing")
page.reload(wait_until="networkidle")
assert page.locator("#view-missing").is_visible()
assert not page.locator("#view-search").is_visible()
page.click("nav button[data-view=settings]")
page.go_back()
assert page.locator("#view-missing").is_visible()
assert page.locator("nav button[data-view=missing]").get_attribute("class") == "active"
def test_a_card_opens_from_the_keyboard(page, server, tmp_path):
lib = page.request.post(f"{server}/api/libraries", data={
"name": "Tastatur", "media_type": "ebook", "root_path": str(tmp_path)}).json()
page.request.post(f"{server}/api/requests", data={
"title": "Zyxw Tastaturprobe", "library_id": lib["id"]})
page.click("nav button[data-view=missing]")
card = page.locator("[data-detail]", has_text="Zyxw Tastaturprobe")
card.focus()
page.keyboard.press("Enter")
assert page.locator("#detail-dialog").is_visible()