import { useTranslation } from "react-i18next";
import { CheckCircle2, Circle, Film, Info, ListPlus, Play, Star, Tv2 } from "lucide-react";
import { type CSSProperties } from "react";
import { formatRuntime } from "../lib/format";
import { Carousel } from "./ui/Carousel";
import type { PlexCard, PlexCastMember, PlexFilters, PlexSeasonDetail } from "../lib/api";
import { navItemAttrs } from "../lib/listNav";
import { PLAYABLE_TINT, playableClass } from "./plexPlayable";
// Presentational Plex cards + strips, split out of PlexBrowse (all purely prop-driven, no shared
// state). Rendered by the browse grid, the person page, and the series show/season views.
// The reveal class for a status pill (watched / in-progress) that shares a corner with a hover-reveal
// control (the watched-toggle). When that control is present it takes over on touch, so the pill yields
// via `.reveal-idle` (hidden on touch, faded on hover); when there's no such control the pill is the
// sole indicator and just fades on hover. (The single-episode card, where the pill is itself the
// full-size watched marker with no fade, keeps its own inline rule.)
const swapBadgeReveal = (hasControl: boolean): string =>
hasControl ? "reveal-idle" : "group-hover:opacity-0";
// Watched-toggle icon. Distinguishes state by SHAPE (a filled check vs an empty circle), not color
// alone — on touch the toggle is the only surviving watched indicator (the text pill is hidden), so a
// green-vs-white CheckCircle2 would be a use-of-color (WCAG 1.4.1) gap. Watched = CheckCircle2 (green),
// not-watched = Circle (empty, white).
function WatchedIcon({ watched }: { watched: boolean }) {
return watched ? (
) : (
);
}
export function PlexPosterCard({
card,
onOpen,
onInfo,
onToggleWatched,
menuProps,
navIndex,
}: {
card: PlexCard;
onOpen: () => void;
onInfo: () => void;
onToggleWatched: (c: PlexCard) => void;
/** Right-click / long-press props for the GRID's one context menu (E24 S3 — the comfort pin).
* A function of the card rather than a ready-made object, so every card surface in the app takes
* `menuProps={menu.triggerProps}` and reads the same at every call site. */
menuProps?: (c: PlexCard) => Record;
/** Position in a keyboard-walkable grid or rail (lib/listNav); the poster is what Enter opens. */
navIndex?: number;
}) {
const { t } = useTranslation();
const inProgress = (card.position_seconds ?? 0) > 0 && card.status !== "watched";
const isPlayable = card.type !== "show"; // movies/episodes play; shows open the drill-down
const watchedLabel = card.status === "watched" ? t("plex.markUnwatched") : t("plex.markWatched");
const runtime = formatRuntime(card.duration_seconds);
const pct =
inProgress && card.duration_seconds
? Math.min(100, Math.round(((card.position_seconds ?? 0) / card.duration_seconds) * 100))
: 0;
return (
{
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
onOpen();
}
}}
className="glass-card glass-hover relative aspect-2/3 rounded-xl overflow-hidden transition-all duration-200 cursor-pointer"
>
{/* Playable-cost tint (always shown — a quick "will it direct-play" cue). z-20 keeps the dot
ABOVE the full-card hover-reveal overlay below (absolute inset-0, no pointer-events-none):
without it the overlay sits on top and swallows the cursor, so the dot's native `title`
tooltip never fired on hover (a regression once the reveal overlay was added). */}
{card.playable &&
(() => {
const cls = playableClass(card.playable, card.transcode_hw);
return (
);
})()}
{/* Rest-state watched badge (top-right). Yields to the hover quick-actions on a playable card
(reveal-idle: gone on touch where the toggle is always shown); a watched show keeps it. */}
{card.status === "watched" && (
{t("plex.watched")}
)}
{/* Aggregate in-progress badge for a partially-watched show. */}
{card.type === "show" && card.status === "in_progress" && (
{t("plex.inProgress")}
)}
{/* Rest-state bottom badges — hidden on hover, where the scrim takes over the bottom. */}
{card.type === "show" && card.season_count != null && (
{t("plex.seasons", { count: card.season_count })}
)}
{card.type === "show" ? : }
{/* Hover reveal: a soft dim, a less-flat play ring, and a bottom scrim that slides up with the
synopsis + rating (the info you'd otherwise click through for). */}
{/* Quick watched toggle (whole season), on hover. */}
{onToggleWatched && (
)}
{/* Add whole season to a playlist, on hover. */}
{onAdd && (
)}
{watched && (
{t("plex.watched")}
)}
{/* In-progress lives at the top-LEFT (the watched-toggle owns the top-right) and stays visible
on touch: unlike movie/episode cards, a season has no progress bar, and the always-shown
toggle reads as unwatched for a partial season — so this badge is the sole in-progress cue
and must not be a reveal-idle swap-badge (which display:none-s on touch). */}
{se.status === "in_progress" && (
{t("plex.inProgress")}
)}
{t("plex.series.episodeCount", { count: se.episode_count })}
{/* Name + role are each capped to ONE line (truncate → ellipsis) so every tile has the
same text height and the row of circles sits on one baseline regardless of name length. */}