Art page round 2: virtual frames, real logo, hi-res zoom, spacing/affordance polish
- Virtual frames (Walnut/Gold/Silver/None), selectable + remembered in localStorage,
built as a beveled moulding around a cream museum mat.
- Header uses the real /logo.svg wordmark; the "No ads" pill is replaced by an
account icon (the pill doesn't need to follow every page).
- Lightbox now opens a full-resolution copy that fills the screen: art._download_image
caches a hi-res {id}-full copy alongside the web-large display copy, served via
/api/art/image/{id}?size=full (image_url_large in /api/art/today).
- Centered the placard bullet separators (explicit .sep spans, equal margins).
- Image no longer shifts on hover; a quiet "Click to expand" affordance sits on the art.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+6
-2
@@ -2276,11 +2276,15 @@ def create_app() -> FastAPI:
|
||||
"is_public_domain": bool(a["is_public_domain"]),
|
||||
"license": "Public Domain (CC0)" if a["is_public_domain"] else None,
|
||||
"image_url": f"/api/art/image/{a['object_id']}",
|
||||
"image_url_large": f"/api/art/image/{a['object_id']}?size=full",
|
||||
}
|
||||
|
||||
@app.get("/api/art/image/{object_id}")
|
||||
def art_image(object_id: int) -> FileResponse:
|
||||
matches = sorted(art.cache_dir().glob(f"{object_id}.*"))
|
||||
def art_image(object_id: int, size: str = Query("")) -> FileResponse:
|
||||
cdir = art.cache_dir()
|
||||
matches = sorted(cdir.glob(f"{object_id}-full.*")) if size == "full" else []
|
||||
if not matches: # fall back to the web-large copy
|
||||
matches = sorted(cdir.glob(f"{object_id}.*"))
|
||||
if not matches:
|
||||
raise HTTPException(status_code=404, detail="Not cached.")
|
||||
# Cached museum image: immutable for a given object id.
|
||||
|
||||
+36
-25
@@ -91,34 +91,45 @@ def _object(object_id: int) -> dict:
|
||||
return _http_json(f"{MET_BASE}/objects/{object_id}")
|
||||
|
||||
|
||||
def _download_image(obj: dict, object_id: int) -> str | None:
|
||||
"""Download the web-large (then full) image to our cache; return the filename or None.
|
||||
def _fetch_to_cache(url: str | None, stem: str) -> str | None:
|
||||
"""Download one image URL to cache as {stem}.{ext}; return the filename or None.
|
||||
Writes to a temp file then atomically renames, so a reader never sees a half-file."""
|
||||
for key in ("primaryImageSmall", "primaryImage"):
|
||||
url = obj.get(key)
|
||||
if not url:
|
||||
continue
|
||||
if not url:
|
||||
return None
|
||||
try:
|
||||
data, ctype = _http_bytes(url)
|
||||
except Exception: # noqa: BLE001
|
||||
return None
|
||||
if not ctype.startswith("image/") or len(data) < _MIN_IMAGE_BYTES:
|
||||
return None
|
||||
ext = ".png" if "png" in ctype else ".jpg"
|
||||
fname = f"{stem}{ext}"
|
||||
cdir = cache_dir()
|
||||
tmp = cdir / f".{stem}.tmp"
|
||||
try:
|
||||
tmp.write_bytes(data)
|
||||
os.replace(tmp, cdir / fname) # atomic
|
||||
except OSError:
|
||||
try:
|
||||
data, ctype = _http_bytes(url)
|
||||
except Exception: # noqa: BLE001
|
||||
continue
|
||||
if not ctype.startswith("image/") or len(data) < _MIN_IMAGE_BYTES:
|
||||
continue
|
||||
ext = ".png" if "png" in ctype else ".jpg"
|
||||
fname = f"{object_id}{ext}"
|
||||
cdir = cache_dir()
|
||||
tmp = cdir / f".{object_id}.tmp"
|
||||
try:
|
||||
tmp.write_bytes(data)
|
||||
os.replace(tmp, cdir / fname) # atomic
|
||||
tmp.unlink()
|
||||
except OSError:
|
||||
try:
|
||||
tmp.unlink()
|
||||
except OSError:
|
||||
pass
|
||||
return None
|
||||
return fname
|
||||
return None
|
||||
pass
|
||||
return None
|
||||
return fname
|
||||
|
||||
|
||||
def _download_image(obj: dict, object_id: int) -> str | None:
|
||||
"""Cache the day's images to our origin. Stores the web-large display copy as
|
||||
{id}.{ext} (what the page shows) and, when available, the full-resolution copy as
|
||||
{id}-full.{ext} (what the lightbox opens, so zoom fills the screen). Returns the
|
||||
display filename or None if even the display copy couldn't be fetched."""
|
||||
display = _fetch_to_cache(obj.get("primaryImageSmall") or obj.get("primaryImage"), str(object_id))
|
||||
if not display:
|
||||
return None
|
||||
full_url = obj.get("primaryImage")
|
||||
if full_url and full_url != obj.get("primaryImageSmall"):
|
||||
_fetch_to_cache(full_url, f"{object_id}-full") # best-effort hi-res for zoom
|
||||
return display
|
||||
|
||||
|
||||
def _candidates(conn: sqlite3.Connection, art_date: str, source: str) -> list[int]:
|
||||
|
||||
Reference in New Issue
Block a user