diff --git a/frontend/src/components/PlexBrowse.tsx b/frontend/src/components/PlexBrowse.tsx
index 6aedf49d..cca253f1 100644
--- a/frontend/src/components/PlexBrowse.tsx
+++ b/frontend/src/components/PlexBrowse.tsx
@@ -6,6 +6,7 @@ import {
useRef,
useState,
type CSSProperties,
+ type ReactNode,
} from "react";
import { qk } from "../lib/queryKeys";
import { createPortal } from "react-dom";
@@ -985,6 +986,79 @@ function EpisodeCard({
);
}
+/** The shared detail-page hero shell used by BOTH the show page and the season subpage (they were
+ * copy-paste siblings). Owns the glass frame, the play/resume poster button, the customize-menu slot,
+ * and the flex info column; the caller passes the customize menu + the title/metadata/actions as
+ * `children`. Poster is slightly larger on the show page (default) than the season page. */
+function DetailHero({
+ thumb,
+ posterTitle,
+ posterDisabled,
+ showPlayOverlay,
+ onPosterClick,
+ posterSizeClass = "w-32 sm:w-44",
+ customizeMenu,
+ children,
+}: {
+ thumb: string;
+ posterTitle: string;
+ posterDisabled: boolean;
+ showPlayOverlay: boolean;
+ onPosterClick: () => void;
+ posterSizeClass?: string;
+ customizeMenu: ReactNode;
+ children: ReactNode;
+}) {
+ return (
+
+
{customizeMenu}
+
+
+ {showPlayOverlay && (
+
+ )}
+
+
{children}
+
+ );
+}
+
+/** The "mark whole show/season watched" hero action (icon + label flip on state). Verbatim in both
+ * the show and season heroes; the labels differ, so the caller passes both. */
+function WatchedToggle({
+ watched,
+ busy,
+ onToggle,
+ watchedLabel,
+ unwatchedLabel,
+}: {
+ watched: boolean;
+ busy?: boolean;
+ onToggle: () => void;
+ watchedLabel: string;
+ unwatchedLabel: string;
+}) {
+ return (
+
+ );
+}
+
function PlexShowView({
showId,
onBack,
@@ -1050,8 +1124,13 @@ function PlexShowView({
) : (
<>
{/* Hero */}
-
-
+ show.resume && onPlay(show.resume.id, allKeys)}
+ customizeMenu={
0}
/>
-
-
show.resume && onPlay(show.resume.id, allKeys)}
- disabled={!show.resume}
- className="group/poster relative w-32 sm:w-44 shrink-0 self-start"
- title={show.status === "new" ? t("plex.play") : t("plex.resume")}
- >
-
- {show.resume && (
-
- )}
-
-
-
{show.title}
-
- {show.year != null && (
- onFilter({ yearMin: show.year, yearMax: show.year })
- : undefined
- }
- >
- {show.year}
-
- )}
- {show.content_rating && (
- onFilter({ contentRatings: [show.content_rating!] })
- : undefined
- }
- >
- · {show.content_rating}
-
- )}
- {show.season_count != null && (
- · {t("plex.seasons", { count: show.season_count })}
- )}
- {show.imdb_rating != null && (
- onFilter({ ratingMin: Math.floor(show.imdb_rating!) })
- : undefined
- }
- className={`inline-flex items-center gap-1 ${onFilter ? "cursor-pointer hover:text-accent" : "cursor-default"}`}
- >
- {/* decorative rating gold — intentionally NOT a status token */}
- ·
- {show.imdb_rating}
-
- )}
-
- {show.genres.length > 0 && (
-
- {show.genres.map((g) => (
- onFilter({ genres: [g] }) : undefined}
- >
- {g}
-
- ))}
-
- )}
- {show.summary && (
-
- {show.summary}
-
- )}
-
- {show.resume && (
-
onPlay(show.resume!.id, allKeys)}
- icon={Play}
- label={show.status === "new" ? t("plex.play") : t("plex.resume")}
- sub={epLabel(show.resume)}
- />
- )}
- {show.first && show.status !== "new" && (
- onPlay(show.first!.id, allKeys)}
- icon={RotateCcw}
- label={t("plex.series.playFromStart")}
- />
- )}
- markAll(!watched)}
- disabled={busy}
- icon={watched ? CheckCheck : Check}
- label={
- watched ? t("plex.series.markShowUnwatched") : t("plex.series.markShowWatched")
+ }
+ >
+ {show.title}
+
+ {show.year != null && (
+
onFilter({ yearMin: show.year, yearMax: show.year })
+ : undefined
}
- />
- {allKeys.length > 0 && (
-
- setAddTarget({ kind: "group", ratingKeys: allKeys, title: show.title })
- }
- icon={ListPlus}
- label={t("plex.playlist.addShow")}
- />
- )}
- {isAdmin && showLib && (
- setCollOpen(true)}
- icon={Layers}
- label={t("plex.series.addShowCollection")}
- />
- )}
-
+ >
+ {show.year}
+
+ )}
+ {show.content_rating && (
+ onFilter({ contentRatings: [show.content_rating!] })
+ : undefined
+ }
+ >
+ · {show.content_rating}
+
+ )}
+ {show.season_count != null && (
+ · {t("plex.seasons", { count: show.season_count })}
+ )}
+ {show.imdb_rating != null && (
+ onFilter({ ratingMin: Math.floor(show.imdb_rating!) })
+ : undefined
+ }
+ className={`inline-flex items-center gap-1 ${onFilter ? "cursor-pointer hover:text-accent" : "cursor-default"}`}
+ >
+ {/* decorative rating gold — intentionally NOT a status token */}
+ ·
+ {show.imdb_rating}
+
+ )}
-
+ {show.genres.length > 0 && (
+
+ {show.genres.map((g) => (
+ onFilter({ genres: [g] }) : undefined}
+ >
+ {g}
+
+ ))}
+
+ )}
+ {show.summary && (
+
{show.summary}
+ )}
+
+ {show.resume && (
+
onPlay(show.resume!.id, allKeys)}
+ icon={Play}
+ label={show.status === "new" ? t("plex.play") : t("plex.resume")}
+ sub={epLabel(show.resume)}
+ />
+ )}
+ {show.first && show.status !== "new" && (
+ onPlay(show.first!.id, allKeys)}
+ icon={RotateCcw}
+ label={t("plex.series.playFromStart")}
+ />
+ )}
+ markAll(!watched)}
+ watchedLabel={t("plex.series.markShowUnwatched")}
+ unwatchedLabel={t("plex.series.markShowWatched")}
+ />
+ {allKeys.length > 0 && (
+
+ setAddTarget({ kind: "group", ratingKeys: allKeys, title: show.title })
+ }
+ icon={ListPlus}
+ label={t("plex.playlist.addShow")}
+ />
+ )}
+ {isAdmin && showLib && (
+ setCollOpen(true)}
+ icon={Layers}
+ label={t("plex.series.addShowCollection")}
+ />
+ )}
+
+
{/* Seasons — in a glassy panel like the movie info-page strips. */}
) : (
<>
-
-
+ se.resume && onPlay(se.resume.id, allKeys)}
+ posterSizeClass="w-28 sm:w-40"
+ customizeMenu={
-
+ }
+ >
se.resume && onPlay(se.resume.id, allKeys)}
- disabled={!se.resume}
- className="group/poster relative w-28 sm:w-40 shrink-0 self-start"
- title={se.status === "new" ? t("plex.play") : t("plex.resume")}
+ onClick={onBack}
+ className="text-sm text-muted hover:text-accent transition text-left"
>
-
- {se.resume && (
-
- )}
+ {d.show.title}
-
-
- {d.show.title}
-
-
{se.title}
-
- {t("plex.series.episodeCount", { count: se.episode_count })}
-
-
- {se.resume && (
-
onPlay(se.resume!.id, allKeys)}
- icon={Play}
- label={se.status === "new" ? t("plex.play") : t("plex.resume")}
- sub={epLabel(se.resume)}
- />
- )}
- {se.first && se.status !== "new" && (
- onPlay(se.first!.id, allKeys)}
- icon={RotateCcw}
- label={t("plex.series.playFromStart")}
- />
- )}
+ {se.title}
+
+ {t("plex.series.episodeCount", { count: se.episode_count })}
+
+
+ {se.resume && (
markAll(!watched)}
- disabled={busy}
- icon={watched ? CheckCheck : Check}
- label={
- watched
- ? t("plex.series.markSeasonUnwatched")
- : t("plex.series.markSeasonWatched")
- }
+ primary
+ onClick={() => onPlay(se.resume!.id, allKeys)}
+ icon={Play}
+ label={se.status === "new" ? t("plex.play") : t("plex.resume")}
+ sub={epLabel(se.resume)}
/>
- {seasonKeys.length > 0 && (
-
- setAddTarget({
- kind: "group",
- ratingKeys: seasonKeys,
- title: `${d.show.title} — ${se.title}`,
- })
- }
- icon={ListPlus}
- label={t("plex.playlist.addSeason")}
- />
- )}
-
+ )}
+ {se.first && se.status !== "new" && (
+ onPlay(se.first!.id, allKeys)}
+ icon={RotateCcw}
+ label={t("plex.series.playFromStart")}
+ />
+ )}
+ markAll(!watched)}
+ watchedLabel={t("plex.series.markSeasonUnwatched")}
+ unwatchedLabel={t("plex.series.markSeasonWatched")}
+ />
+ {seasonKeys.length > 0 && (
+
+ setAddTarget({
+ kind: "group",
+ ratingKeys: seasonKeys,
+ title: `${d.show.title} — ${se.title}`,
+ })
+ }
+ icon={ListPlus}
+ label={t("plex.playlist.addSeason")}
+ />
+ )}
-
+
{se.episodes.map((ep) => (