feat(booth): image viewer page with Fit/1:1, download, Esc-back (v0.1.2)

Clicking a gallery image now opens a dedicated viewer instead of dumping you on
the raw file.

- GET /b/<name>/view?f=<img> — full-viewport viewer (registered before the file
  catch-all so /view wins; non-image f 307-redirects to the raw file, traversal
  and missing f 404).
- Fit (downscale-only) / 1:1 (natural pixels, scroll-to-pan) toggle that only
  appears when the image is larger than the viewport — when it already fits,
  Fit ≡ 1:1 so the toggle is hidden. Re-evaluates on resize.
- Download button + ✕/Esc back to the gallery. Australis-themed, progressive
  JS (degrades to fit-only, no-JS still shows the image + download + back).
- 5 new tests (34 total, all green); verified Fit/1:1/hidden-toggle states in a
  real browser.
This commit is contained in:
vh
2026-07-20 22:10:46 -07:00
parent f328848bd7
commit 4e6c7951ff
7 changed files with 152 additions and 3 deletions
+40
View File
@@ -286,3 +286,43 @@ def test_upload_dedupes_repeated_names(client):
pid = r.headers["location"].split("/b/")[1].rstrip("/")
names = sorted(p.name for p in (data / pid).iterdir() if not p.name.startswith("."))
assert names == ["shot-1.png", "shot.png"]
# ---- image viewer -----------------------------------------------------------
def test_image_view_renders(client):
c, data = client
_touch(data / "run1" / "shot.png")
r = c.get("/b/run1/view", params={"f": "shot.png"})
assert r.status_code == 200
assert 'id="vstage"' in r.text # the viewer stage
assert ">Fit<" in r.text and ">1:1<" in r.text
assert "shot.png" in r.text
def test_gallery_image_links_to_viewer(client):
c, data = client
_touch(data / "run1" / "shot.png")
page = c.get("/b/run1/")
assert "view?f=shot.png" in page.text # gallery routes images to the viewer, not the raw file
def test_image_view_missing_404(client):
c, data = client
(data / "run1").mkdir()
assert c.get("/b/run1/view", params={"f": "nope.png"}).status_code == 404
def test_image_view_traversal_404(client):
c, data = client
(data / "run1").mkdir()
assert c.get("/b/run1/view", params={"f": "../../etc/passwd"}).status_code == 404
def test_image_view_nonimage_redirects_to_raw(client):
c, data = client
_touch(data / "run1" / "notes.txt")
r = c.get("/b/run1/view", params={"f": "notes.txt"}, follow_redirects=False)
assert r.status_code == 307
assert r.headers["location"] == "/b/run1/notes.txt"