From bb825a3acb9d9b71b3eada744f90864f4510a06b Mon Sep 17 00:00:00 2001 From: Josue Zamudio Date: Mon, 24 Aug 2026 20:09:39 +0000 Subject: [PATCH] Home: status board instead of a feed - replace 'Recently played' with one card per anime grouped by state: Continue watching (<=6, progress + ~Xm left), On hold (<=4, dimmed, days ago), Recently finished (<=6, check) - hero button says 'Next ep' when the last ep is >=95% watched - empty home shows recent searches as clickable chips (race-guarded: only appended while the empty state is still on screen) - helpers: byShow (latest row per anime), homeCard, showSec, link --- static/app.js | 97 +++++++++++++++++++++++++++++++----------------- static/style.css | 1 + 2 files changed, 63 insertions(+), 35 deletions(-) diff --git a/static/app.js b/static/app.js index a6ef705..0f8ba81 100644 --- a/static/app.js +++ b/static/app.js @@ -85,10 +85,60 @@ const bar = (x, cls) => { return b; }; const histHref = x => `?anime=${x.slug_id}&season=${x.season_id}&ep=${x.ep_id}&lang=${x.lang}`; +// one entry per anime (latest row wins), newest first +const byShow = () => { + const m = new Map(); + for (const x of history) { + const k = x.slug_id + ":" + x.season_id; + if (!m.has(k) || x.t > m.get(k).t) m.set(k, x); + } + return [...m.values()].sort((a, b) => b.t - a.t); +}; +const homeCard = x => { + const done = histClass(x) === "done", hold = histClass(x) === "hold"; + const c = el("a", "card" + (hold ? " hold" : "")); + c.href = histHref(x); + if (x.poster) { + const i = el("img"); i.src = x.poster; i.alt = ""; + c.append(i); + } + let meta = `E${x.ep} \u00b7 ${x.lang === "eng" ? "dub" : "sub"}`; + if (done) meta += " \u00b7 \u2713"; + else if (x.dur > 0 && x.pos < x.dur * 0.95) meta += ` \u00b7 ${fmtMin((x.dur - x.pos) / 60)} left`; + else if (hold) meta += ` \u00b7 ${Math.round(Date.now() / 1000 / 86400 - x.t / 86400)}d ago`; + const info = el("div", "mini-info"); + info.append(el("div", "card-name", x.title), el("div", "mini-meta", meta)); + const pb = bar(x, "small"); + if (pb) info.append(pb); + c.append(info); + return c; +}; +const showSec = (title, items, extra) => { + if (!items.length) return; + const head = el("div", "sec-head"); + head.append(el("h2", null, title)); + for (const a of extra) head.append(a); + const grid = el("div", "grid mini"); + for (const x of items) grid.append(homeCard(x)); + main.append(head, grid); +}; +const link = (text, href) => Object.assign(el("a", null, text), {href: BASE + href}); function showHome() { main.innerHTML = ""; - if (!history.length) { main.append(el("p", "muted", "Search for an anime to get started.")); return; } + if (!history.length) { + main.append(el("p", "muted empty", "Search for an anime to get started.")); + api("/api/searches").then(rs => { + if (!rs.length || !main.querySelector("p.empty")) return; + const d = el("div", null); + d.append(el("div", "muted", "recent searches")); + const chips = el("div", "chips"); + for (const q of rs) chips.append(link(q, "?q=" + encodeURIComponent(q))); + d.append(chips); + main.append(d); + }).catch(() => {}); + return; + } const hero = history[0]; // 1. continue-watching hero const sec = el("section", "hero-sec"); @@ -107,44 +157,21 @@ function showHome() { ` · ${timeago(hero.t)}`)); const p = bar(hero); if (p) body.append(p); - const go = el("button", "btn resume", "▶ Resume"); + const heroDone = hero.dur > 0 && hero.pos >= hero.dur * 0.95; + const go = el("button", "btn resume", heroDone ? "\u25b6 Next ep" : "\u25b6 Resume"); go.onclick = () => location.href = histHref(hero); right.append(body, go); sec.append(card, right); main.append(sec); - // 2-4. the rest as mini poster cards with progress + time-ago - if (history.length > 1) { - const head = el("div", "sec-head"); - head.append(el("h2", null, "Recently played")); - const st = el("a", null, "stats \u2192"); - st.href = BASE + "?stats"; - head.append(st); - if (history.length >= 5) { - const va = el("a", null, "view all \u2192"); - va.href = BASE + "?history"; - head.append(va); - } - main.append(head); - const grid = el("div", "grid mini"); - for (const x of history.slice(1, 6)) { - const c = el("a", "card"); - c.href = histHref(x); - if (x.poster) { - const i = el("img"); i.src = x.poster; i.alt = ""; - c.append(i); - } - const info = el("div", "mini-info"); - info.append(el("div", "card-name", x.title)); - const meta = el("div", "mini-meta"); - meta.textContent = `E${x.ep} · ${x.lang === "eng" ? "dub" : "sub"} · ${timeago(x.t)}`; - info.append(meta); - const pb = bar(x, "small"); - if (pb) info.append(pb); - c.append(info); - grid.append(c); - } - main.append(grid); - } + // 2-4. status board: one card per anime, grouped by state + const shows = byShow().filter(x => x !== hero); + const cont = shows.filter(x => histClass(x) === "watching").slice(0, 6); + const hold = shows.filter(x => histClass(x) === "hold").slice(0, 4); + const done = shows.filter(x => histClass(x) === "done").slice(0, 6); + showSec("Continue watching", cont, [link("stats \u2192", "?stats"), + link("history \u2192", "?history")]); + showSec("On hold", hold, hold.length ? [link("history \u2192", "?history")] : []); + showSec("Recently finished", done, done.length ? [link("history \u2192", "?history")] : []); } // ---------- search ---------- diff --git a/static/style.css b/static/style.css index bcd1940..b66e1dd 100644 --- a/static/style.css +++ b/static/style.css @@ -9,6 +9,7 @@ header form { flex: 1; max-width: 420px; } button.tiny, a.tiny { padding: .2rem .55rem; font-size: .8rem; border-radius: 6px; border: 1px solid var(--bd); background: var(--card); color: var(--fg); cursor: pointer; } a.tiny:hover { border-color: var(--acc); } +a.chip:hover { border-color: var(--acc); } button.tiny.danger { border-color: #7f1d1d; color: #f87171; } #menubox { position: relative; } #menu-items { display: none; position: absolute; right: 0; top: calc(100% + 4px);