From cea04a20f68466776957717e9d8ba97a317c364e Mon Sep 17 00:00:00 2001 From: Josue Zamudio Date: Sat, 19 Sep 2026 00:11:45 +0000 Subject: [PATCH] player: fullscreen button (scales with the window) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit '⛶ full' in the controls: native requestFullscreen on the video (it fills the viewport and rescales on window resize for free), with the iOS webkitEnterFullscreen fallback where the Fullscreen API doesn't exist. Click again to exit. Live browser test: enter + exit via the button. --- static/app.js | 16 ++++++++++++++++ tests/test_browser.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/static/app.js b/static/app.js index 5b99457..31f214e 100644 --- a/static/app.js +++ b/static/app.js @@ -446,6 +446,22 @@ async function showAnime(slug_id, opts = {}) { anB.classList.toggle("on", autoNext); }; const ctl = [langBtn, spd, prevB, nextB, fillB, jump, anB]; + const fsB = el("button", "tiny", "\u26f6 full"); + fsB.title = "fullscreen"; + fsB.onclick = () => { + const v = document.querySelector("#player video"); + if (!v) return; + if (document.fullscreenElement || document.webkitFullscreenElement) { + (document.exitFullscreen || document.webkitExitFullscreen).call(document); + return; + } + // native Fullscreen API: the video fills the window (and scales with it); + // iOS Safari has no element fullscreen, so fall back to its in-player one + if (v.requestFullscreen) v.requestFullscreen(); + else if (v.webkitEnterFullscreen) v.webkitEnterFullscreen(); + else if (v.webkitRequestFullscreen) v.webkitRequestFullscreen(); + }; + ctl.push(fsB); if (document.pictureInPictureEnabled) { const pipB = el("button", "tiny", "\u29c9 PiP"); pipB.title = "picture-in-picture"; diff --git a/tests/test_browser.py b/tests/test_browser.py index 2e0d8e3..07026e1 100644 --- a/tests/test_browser.py +++ b/tests/test_browser.py @@ -490,3 +490,31 @@ async def _stale_history_remap_board(): "location.search.includes('q=')", timeout=15000, polling=POLL) assert not errors, errors await browser.close() + + +def test_fullscreen_toggle(): + asyncio.run(_fullscreen_toggle()) + + +async def _fullscreen_toggle(): + # the player controls get a fullscreen button; in headless chromium the + # native API works, so exercise enter + exit. + 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") + btn = page.locator("button:has-text('full')") + await btn.wait_for(timeout=30000) + assert await page.evaluate( + "() => typeof document.querySelector('#player video').requestFullscreen === 'function'") + await btn.click(force=True) + await page.wait_for_function( + "() => !!document.fullscreenElement", timeout=15000, polling=POLL) + await btn.click(force=True) + await page.wait_for_function( + "() => !document.fullscreenElement", timeout=15000, polling=POLL) + assert not errors, errors + await browser.close()