Files
romm/backend/utils/screenshots.py
Georges-Antoine Assi af4b057894 feat(v2): screenshot-forward covers + cover-art PIP for continue-playing & activity
Show a "where you left off" screenshot on the Home continue-playing rail and
the live-activity board, with a small cover-art thumbnail (PIP) in the corner
so the game stays identifiable. Both render at the image's natural aspect.

Backend:
- New shared util `continue_playing_screenshot(rom, latest_save)` resolving the
  image in priority order: latest save's screenshot, then title screen, then
  first gameplay screenshot (None → frontend falls back to cover art).
- `SimpleRomSchema.screenshot_path` populated only on the `last_played` query;
  `get_latest_saves_for_roms` batch handler (+ tests).
- ActivityEntry / ActivityEntrySchema gain `screenshot_path`, computed from the
  session player's latest save in both the socket and REST heartbeat paths.

Frontend:
- New shared `CoverArtPip.vue` (bottom-right 2D cover thumbnail), reused by
  GameCard and ActivityCard.
- Home continue-playing rail uses `screenshot_path` + PIP, natural aspect (no
  forced hero/style).
- Activity board: screenshot-forward cover + PIP, and a wrapping flex layout so
  cards share a uniform height with natural-ratio widths (gallery-card
  behavior).
- GameCover only keys the measured ratio by rom id for the rom's own cover, so
  a `coverSrc` override (screenshot) never pollutes the gallery's ratio cache.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 17:40:56 -04:00

38 lines
1.3 KiB
Python

import os
from config import FRONTEND_RESOURCES_PATH
from models.assets import Save
from models.rom import Rom
# Image formats every modern browser can decode. Shared by the per-ROM
# (endpoints/roms/screenshot.py) and per-user (endpoints/screenshots.py)
# screenshot upload endpoints.
ALLOWED_SCREENSHOT_EXTENSIONS = frozenset(
{".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp", ".avif"}
)
def is_allowed_screenshot_file(file_name: str) -> bool:
_, ext = os.path.splitext(file_name)
return ext.lower() in ALLOWED_SCREENSHOT_EXTENSIONS
def continue_playing_screenshot(rom: Rom, latest_save: Save | None) -> str | None:
"""The "where you left off" image for a rom, in priority order: the latest
save's screenshot, then the title screen, then the first gameplay
screenshot. None lets the frontend fall back to cover art. Shared by the
continue-playing rail and the live-activity board."""
if latest_save is not None and latest_save.screenshot is not None:
return latest_save.screenshot.download_path
title_screen = (rom.ss_metadata or {}).get("title_screen_path") or (
rom.gamelist_metadata or {}
).get("title_screen_path")
if title_screen:
return f"{FRONTEND_RESOURCES_PATH}/{title_screen}"
if rom.merged_screenshots:
return rom.merged_screenshots[0]
return None