UX: remember sub/dub + filler toggle, hero time-left, next-ep CTA,
history filter chips, per-ep progress bars, stats in user menu - sub/dub choice persists per anime (localStorage aniweb_lang_<slug>), so search/reload no longer drops dub watchers back to sub - 'hide fillers' toggle persists (aniweb_ff) - continue-watching hero shows '~Xm left' when position+duration known - detail page: prominent 'next episode' button when the resumed ep is finished (auto-removed on play) - ?history: all / watching / completed / on-hold filter chips (client-side) - ep list: thin progress bar on partially-watched episodes - stats page link moved into the user menu (always reachable) - search results show a count line
This commit is contained in:
1 parent
1be3f4f307
commit
1de58629a0
3 files changed
+52
-11
No files matched your search
+46
-9
@@ -102,7 +102,9 @@ function showHome() {
|
||||
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)}`));
|
||||
el("div", "muted", `Ep ${hero.ep} · ${hero.lang === "eng" ? "dub" : "sub"} · ${fmtPos(hero.pos)} in` +
|
||||
(hero.dur > 0 && hero.pos < hero.dur * 0.95 ? ` · ${fmtMin((hero.dur - hero.pos) / 60)} left` : "") +
|
||||
` · ${timeago(hero.t)}`));
|
||||
const p = bar(hero);
|
||||
if (p) body.append(p);
|
||||
const go = el("button", "btn resume", "▶ Resume");
|
||||
@@ -157,6 +159,7 @@ async function showSearch(q) {
|
||||
const results = await api(`/api/search?q=${encodeURIComponent(q)}`);
|
||||
main.innerHTML = "";
|
||||
if (!results.length) { main.append(el("p", "muted", "No results.")); return; }
|
||||
main.append(el("p", "muted", `${results.length} result${results.length > 1 ? "s" : ""}`));
|
||||
const grid = el("div", "grid");
|
||||
for (const r of results) {
|
||||
const card = el("a", "card");
|
||||
@@ -198,7 +201,7 @@ function initRecent() {
|
||||
async function showAnime(slug_id, opts = {}) {
|
||||
skel();
|
||||
detail = await api(`/api/anime/${slug_id}`);
|
||||
lang = opts.lang || "jpn";
|
||||
lang = opts.lang || localStorage.getItem("aniweb_lang_" + slug_id) || "jpn";
|
||||
seasonId = opts.season || detail.id;
|
||||
detail.slug_id = slug_id;
|
||||
main.innerHTML = "";
|
||||
@@ -240,9 +243,10 @@ async function showAnime(slug_id, opts = {}) {
|
||||
const nextB = el("button", "tiny", "next ▶");
|
||||
prevB.onclick = () => stepEp(-1);
|
||||
nextB.onclick = () => stepEp(1);
|
||||
const fillB = el("button", "tiny", "hide fillers");
|
||||
const fillB = el("button", "tiny" + (hideFiller ? " on" : ""), "hide fillers");
|
||||
fillB.onclick = () => {
|
||||
hideFiller = !hideFiller;
|
||||
localStorage.setItem("aniweb_ff", hideFiller ? "1" : "0");
|
||||
fillB.textContent = hideFiller ? "show fillers" : "hide fillers";
|
||||
fillB.classList.toggle("on", hideFiller);
|
||||
renderEps();
|
||||
@@ -273,12 +277,14 @@ async function showAnime(slug_id, opts = {}) {
|
||||
// good enough for sequential viewing)
|
||||
epsList = await api(`/api/anime/${seasonId}/episodes`);
|
||||
watchDone = new Set();
|
||||
epProg = new Map();
|
||||
let started = false;
|
||||
try {
|
||||
for (const x of await api("/api/history?all=1")) {
|
||||
if (x.slug_id !== slug_id || x.season_id !== seasonId) continue;
|
||||
started = true;
|
||||
if (x.dur > 0 && x.pos >= x.dur * 0.95) watchDone.add(String(x.ep_id));
|
||||
else if (x.dur > 0) epProg.set(String(x.ep_id), Math.min(100, Math.round(100 * x.pos / x.dur)));
|
||||
}
|
||||
} catch {}
|
||||
watchNext = started ? (epsList.find(e => !watchDone.has(String(e.id))) || {}).id : null;
|
||||
@@ -289,26 +295,48 @@ async function showAnime(slug_id, opts = {}) {
|
||||
const cur = [...eps.children].find(b => +b.dataset.ep === start);
|
||||
if (cur) { markActive(cur); play(start, cur.dataset.no); }
|
||||
}
|
||||
// explicit CTA when the resumed episode is already finished
|
||||
if (start && watchDone.has(String(start)) && watchNext != null && !nextCta) {
|
||||
nextCta = el("button", "btn", "\u25b6 next episode");
|
||||
nextCta.onclick = () => stepEp(1);
|
||||
controls.append(nextCta);
|
||||
}
|
||||
|
||||
if (detail.synopsis) main.append(el("p", "synopsis bottom", detail.synopsis));
|
||||
}
|
||||
|
||||
function langLabel() { return lang === "eng" ? "▶ dub" : "▶ sub"; }
|
||||
|
||||
// ---------- history page (full list + delete) ----------
|
||||
// ---------- history page (full list + delete + client-side filter) ----------
|
||||
const histClass = x => x.dur > 0 && x.pos >= x.dur * 0.95 ? "done"
|
||||
: (x.t && Date.now() / 1000 - x.t > 7 * 86400 ? "hold" : "watching");
|
||||
async function showHistory() {
|
||||
skel(10);
|
||||
const items = await api("/api/history?all=1");
|
||||
main.innerHTML = "";
|
||||
main.append(el("h2", null, "History"));
|
||||
if (!items.length) { main.append(el("p", "muted", "Nothing here.")); return; }
|
||||
const labels = {all: "all", watching: "watching", done: "completed", hold: "on hold"};
|
||||
let active = "all";
|
||||
const chips = el("div", "chips");
|
||||
const grid = el("div", "grid");
|
||||
for (const x of items) grid.append(histCard(x));
|
||||
const draw = () => {
|
||||
grid.replaceChildren(...items.filter(x => active === "all" || histClass(x) === active).map(histCard));
|
||||
for (const c of chips.children) c.classList.toggle("on", c.dataset.f === active);
|
||||
};
|
||||
for (const f of Object.keys(labels)) {
|
||||
const b = el("button", "chip", labels[f]);
|
||||
b.dataset.f = f;
|
||||
b.onclick = () => { active = f; draw(); };
|
||||
chips.append(b);
|
||||
}
|
||||
main.append(chips);
|
||||
draw();
|
||||
main.append(grid);
|
||||
}
|
||||
|
||||
function histCard(x) {
|
||||
const hold = x.t && Date.now() / 1000 - x.t > 7 * 86400;
|
||||
const hold = histClass(x) === "hold";
|
||||
const c = el("div", "card hist" + (hold ? " hold" : ""));
|
||||
c.onclick = () => location.href = histHref(x);
|
||||
if (x.poster) {
|
||||
@@ -429,8 +457,8 @@ function markActive(b) {
|
||||
b.classList.add("active");
|
||||
}
|
||||
|
||||
let epsList = [], hideFiller = false;
|
||||
let watchDone = new Set(), watchNext = null;
|
||||
let epsList = [], hideFiller = localStorage.getItem("aniweb_ff") === "1";
|
||||
let watchDone = new Set(), watchNext = null, epProg = new Map();
|
||||
|
||||
function renderEps() {
|
||||
const eps = document.getElementById("eps");
|
||||
@@ -442,6 +470,12 @@ function renderEps() {
|
||||
const done = watchDone.has(String(e.id));
|
||||
b.textContent = done ? "E" + e.number + " \u2713" : "E" + e.number;
|
||||
if (done) b.classList.add("done");
|
||||
const pct = epProg.get(String(e.id));
|
||||
if (!done && pct != null && pct > 5) {
|
||||
const pb = el("div", "bar");
|
||||
pb.append(Object.assign(el("div", "bar-fill"), {style: `width:${pct}%`}));
|
||||
b.append(pb);
|
||||
}
|
||||
if (watchNext != null && String(watchNext) === String(e.id)) b.classList.add("nextup");
|
||||
if (e.filler) b.append(el("span", "badge", " filler"));
|
||||
b.dataset.ep = e.id; b.dataset.no = e.number;
|
||||
@@ -468,7 +502,7 @@ function videoEl() {
|
||||
|
||||
function stopHls() { if (hls) { hls.destroy(); hls = null; } }
|
||||
|
||||
let currentEp = null, lastSavedPos = 0, playerBroken = false;
|
||||
let currentEp = null, lastSavedPos = 0, playerBroken = false, nextCta = null;
|
||||
|
||||
function playerOverlay(text, onAction, actionLabel = "Retry") {
|
||||
const p = document.getElementById("player");
|
||||
@@ -536,6 +570,7 @@ function nextCountdown(nxt) {
|
||||
|
||||
async function play(ep_id, ep_no) {
|
||||
playerBroken = false;
|
||||
localStorage.setItem("aniweb_lang_" + detail.slug_id, lang);
|
||||
try {
|
||||
const s = await api(`/api/episode/${ep_id}/stream?lang=${lang}`);
|
||||
stopHls();
|
||||
@@ -555,6 +590,7 @@ async function play(ep_id, ep_no) {
|
||||
hls.attachMedia(v);
|
||||
currentEp = {ep_id, ep_no, v};
|
||||
lastSavedPos = 0;
|
||||
if (nextCta) { nextCta.remove(); nextCta = null; }
|
||||
const saved = history.find(x => x.slug_id === detail.slug_id && x.ep_id === ep_id);
|
||||
if (saved?.pos > 10) {
|
||||
v.addEventListener("loadedmetadata", () => {
|
||||
@@ -613,6 +649,7 @@ window.addEventListener("pagehide", savePos);
|
||||
// ---------- boot ----------
|
||||
(async () => {
|
||||
document.querySelector(".logo").href = BASE;
|
||||
document.getElementById("stats").href = BASE + "?stats";
|
||||
try {
|
||||
const me = await api("/api/me");
|
||||
document.getElementById("me").textContent = "hi, " + me.user;
|
||||
|
||||
Reference in new issue
Block a user