features: auto-next w/ 5s countdown, prev/next, playback speed, hide fillers, recent searches
This commit is contained in:
1 parent
9fdf3944fc
commit
98fb4e5461
6 files changed
+269
-25
No files matched your search
+134
-24
@@ -138,7 +138,13 @@ function showHome() {
|
||||
}
|
||||
|
||||
// ---------- search ----------
|
||||
function saveSearch(q) {
|
||||
fetch(BASE + "api/searches", {method: "POST", headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({q})}).catch(() => {});
|
||||
}
|
||||
|
||||
async function showSearch(q) {
|
||||
saveSearch(q);
|
||||
skel(8);
|
||||
const results = await api(`/api/search?q=${encodeURIComponent(q)}`);
|
||||
main.innerHTML = "";
|
||||
@@ -155,6 +161,31 @@ async function showSearch(q) {
|
||||
main.append(grid);
|
||||
}
|
||||
|
||||
// ---------- recent searches dropdown ----------
|
||||
function initRecent() {
|
||||
const q = document.getElementById("q");
|
||||
const box = el("div", "recent");
|
||||
q.closest("form").append(box);
|
||||
let items = [];
|
||||
const render = () => {
|
||||
const v = q.value.trim().toLowerCase();
|
||||
const list = items.filter(s => !v || s.toLowerCase().includes(v)).slice(0, 8);
|
||||
box.replaceChildren();
|
||||
if (!list.length) { box.classList.remove("open"); return; }
|
||||
for (const s of list) {
|
||||
const b = el("button", null, s);
|
||||
b.onclick = () => { q.value = s; location.href = "?q=" + encodeURIComponent(s); };
|
||||
box.append(b);
|
||||
}
|
||||
box.classList.add("open");
|
||||
};
|
||||
const hide = () => box.classList.remove("open");
|
||||
q.addEventListener("focus", () => api("/api/searches").then(r => { items = r; render(); }).catch(hide));
|
||||
q.addEventListener("input", render);
|
||||
document.addEventListener("click", e => { if (!e.target.closest("header form")) hide(); });
|
||||
document.addEventListener("keydown", e => { if (e.key === "Escape") hide(); });
|
||||
}
|
||||
|
||||
// ---------- detail + player ----------
|
||||
async function showAnime(slug_id, opts = {}) {
|
||||
skel();
|
||||
@@ -177,13 +208,34 @@ async function showAnime(slug_id, opts = {}) {
|
||||
|
||||
const controls = el("div", "controls");
|
||||
const langBtn = el("button", null, langLabel());
|
||||
const setLang = l => { lang = l; langBtn.textContent = langLabel(); };
|
||||
langBtn.onclick = () => {
|
||||
setLang(lang === "eng" ? "jpn" : "eng");
|
||||
lang = lang === "eng" ? "jpn" : "eng";
|
||||
langBtn.textContent = langLabel();
|
||||
const cur = main.querySelector(".ep.active");
|
||||
if (cur) play(cur.dataset.ep, cur.dataset.no);
|
||||
};
|
||||
controls.append(langBtn, el("span", "muted", " — click an episode to play"));
|
||||
const spd = el("select", "spd");
|
||||
for (const r of [0.5, 0.75, 1, 1.25, 1.5, 2]) spd.append(new Option(r + "×", r));
|
||||
spd.value = localStorage.getItem("aniweb_spd") || "1";
|
||||
spd.title = "playback speed";
|
||||
spd.onchange = () => {
|
||||
localStorage.setItem("aniweb_spd", spd.value);
|
||||
const v = document.querySelector("#player video");
|
||||
if (v) v.playbackRate = +spd.value;
|
||||
};
|
||||
const prevB = el("button", "tiny", "◀ prev");
|
||||
const nextB = el("button", "tiny", "next ▶");
|
||||
prevB.onclick = () => stepEp(-1);
|
||||
nextB.onclick = () => stepEp(1);
|
||||
const fillB = el("button", "tiny", "hide fillers");
|
||||
fillB.onclick = () => {
|
||||
hideFiller = !hideFiller;
|
||||
fillB.textContent = hideFiller ? "show fillers" : "hide fillers";
|
||||
fillB.classList.toggle("on", hideFiller);
|
||||
renderEps();
|
||||
};
|
||||
controls.append(langBtn, spd, prevB, nextB, fillB,
|
||||
el("span", "muted", " — click an episode to play"));
|
||||
main.append(controls);
|
||||
|
||||
const tabs = el("div", "tabs");
|
||||
@@ -203,27 +255,14 @@ async function showAnime(slug_id, opts = {}) {
|
||||
eps.append(el("p", "muted", "loading episodes\u2026"));
|
||||
main.append(eps);
|
||||
|
||||
const list = await api(`/api/anime/${seasonId}/episodes`);
|
||||
eps.innerHTML = "";
|
||||
for (const e of list) {
|
||||
const b = el("button", "ep" + (e.filler ? " filler" : ""));
|
||||
b.textContent = "E" + e.number;
|
||||
if (e.filler) b.append(el("span", "badge", " filler"));
|
||||
b.dataset.ep = e.id; b.dataset.no = e.number;
|
||||
b.onclick = () => { markActive(b); play(e.id, e.number); };
|
||||
eps.append(b);
|
||||
}
|
||||
if (opts.ep) {
|
||||
const cur = [...eps.children].find(b => +b.dataset.ep === opts.ep);
|
||||
if (cur) { markActive(cur); play(opts.ep, cur.dataset.no); }
|
||||
} else {
|
||||
// default: resume last watched ep of this season, else start at ep 1
|
||||
const h = history.find(x => x.slug_id === slug_id && x.season_id === seasonId);
|
||||
const auto = h ? h.ep_id : +eps.querySelector(".ep")?.dataset.ep;
|
||||
if (auto) {
|
||||
const cur = [...eps.children].find(b => +b.dataset.ep === auto);
|
||||
if (cur) { markActive(cur); play(auto, cur.dataset.no); }
|
||||
}
|
||||
// default: resume last watched ep of this season, else start at ep 1
|
||||
epsList = await api(`/api/anime/${seasonId}/episodes`);
|
||||
renderEps();
|
||||
const h = history.find(x => x.slug_id === slug_id && x.season_id === seasonId);
|
||||
const start = opts.ep || (h ? h.ep_id : null) || epsList[0]?.id;
|
||||
if (start) {
|
||||
const cur = [...eps.children].find(b => +b.dataset.ep === start);
|
||||
if (cur) { markActive(cur); play(start, cur.dataset.no); }
|
||||
}
|
||||
|
||||
if (detail.synopsis) main.append(el("p", "synopsis bottom", detail.synopsis));
|
||||
@@ -275,6 +314,33 @@ function markActive(b) {
|
||||
b.classList.add("active");
|
||||
}
|
||||
|
||||
let epsList = [], hideFiller = false;
|
||||
|
||||
function renderEps() {
|
||||
const eps = document.getElementById("eps");
|
||||
if (!eps) return;
|
||||
eps.innerHTML = "";
|
||||
for (const e of epsList) {
|
||||
if (hideFiller && e.filler) continue;
|
||||
const b = el("button", "ep" + (e.filler ? " filler" : ""));
|
||||
b.textContent = "E" + e.number;
|
||||
if (e.filler) b.append(el("span", "badge", " filler"));
|
||||
b.dataset.ep = e.id; b.dataset.no = e.number;
|
||||
b.onclick = () => { markActive(b); play(e.id, e.number); };
|
||||
eps.append(b);
|
||||
}
|
||||
}
|
||||
|
||||
function stepEp(d) {
|
||||
const vis = epsList.filter(e => !(hideFiller && e.filler));
|
||||
if (!vis.length || !currentEp) return;
|
||||
const i = vis.findIndex(e => String(e.id) === String(currentEp.ep_id));
|
||||
const n = vis[i + d];
|
||||
if (!n) return;
|
||||
const b = [...document.querySelectorAll(".ep")].find(x => +x.dataset.ep === n.id);
|
||||
if (b) { markActive(b); play(n.id, n.number); }
|
||||
}
|
||||
|
||||
function videoEl() {
|
||||
const v = document.createElement("video");
|
||||
v.controls = true; v.autoplay = true;
|
||||
@@ -307,12 +373,54 @@ function playerFail(msg) {
|
||||
playerOverlay("Playback failed: " + msg, () => play(ep_id, ep_no));
|
||||
}
|
||||
|
||||
// ---------- auto-next on episode end (5s countdown) ----------
|
||||
let nextTimer = null;
|
||||
function onEnded() {
|
||||
if (!currentEp || playerBroken) return;
|
||||
const vis = epsList.filter(e => !(hideFiller && e.filler));
|
||||
const i = vis.findIndex(e => String(e.id) === String(currentEp.ep_id));
|
||||
const nxt = vis[i + 1];
|
||||
if (!nxt) { playerHideOverlay(); toast("No more episodes"); return; }
|
||||
nextCountdown(nxt);
|
||||
}
|
||||
|
||||
function nextCountdown(nxt) {
|
||||
const p = document.getElementById("player");
|
||||
let o = p.querySelector(".pl-overlay");
|
||||
if (!o) { o = el("div", "pl-overlay"); p.append(o); }
|
||||
let n = 5;
|
||||
const done = () => {
|
||||
if (nextTimer) { clearTimeout(nextTimer); nextTimer = null; }
|
||||
const b = [...document.querySelectorAll(".ep")].find(x => +x.dataset.ep === nxt.id);
|
||||
if (b) markActive(b);
|
||||
play(nxt.id, nxt.number);
|
||||
};
|
||||
const cancel = () => {
|
||||
if (nextTimer) { clearTimeout(nextTimer); nextTimer = null; }
|
||||
playerHideOverlay();
|
||||
};
|
||||
const draw = () => {
|
||||
o.replaceChildren(el("span", null, `Next: E${nxt.number} in ${n}\u2026`));
|
||||
const s = el("button", "btn", "Skip");
|
||||
s.onclick = done;
|
||||
const x = el("button", "cnt-x", "\u2715");
|
||||
x.title = "cancel auto-next";
|
||||
x.onclick = cancel;
|
||||
o.append(s, x);
|
||||
if (n <= 0) { done(); return; }
|
||||
n--;
|
||||
nextTimer = setTimeout(draw, 1000);
|
||||
};
|
||||
draw();
|
||||
}
|
||||
|
||||
async function play(ep_id, ep_no) {
|
||||
playerBroken = false;
|
||||
try {
|
||||
const s = await api(`/api/episode/${ep_id}/stream?lang=${lang}`);
|
||||
stopHls();
|
||||
const v = videoEl();
|
||||
v.playbackRate = +(localStorage.getItem("aniweb_spd") || 1);
|
||||
document.getElementById("player").replaceChildren(v);
|
||||
playerOverlay("Loading\u2026");
|
||||
if (!window.Hls || !Hls.isSupported()) throw new Error("HLS not supported in this browser");
|
||||
@@ -338,6 +446,7 @@ async function play(ep_id, ep_no) {
|
||||
v.addEventListener("waiting", () => {
|
||||
if (currentEp && !playerBroken && !v.ended) playerOverlay("Buffering\u2026");
|
||||
});
|
||||
v.addEventListener("ended", onEnded);
|
||||
v.play().catch(() => showPlayOverlay(v)); // autoplay blocked (no user gesture) -> visible prompt
|
||||
watchPos(v);
|
||||
postHistory({...meta(v), title: detail.name, slug_id: detail.slug_id, season_id: seasonId,
|
||||
@@ -378,6 +487,7 @@ function showPlayOverlay(v) {
|
||||
try {
|
||||
const me = await api("/api/me");
|
||||
document.getElementById("me").textContent = "hi, " + me.user;
|
||||
initRecent();
|
||||
const lo = document.getElementById("logout");
|
||||
lo.onclick = async () => {
|
||||
await fetch(BASE + "logout", {method: "POST"}); location.href = BASE + "login?signedout=1";
|
||||
|
||||
Reference in new issue
Block a user