ui: watched-episode state (dimmed + check, next-up highlight), favicon
This commit is contained in:
1 parent
98fb4e5461
commit
bb1297f8e2
5 files changed
+54
-2
No files matched your search
+18
-2
@@ -255,8 +255,20 @@ async function showAnime(slug_id, opts = {}) {
|
|||||||
eps.append(el("p", "muted", "loading episodes\u2026"));
|
eps.append(el("p", "muted", "loading episodes\u2026"));
|
||||||
main.append(eps);
|
main.append(eps);
|
||||||
|
|
||||||
// default: resume last watched ep of this season, else start at ep 1
|
// watched state: dim completed eps, highlight the first unwatched one
|
||||||
|
// (ponytail: "first unwatched in list order" is wrong if you skip around;
|
||||||
|
// good enough for sequential viewing)
|
||||||
epsList = await api(`/api/anime/${seasonId}/episodes`);
|
epsList = await api(`/api/anime/${seasonId}/episodes`);
|
||||||
|
watchDone = new Set();
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
watchNext = started ? (epsList.find(e => !watchDone.has(String(e.id))) || {}).id : null;
|
||||||
renderEps();
|
renderEps();
|
||||||
const h = history.find(x => x.slug_id === slug_id && x.season_id === seasonId);
|
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;
|
const start = opts.ep || (h ? h.ep_id : null) || epsList[0]?.id;
|
||||||
@@ -315,6 +327,7 @@ function markActive(b) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let epsList = [], hideFiller = false;
|
let epsList = [], hideFiller = false;
|
||||||
|
let watchDone = new Set(), watchNext = null;
|
||||||
|
|
||||||
function renderEps() {
|
function renderEps() {
|
||||||
const eps = document.getElementById("eps");
|
const eps = document.getElementById("eps");
|
||||||
@@ -323,7 +336,10 @@ function renderEps() {
|
|||||||
for (const e of epsList) {
|
for (const e of epsList) {
|
||||||
if (hideFiller && e.filler) continue;
|
if (hideFiller && e.filler) continue;
|
||||||
const b = el("button", "ep" + (e.filler ? " filler" : ""));
|
const b = el("button", "ep" + (e.filler ? " filler" : ""));
|
||||||
b.textContent = "E" + e.number;
|
const done = watchDone.has(String(e.id));
|
||||||
|
b.textContent = done ? "E" + e.number + " \u2713" : "E" + e.number;
|
||||||
|
if (done) b.classList.add("done");
|
||||||
|
if (watchNext != null && String(watchNext) === String(e.id)) b.classList.add("nextup");
|
||||||
if (e.filler) b.append(el("span", "badge", " filler"));
|
if (e.filler) b.append(el("span", "badge", " filler"));
|
||||||
b.dataset.ep = e.id; b.dataset.no = e.number;
|
b.dataset.ep = e.id; b.dataset.no = e.number;
|
||||||
b.onclick = () => { markActive(b); play(e.id, e.number); };
|
b.onclick = () => { markActive(b); play(e.id, e.number); };
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>ani-web</title>
|
<title>ani-web</title>
|
||||||
|
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Crect width='64' height='64' rx='14' fill='%23f97316'/%3E%3Ctext x='32' y='45' font-family='sans-serif' font-size='42' font-weight='700' text-anchor='middle' fill='%230e0f13'%3Ea%3C/text%3E%3C/svg%3E">
|
||||||
|
|
||||||
<link rel="stylesheet" href="style.css">
|
<link rel="stylesheet" href="style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -115,5 +115,7 @@ button.tiny.on { border-color: var(--acc); color: var(--acc); }
|
|||||||
.ep { position: relative; font-size: 0.9rem; }
|
.ep { position: relative; font-size: 0.9rem; }
|
||||||
.ep.active { border-color: var(--acc); color: var(--acc); }
|
.ep.active { border-color: var(--acc); color: var(--acc); }
|
||||||
.ep.filler { opacity: 0.55; }
|
.ep.filler { opacity: 0.55; }
|
||||||
|
.ep.done { opacity: 0.5; }
|
||||||
|
.ep.nextup { border-color: #4b5568; background: #1f2130; }
|
||||||
.badge { font-size: 0.6rem; color: var(--muted); }
|
.badge { font-size: 0.6rem; color: var(--muted); }
|
||||||
|
|
||||||
@@ -78,6 +78,7 @@ def main():
|
|||||||
asyncio.run(test_browser.test_stream_failure_retry())
|
asyncio.run(test_browser.test_stream_failure_retry())
|
||||||
asyncio.run(test_browser.test_recent_searches())
|
asyncio.run(test_browser.test_recent_searches())
|
||||||
asyncio.run(test_browser.test_auto_next_countdown())
|
asyncio.run(test_browser.test_auto_next_countdown())
|
||||||
|
asyncio.run(test_browser.test_episode_watch_state())
|
||||||
|
|
||||||
print("ALL TESTS PASSED")
|
print("ALL TESTS PASSED")
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ async def test_login_ui():
|
|||||||
await page.keyboard.press("Enter")
|
await page.keyboard.press("Enter")
|
||||||
await _login_settled(page)
|
await _login_settled(page)
|
||||||
assert await page.inner_text("#me") == "hi, admin"
|
assert await page.inner_text("#me") == "hi, admin"
|
||||||
|
assert await page.query_selector('link[rel="icon"]'), "favicon missing"
|
||||||
assert "Seed Anime" in await page.inner_text("#main"), "seeded history must show"
|
assert "Seed Anime" in await page.inner_text("#main"), "seeded history must show"
|
||||||
assert not errors, errors
|
assert not errors, errors
|
||||||
await browser.close()
|
await browser.close()
|
||||||
@@ -225,6 +226,35 @@ async def test_auto_next_countdown():
|
|||||||
print("ok test_auto_next_countdown")
|
print("ok test_auto_next_countdown")
|
||||||
|
|
||||||
|
|
||||||
|
async def test_episode_watch_state():
|
||||||
|
# completed episodes dimmed with a check, first unwatched highlighted.
|
||||||
|
# seed the first rendered ep as finished (id read off the list),
|
||||||
|
# reload, and check the classes.
|
||||||
|
async with async_playwright() as p:
|
||||||
|
browser = await p.chromium.launch(args=LAUNCH_ARGS)
|
||||||
|
page = await (await browser.new_context()).new_page()
|
||||||
|
errors = []
|
||||||
|
page.on("pageerror", lambda e: errors.append(str(e)))
|
||||||
|
|
||||||
|
await _login(page, "admin", "adminpw12345")
|
||||||
|
await page.goto(BASE + "/?anime=chainsaw-man-922")
|
||||||
|
await page.wait_for_selector("#eps .ep", timeout=30000)
|
||||||
|
e1_id = await page.evaluate("document.querySelector('#eps .ep').dataset.ep")
|
||||||
|
await page.evaluate(
|
||||||
|
"(id) => fetch('api/history', {method: 'POST',"
|
||||||
|
" headers: {'Content-Type': 'application/json'},"
|
||||||
|
" body: JSON.stringify({title: 'Chainsaw Man', slug_id: 'chainsaw-man-922',"
|
||||||
|
" season_id: 922, ep_id: +id, lang: 'eng', ep: 1, pos: 2350, dur: 2400})})",
|
||||||
|
e1_id)
|
||||||
|
await page.reload()
|
||||||
|
await page.wait_for_selector("#eps .ep.done", timeout=30000)
|
||||||
|
assert "\u2713" in await page.inner_text("#eps .ep.done")
|
||||||
|
await page.wait_for_selector("#eps .ep.nextup", timeout=10000)
|
||||||
|
assert not errors, errors
|
||||||
|
await browser.close()
|
||||||
|
print("ok test_episode_watch_state")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
if len(sys.argv) > 1:
|
if len(sys.argv) > 1:
|
||||||
BASE = sys.argv[1]
|
BASE = sys.argv[1]
|
||||||
@@ -234,3 +264,4 @@ if __name__ == "__main__":
|
|||||||
asyncio.run(test_stream_failure_retry())
|
asyncio.run(test_stream_failure_retry())
|
||||||
asyncio.run(test_recent_searches())
|
asyncio.run(test_recent_searches())
|
||||||
asyncio.run(test_auto_next_countdown())
|
asyncio.run(test_auto_next_countdown())
|
||||||
|
asyncio.run(test_episode_watch_state())
|
||||||
Reference in new issue
Block a user