E5 S3: unified Plex card — scrim-reveal hover + rest polish
The poster card (grid + person page) gets the Seerr-style hover: on hover a bottom scrim slides up with the title, a "year · runtime · ★rating" meta line, and a 3-line synopsis — the info you'd otherwise click through for — plus a less-flat play ring (radial-gradient + ring shadow) and a top-right quick-action cluster (Info + watched-toggle, revealed on hover / always on touch). Rest-state badges (season / type tag) yield to the scrim on hover; the playable-cost tint stays. (§1 A + C from the enrichment study.) Backend (the data the scrim needs, mirroring S1's `art`): the grid (browse.unified_library) now emits a lang-coalesced, truncated `summary` (new query.summary_col) + the Plex `rating`; movie_card/show_card do the same (EN) for the detail/related/person cards. PlexCard.rating type added (summary already existed for episodes). Gate: tsc x3 / eslint / prettier / knip / vitest 326 green; ruff green; backend lane full suite green. Self-E2E on the live dev server (rebuilt on this branch, S2-enriched): the grid serves rating+summary (Shawshank 9.3, Godfather 9.2, real synopses) and the card renders the play ring + scrim (The Expanse: ring, synopsis, ★8.1) — verified via the DOM (the pane wouldn't composite for a screenshot).
This commit is contained in:
@@ -14,12 +14,21 @@ def _art_url(rating_key: str, art_key: str | None) -> str | None:
|
||||
return f"/api/plex/image/{rating_key}?variant=art" if art_key else None
|
||||
|
||||
|
||||
# Card hover-overview: a short synopsis for the reveal panel, trimmed so a strip/related row of cards
|
||||
# stays a few hundred bytes each rather than carrying full synopses. (These non-grid card paths serve
|
||||
# the EN summary/rating; the main grid coalesces the HU twin in browse.unified_library.)
|
||||
def _card_summary(summary: str | None) -> str | None:
|
||||
return (summary or "")[:300] or None
|
||||
|
||||
|
||||
def movie_card(it: PlexItem, st: PlexState | None) -> dict:
|
||||
return {
|
||||
"id": it.rating_key,
|
||||
"type": "movie",
|
||||
"title": it.title,
|
||||
"year": it.year,
|
||||
"rating": it.rating,
|
||||
"summary": _card_summary(it.summary),
|
||||
"duration_seconds": it.duration_s,
|
||||
"thumb": f"/api/plex/image/{it.rating_key}",
|
||||
"art": _art_url(it.rating_key, it.art_key),
|
||||
@@ -35,6 +44,8 @@ def show_card(sh: PlexShow, status: str = "new") -> dict:
|
||||
"type": "show",
|
||||
"title": sh.title,
|
||||
"year": sh.year,
|
||||
"rating": sh.rating,
|
||||
"summary": _card_summary(sh.summary),
|
||||
"thumb": f"/api/plex/image/{sh.rating_key}",
|
||||
"art": _art_url(sh.rating_key, sh.art_key),
|
||||
"season_count": sh.child_count,
|
||||
|
||||
@@ -29,6 +29,11 @@ def title_col(model, lang: str):
|
||||
return func.coalesce(model.title_hu, model.title) if lang == "hu" else model.title
|
||||
|
||||
|
||||
def summary_col(model, lang: str):
|
||||
"""DISPLAY summary, HU-coalesced (EN fallback) — the card hover-overview + info page read this."""
|
||||
return func.coalesce(model.summary_hu, model.summary) if lang == "hu" else model.summary
|
||||
|
||||
|
||||
def genres_col(model, lang: str):
|
||||
# type_coerce so the JSONB comparator (`.contains` / `@>`, GIN-backed) survives the coalesce.
|
||||
if lang == "hu":
|
||||
|
||||
@@ -38,6 +38,7 @@ from app.plex.query import (
|
||||
movie_status_case,
|
||||
show_status_case,
|
||||
split_csv,
|
||||
summary_col,
|
||||
title_col,
|
||||
vector_col,
|
||||
vector_match,
|
||||
@@ -130,6 +131,8 @@ def unified_library(
|
||||
title_col(PlexItem, lang).label("title"),
|
||||
PlexItem.year.label("year"),
|
||||
PlexItem.rating.label("rating"),
|
||||
# Truncated so the card hover-overview costs a few hundred bytes, not the full synopsis.
|
||||
func.left(summary_col(PlexItem, lang), 300).label("summary"),
|
||||
PlexItem.added_at.label("added_at"),
|
||||
PlexItem.originally_available_at.label("rel"),
|
||||
PlexItem.duration_s.label("duration_s"),
|
||||
@@ -170,6 +173,7 @@ def unified_library(
|
||||
title_col(PlexShow, lang).label("title"),
|
||||
PlexShow.year.label("year"),
|
||||
PlexShow.rating.label("rating"),
|
||||
func.left(summary_col(PlexShow, lang), 300).label("summary"),
|
||||
PlexShow.added_at.label("added_at"),
|
||||
PlexShow.originally_available_at.label("rel"),
|
||||
cast(null(), Integer).label("duration_s"),
|
||||
@@ -227,6 +231,8 @@ def unified_library(
|
||||
"type": "movie",
|
||||
"title": r.title,
|
||||
"year": r.year,
|
||||
"rating": r.rating,
|
||||
"summary": r.summary,
|
||||
"duration_seconds": r.duration_s,
|
||||
"thumb": f"/api/plex/image/{r.rk}",
|
||||
"playable": r.playable,
|
||||
@@ -241,6 +247,8 @@ def unified_library(
|
||||
"type": "show",
|
||||
"title": r.title,
|
||||
"year": r.year,
|
||||
"rating": r.rating,
|
||||
"summary": r.summary,
|
||||
"thumb": f"/api/plex/image/{r.rk}",
|
||||
"season_count": r.season_count,
|
||||
"status": r.status,
|
||||
|
||||
Reference in New Issue
Block a user