home polish: continue-watching hero, progress bars, posters, time-ago

- showHome(): most recent entry renders as a hero card (poster,
  episode/lang/position, progress bar, Resume); the rest become
  mini poster cards with progress + relative timestamp
- history entries now persist dur (video.duration) and poster;
  /api/anime returns poster (parsed from the details page)
- old entries without dur render without a bar (backward compatible)
- browser test asserts the seeded entry, not a section header that
  only existed via a cross-test side effect
This commit is contained in:
jozamudi committed 2026-08-19 17:01:04 +00:00
1 parent e0f5427002
commit 2efb537edc
4 files changed
+89 -12

No files matched your search

+61 -9
View File
@@ -50,18 +50,66 @@ function postHistory(entry) {
body: JSON.stringify(entry)}).catch(() => {});
}
const fmtPos = s => { s = Math.floor(s || 0); return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`; };
const timeago = t => {
if (!t) return "";
const d = Math.max(0, Date.now() / 1000 - t);
if (d < 90) return "just now";
if (d < 3600) return `${Math.round(d / 60)}m ago`;
if (d < 86400) return `${Math.round(d / 3600)}h ago`;
return `${Math.round(d / 86400)}d ago`;
};
const prog = x => x.dur > 0 ? Math.min(100, Math.round(100 * x.pos / x.dur)) : null;
const bar = (x, cls) => {
const p = prog(x);
if (p == null) return null;
const b = el("div", "bar" + (cls ? " " + cls : ""));
b.append(Object.assign(el("div", "bar-fill"), {style: `width:${p}%`}));
return b;
};
const histHref = x => `?anime=${x.slug_id}&season=${x.season_id}&ep=${x.ep_id}&lang=${x.lang}`;
function showHome() {
main.innerHTML = "";
if (!history.length) { main.append(el("p", "muted", "Search for an anime to get started.")); return; }
const sec = el("section");
sec.append(el("h2", null, "Recently played"));
for (const x of history.slice(0, 5)) {
const b = el("button", "hist-item", `${x.title} — Ep ${x.ep} (${x.lang === "eng" ? "dub" : "sub"}) · ${fmtPos(x.pos)}`);
b.onclick = () => location.href = `?anime=${x.slug_id}&season=${x.season_id}&ep=${x.ep_id}&lang=${x.lang}`;
sec.append(b);
}
const hero = history[0];
// 1. continue-watching hero
const sec = el("section", "hero-sec");
const card = el("a", "hero");
card.href = histHref(hero);
const img = el("img"); img.src = hero.poster || ""; img.alt = "";
card.append(img);
const right = el("div", "right");
const body = el("div", "hero-body");
body.append(el("div", "hero-kicker", "continue watching"),
el("h2", null, hero.title),
el("div", "muted", `Ep ${hero.ep} · ${hero.lang === "eng" ? "dub" : "sub"} · ${fmtPos(hero.pos)} in · ${timeago(hero.t)}`));
const p = bar(hero);
if (p) body.append(p);
const go = el("button", "btn resume", "▶ 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) {
main.append(el("h2", null, "Recently played"));
const grid = el("div", "grid mini");
for (const x of history.slice(1, 6)) {
const c = el("a", "card");
c.href = histHref(x);
const i = el("img"); i.src = x.poster || ""; i.alt = "";
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(i, info);
grid.append(c);
}
main.append(grid);
}
}
// ---------- search ----------
@@ -195,7 +243,7 @@ async function play(ep_id, ep_no) {
v.onerror = () => showStreamError("media error: " + (v.error?.message || v.error?.code));
v.play().catch(() => showPlayOverlay(v)); // autoplay blocked (no user gesture) -> visible prompt
watchPos(v);
postHistory({title: detail.name, slug_id: detail.slug_id, season_id: seasonId,
postHistory({...meta(v), title: detail.name, slug_id: detail.slug_id, season_id: seasonId,
ep_id, lang, ep: ep_no, pos: saved?.pos || 0});
} catch (e) {
showStreamError(e.message);
@@ -203,11 +251,15 @@ async function play(ep_id, ep_no) {
document.title = `${detail.name} E${ep_no} — ani-web`;
}
// extra fields to persist with every history entry (progress + poster)
const meta = v => ({...(Number.isFinite(v.duration) && v.duration > 0 ? {dur: v.duration} : {}),
poster: detail ? (detail.poster || null) : null});
function watchPos(v) {
const save = () => {
if (!currentEp || v.currentTime <= 3) return;
lastSavedPos = v.currentTime;
postHistory({title: detail.name, slug_id: detail.slug_id, season_id: seasonId,
postHistory({...meta(v), title: detail.name, slug_id: detail.slug_id, season_id: seasonId,
ep_id: currentEp.ep_id, lang, ep: currentEp.ep_no, pos: v.currentTime});
};
v.addEventListener("timeupdate", () => { if (v.currentTime - lastSavedPos >= 10) save(); });