feat(plex): a title opens its page, the picture plays; an episode links up
A card title lit like a link but played the item. Now the picture plays and the title opens the page of what it names: a movie's or an episode's info page, a show's page, and for an episode standing as its show (Recently watched) the show. Every poster and episode card carries it (grid, rails, person, collection, season, related, Home), a card with no page keeps a plain title, and the season page gains the menu's Details row too. The episode page had no way up: the detail now serves the show's and the season's rating keys, and on the page the show title opens the show and "S1·E25" the season (Back returns to where the page was).
This commit is contained in:
@@ -504,6 +504,7 @@ def item_detail(
|
||||
|
||||
prev_id, next_id = _episode_nav(db, it)
|
||||
show = db.get(PlexShow, it.show_id) if it.kind == "episode" and it.show_id else None
|
||||
season = db.get(PlexSeason, it.season_id) if show and it.season_id else None
|
||||
return {
|
||||
"id": it.rating_key,
|
||||
"kind": it.kind,
|
||||
@@ -537,6 +538,9 @@ def item_detail(
|
||||
"status": st.status if st else "new",
|
||||
"position_seconds": st.position_seconds if st else 0,
|
||||
"show_title": ((show.title_hu or show.title if hu else show.title) if show else None),
|
||||
# The episode page links up to its show and its season (their rating keys = the view keys).
|
||||
"show_id": show.rating_key if show else None,
|
||||
"season_id": season.rating_key if season else None,
|
||||
"season_number": it.season_number,
|
||||
"episode_number": it.episode_number,
|
||||
"prev_id": prev_id,
|
||||
|
||||
@@ -19,7 +19,7 @@ import app.state as state
|
||||
from app.auth import current_user
|
||||
from app.db import get_db
|
||||
from app.main import app
|
||||
from app.models import PlexItem, PlexLibrary, PlexShow, User
|
||||
from app.models import PlexItem, PlexLibrary, PlexSeason, PlexShow, User
|
||||
from app.plex import detail
|
||||
from app.plex.sync import _apply_facet_fields, _enrich_people_batch
|
||||
|
||||
@@ -276,8 +276,16 @@ def test_item_detail_episode_serves_rich_from_live(db, monkeypatch, override_dep
|
||||
show = PlexShow(rating_key="s1", library_id=lib.id, title="A Show")
|
||||
db.add(show)
|
||||
db.flush()
|
||||
season = PlexSeason(rating_key="se1", show_id=show.id, season_number=1)
|
||||
db.add(season)
|
||||
db.flush()
|
||||
ep = PlexItem(
|
||||
rating_key="e1", library_id=lib.id, kind="episode", title="Pilot", show_id=show.id
|
||||
rating_key="e1",
|
||||
library_id=lib.id,
|
||||
kind="episode",
|
||||
title="Pilot",
|
||||
show_id=show.id,
|
||||
season_id=season.id,
|
||||
)
|
||||
db.add(ep)
|
||||
db.commit()
|
||||
@@ -289,6 +297,8 @@ def test_item_detail_episode_serves_rich_from_live(db, monkeypatch, override_dep
|
||||
assert resp.status_code == 200, resp.text
|
||||
body = resp.json()
|
||||
assert body["imdb_id"] == "tt0068646" # from the live meta, not the (NULL) row
|
||||
# The page's links up to the show and the season.
|
||||
assert (body["show_id"], body["season_id"]) == ("s1", "se1")
|
||||
assert [c["name"] for c in body["cast"]] == ["Marlon Brando", "Al Pacino"]
|
||||
db.refresh(ep)
|
||||
assert ep.full_cast is None # episodes are not persisted/enriched
|
||||
|
||||
@@ -663,7 +663,7 @@ describe("Home", () => {
|
||||
comfort: [],
|
||||
});
|
||||
mount();
|
||||
fireEvent.click(await screen.findByText("A Show"));
|
||||
fireEvent.click(await screen.findByRole("button", { name: "A Show" }));
|
||||
expect(setJumpTo).toHaveBeenCalledWith(expect.objectContaining({ id: "ep-7", type: "play" }));
|
||||
});
|
||||
|
||||
@@ -678,11 +678,24 @@ describe("Home", () => {
|
||||
comfort: [],
|
||||
});
|
||||
mount();
|
||||
fireEvent.click(await screen.findByText("A Show"));
|
||||
fireEvent.click(await screen.findByRole("button", { name: "A Show" }));
|
||||
expect(setJumpTo).toHaveBeenCalledWith(expect.objectContaining({ replace: true }));
|
||||
expect(setPage).toHaveBeenCalledWith("plex");
|
||||
});
|
||||
|
||||
it("a show tile's title opens the show page; its picture plays on", async () => {
|
||||
discover.mockResolvedValue({
|
||||
budget: "any",
|
||||
hero: null,
|
||||
continue_watching: [showCard()],
|
||||
comfort: [],
|
||||
});
|
||||
mount();
|
||||
fireEvent.click(await screen.findByText("A Show"), { detail: 1 });
|
||||
expect(setJumpTo).toHaveBeenCalledWith(expect.objectContaining({ type: "show" }));
|
||||
expect(setJumpTo).not.toHaveBeenCalledWith(expect.objectContaining({ type: "play" }));
|
||||
});
|
||||
|
||||
it("sends the ⓘ affordance to the item's page, not to the player", async () => {
|
||||
// `onInfo` and `onPlay` differ by one ternary arm, and confusing them is the defect the play
|
||||
// test above was written for — but only `onPlay` was pinned, so swapping ⓘ's "movie" for "play"
|
||||
|
||||
@@ -371,6 +371,7 @@ function Rail({
|
||||
onAdd={plexAdd}
|
||||
next={c.next?.plex}
|
||||
onInfo={() => onInfo(c)}
|
||||
onPage={() => onInfo(c)}
|
||||
menuProps={menuFor[i]}
|
||||
menuButtonProps={buttonFor[i]}
|
||||
menuOpen={menu.open && menu.target === c}
|
||||
|
||||
@@ -338,8 +338,11 @@ export default function PlexBrowse() {
|
||||
scrollRef.current = scroller()?.scrollTop ?? 0;
|
||||
sub.open({ kind: "player", id: card.id, fromStart: true });
|
||||
}
|
||||
// A card's page: a show's own page, a movie's or an episode's info page (the card menu's "Details",
|
||||
// and a card title's link).
|
||||
function onInfo(card: PlexCard) {
|
||||
scrollRef.current = scroller()?.scrollTop ?? 0;
|
||||
if (card.type === "show") return void sub.open({ kind: "show", id: card.id });
|
||||
rememberInfoScroll(0); // fresh info page starts at the top
|
||||
sub.open({ kind: "info", id: card.id });
|
||||
}
|
||||
@@ -461,6 +464,20 @@ export default function PlexBrowse() {
|
||||
onPlayItem={(id, fromStart) => sub.open({ kind: "player", id, fromStart })}
|
||||
onFilter={applyFilter}
|
||||
onOpenPerson={onOpenPerson}
|
||||
// Up to the show or season: Back returns to this page where it was (as from a cast row).
|
||||
onOpenShow={(showId) => {
|
||||
rememberInfoScroll(scroller()?.scrollTop ?? 0);
|
||||
sub.open({ kind: "show", id: showId });
|
||||
}}
|
||||
onOpenSeason={(showId, seasonId) => {
|
||||
rememberInfoScroll(scroller()?.scrollTop ?? 0);
|
||||
sub.open({ kind: "season", showId, seasonId });
|
||||
}}
|
||||
// A collection strip's title: another info page, from its top.
|
||||
onOpenInfo={(id) => {
|
||||
rememberInfoScroll(0);
|
||||
sub.open({ kind: "info", id });
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -504,6 +521,7 @@ export default function PlexBrowse() {
|
||||
showId={showId}
|
||||
seasonId={seasonId}
|
||||
onBack={sub.back}
|
||||
onInfoCard={onInfo}
|
||||
onPlay={(epRk, queue, fromStart) =>
|
||||
sub.open({ kind: "player", id: epRk, queue, fromStart })
|
||||
}
|
||||
@@ -707,6 +725,7 @@ export default function PlexBrowse() {
|
||||
navIndex={i}
|
||||
card={c}
|
||||
onOpen={() => onCard(c)}
|
||||
onPage={onInfo}
|
||||
onRestart={() => onRestartCard(c)}
|
||||
onToggleWatched={toggleWatched}
|
||||
onAdd={actions.add}
|
||||
@@ -730,6 +749,7 @@ export default function PlexBrowse() {
|
||||
ep={ep}
|
||||
withShowTitle
|
||||
onPlay={() => onCard(ep)}
|
||||
onPage={() => onInfo(ep)}
|
||||
onRestart={() => onRestartCard(ep)}
|
||||
onToggleWatched={() => toggleWatched(ep)}
|
||||
onAdd={() => epActions.add(ep)}
|
||||
|
||||
@@ -431,6 +431,7 @@ export default function PlexCollectionView({
|
||||
card={c}
|
||||
navIndex={i}
|
||||
onOpen={() => onOpenCard(c)}
|
||||
onPage={onInfoCard}
|
||||
onRestart={() => onRestartCard(c)}
|
||||
onToggleWatched={toggleWatched}
|
||||
onAdd={actions.add}
|
||||
|
||||
@@ -78,6 +78,7 @@ function CardRail({
|
||||
card={c}
|
||||
navIndex={i}
|
||||
onOpen={() => onOpenCard(c)}
|
||||
onPage={onInfoCard}
|
||||
onRestart={() => onRestartCard(c)}
|
||||
onToggleWatched={onToggleWatched}
|
||||
onAdd={actions.add}
|
||||
|
||||
@@ -173,6 +173,55 @@ describe("PlexInfo enriched detail (page)", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("an episode page links up to its show and season", () => {
|
||||
const episode: PlexItemDetail = {
|
||||
...detail,
|
||||
id: "e1",
|
||||
kind: "episode",
|
||||
title: "Pilot",
|
||||
show_title: "A Show",
|
||||
show_id: "s1",
|
||||
season_id: "se1",
|
||||
season_number: 1,
|
||||
episode_number: 25,
|
||||
tmdb: null,
|
||||
};
|
||||
|
||||
it("the show title opens the show, the episode number its season", () => {
|
||||
const onOpenShow = vi.fn();
|
||||
const onOpenSeason = vi.fn();
|
||||
wrap(
|
||||
<PlexInfo
|
||||
onPlayItem={noop}
|
||||
detail={episode}
|
||||
variant="page"
|
||||
onOpenShow={onOpenShow}
|
||||
onOpenSeason={onOpenSeason}
|
||||
/>,
|
||||
);
|
||||
fireEvent.click(screen.getByRole("button", { name: "A Show" }));
|
||||
expect(onOpenShow).toHaveBeenCalledWith("s1");
|
||||
fireEvent.click(screen.getByRole("button", { name: "S1·E25" }));
|
||||
expect(onOpenSeason).toHaveBeenCalledWith("s1", "se1");
|
||||
expect(screen.getByText(/— Pilot/)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("over the running player the header stays plain text", () => {
|
||||
wrap(
|
||||
<PlexInfo
|
||||
onPlayItem={noop}
|
||||
detail={episode}
|
||||
variant="overlay"
|
||||
onOpenShow={noop}
|
||||
onOpenSeason={noop}
|
||||
/>,
|
||||
);
|
||||
expect(screen.queryByRole("button", { name: "A Show" })).toBeNull();
|
||||
expect(screen.queryByRole("button", { name: "S1·E25" })).toBeNull();
|
||||
expect(screen.getByText("S1·E25 — Pilot")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
// The owner's layout: a main column (collection strips → crew → cast) beside a sidebar of
|
||||
// facts (details, keywords); a column with nothing in it is not rendered and the other takes the width.
|
||||
// The owner's pick: the hero's text stopped at ~55% of the panel. The facts and the key crew are
|
||||
|
||||
@@ -11,6 +11,8 @@ import { DetailAside, DetailAsideToggles, DetailKeywords } from "./plexDetailAsi
|
||||
import {
|
||||
DETAIL_POSTER,
|
||||
episodeLine,
|
||||
episodeName,
|
||||
episodeNumbers,
|
||||
tmdbBackdropUrl,
|
||||
useCrossfadeBackdrop,
|
||||
useDetailAside,
|
||||
@@ -186,6 +188,11 @@ type Props = {
|
||||
onPlayItem: (id: string, fromStart?: boolean) => void;
|
||||
// The item's library key — enables the admin "Manage collections" editor (page variant, movies).
|
||||
library?: string;
|
||||
// Page variant, an episode: its show title opens the show page, its "S1·E25" the season page.
|
||||
onOpenShow?: (showId: string) => void;
|
||||
onOpenSeason?: (showId: string, seasonId: string) => void;
|
||||
// Page variant: a collection strip's title opens that title's info page (its picture plays it).
|
||||
onOpenInfo?: (id: string) => void;
|
||||
};
|
||||
|
||||
export default function PlexInfo({
|
||||
@@ -198,6 +205,9 @@ export default function PlexInfo({
|
||||
onOpenPerson,
|
||||
onPlayItem,
|
||||
library,
|
||||
onOpenShow,
|
||||
onOpenSeason,
|
||||
onOpenInfo,
|
||||
}: Props) {
|
||||
const { t, i18n } = useTranslation();
|
||||
const lang = i18n.language;
|
||||
@@ -299,6 +309,12 @@ export default function PlexInfo({
|
||||
const asideShown = !overlay && aside.column;
|
||||
const infoTitle =
|
||||
detail.kind === "episode" && detail.show_title ? detail.show_title : detail.title;
|
||||
// Up from an episode: the show's title and the episode's season are links on the page (not over
|
||||
// the running player, where leaving would stop it).
|
||||
const showId = detail.kind === "episode" && !overlay ? detail.show_id : null;
|
||||
const seasonId = detail.season_id;
|
||||
const numbers = episodeNumbers(detail);
|
||||
const upLink = "hover:text-accent hover:underline";
|
||||
|
||||
return (
|
||||
<div className={overlay ? "relative w-full max-w-3xl" : "relative glass-media"}>
|
||||
@@ -312,11 +328,32 @@ export default function PlexInfo({
|
||||
{overlay ? (
|
||||
<h2 className="text-lg font-bold text-white">{infoTitle}</h2>
|
||||
) : (
|
||||
<PageHeading className="text-2xl font-bold">{infoTitle}</PageHeading>
|
||||
<PageHeading className="text-2xl font-bold">
|
||||
{showId && onOpenShow && detail.show_title ? (
|
||||
<Pressable selectable className={upLink} onClick={() => onOpenShow(showId)}>
|
||||
{infoTitle}
|
||||
</Pressable>
|
||||
) : (
|
||||
infoTitle
|
||||
)}
|
||||
</PageHeading>
|
||||
)}
|
||||
{detail.kind === "episode" && (
|
||||
<p className={`text-sm ${overlay ? "text-white/70" : "text-muted"}`}>
|
||||
{episodeLine(detail)}
|
||||
{showId && seasonId && onOpenSeason && numbers ? (
|
||||
<>
|
||||
<Pressable
|
||||
selectable
|
||||
className={upLink}
|
||||
onClick={() => onOpenSeason(showId, seasonId)}
|
||||
>
|
||||
{episodeName(detail)}
|
||||
</Pressable>
|
||||
{` — ${detail.title}`}
|
||||
</>
|
||||
) : (
|
||||
episodeLine(detail)
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -576,6 +613,7 @@ export default function PlexInfo({
|
||||
shelf
|
||||
navIndex={i}
|
||||
onOpen={() => onPlayItem(m.id)}
|
||||
onPage={!overlay && onOpenInfo ? (c) => onOpenInfo(c.id) : undefined}
|
||||
onRestart={() => onPlayItem(m.id, true)}
|
||||
onToggleWatched={toggleColWatched}
|
||||
onAdd={colActions.add}
|
||||
|
||||
@@ -313,7 +313,7 @@ describe("Plex cards in a keyboard-walkable list", () => {
|
||||
describe("a Plex card's title stays selectable", () => {
|
||||
it("opens from its caption, but not on the click that ends selecting the title", () => {
|
||||
const onOpen = vi.fn();
|
||||
render(<PlexPosterCard card={movie()} onOpen={onOpen} />);
|
||||
render(<PlexPosterCard card={movie()} onOpen={vi.fn()} onPage={onOpen} />);
|
||||
const title = screen.getByText("M");
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(title);
|
||||
@@ -327,10 +327,47 @@ describe("a Plex card's title stays selectable", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// The picture plays; the title, lit like a link, opens the page of what it names (owner).
|
||||
describe("a Plex card's title is a link to its page", () => {
|
||||
it("a movie's title opens its page and never plays it", () => {
|
||||
const onOpen = vi.fn();
|
||||
const onPage = vi.fn();
|
||||
render(<PlexPosterCard card={movie()} onOpen={onOpen} onPage={onPage} />);
|
||||
fireEvent.click(screen.getByText("M"), { detail: 1 });
|
||||
expect(onPage).toHaveBeenCalledWith(expect.objectContaining({ id: "1", type: "movie" }));
|
||||
expect(onOpen).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("an episode standing as its show opens the show", () => {
|
||||
const onPage = vi.fn();
|
||||
const ep: PlexCard = { ...episode(), show_id: "s9", show_title: "The Show" };
|
||||
render(<PlexPosterCard card={ep} onOpen={vi.fn()} onPage={onPage} />);
|
||||
fireEvent.click(screen.getByText("The Show"), { detail: 1 });
|
||||
expect(onPage).toHaveBeenCalledWith(expect.objectContaining({ id: "s9", type: "show" }));
|
||||
});
|
||||
|
||||
it("an episode card's title opens the episode's page, its picture plays", () => {
|
||||
const onPlay = vi.fn();
|
||||
const onPage = vi.fn();
|
||||
render(<EpisodeCard ep={episode()} onPlay={onPlay} onPage={onPage} />);
|
||||
fireEvent.click(screen.getByText(episode().title), { detail: 1 });
|
||||
expect(onPage).toHaveBeenCalledTimes(1);
|
||||
expect(onPlay).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("without a page the title is plain text: a click does nothing", () => {
|
||||
const onOpen = vi.fn();
|
||||
render(<PlexPosterCard card={movie()} onOpen={onOpen} />);
|
||||
fireEvent.click(screen.getByText("M"), { detail: 1 });
|
||||
expect(onOpen).not.toHaveBeenCalled();
|
||||
expect(screen.getByText("M").className).not.toContain("cursor-pointer");
|
||||
});
|
||||
});
|
||||
|
||||
describe("a Plex card's caption and a drag", () => {
|
||||
it("does not open on a press dragged past the slop", () => {
|
||||
const onOpen = vi.fn();
|
||||
render(<PlexPosterCard card={movie()} onOpen={onOpen} />);
|
||||
render(<PlexPosterCard card={movie()} onOpen={vi.fn()} onPage={onOpen} />);
|
||||
const title = screen.getByText("M");
|
||||
fireEvent.mouseDown(title, { clientX: 10, clientY: 10 });
|
||||
fireEvent.click(title, { detail: 1, clientX: 60, clientY: 10 });
|
||||
|
||||
@@ -139,9 +139,11 @@ export function PlexHoverBar({
|
||||
);
|
||||
}
|
||||
|
||||
/** A Plex card's title: it opens the card like its picture does, except for the click that ends
|
||||
* selecting its words, or a press dragged past the slop (DESIGN "Text stays selectable"); a div's
|
||||
* text selects on its own, so only the click needs the guard. */
|
||||
/** A Plex card's title: a link to the page of what it names (a movie's or an episode's info page, a
|
||||
* show's page), while the picture plays it — a title lit like a link that started playback surprised
|
||||
* (owner). Not for the click that ends selecting its words, or a press dragged past the slop (DESIGN
|
||||
* "Text stays selectable"); a div's text selects on its own, so only the click needs the guard.
|
||||
* Without a page to open it is plain text. */
|
||||
function CardTitle({
|
||||
onClick,
|
||||
shelf = false,
|
||||
@@ -267,9 +269,13 @@ export function PlexPosterCard({
|
||||
next,
|
||||
onInfo,
|
||||
onRestart,
|
||||
onPage,
|
||||
}: {
|
||||
card: PlexCard;
|
||||
onOpen: () => void;
|
||||
/** Open a card's page (a show's, or a movie's or an episode's info page): the title's link. It is
|
||||
* handed the card the title NAMES — the show, for an episode standing as its show. */
|
||||
onPage?: (target: PlexCard) => void;
|
||||
/** Play the item (or the `next` episode) from its start, not its resume point (C5). */
|
||||
onRestart?: () => void;
|
||||
onToggleWatched?: (c: PlexCard) => void;
|
||||
@@ -306,6 +312,9 @@ export function PlexPosterCard({
|
||||
const asShow = card.type === "episode" && !wide && !!card.show_id && !!card.show_title;
|
||||
const landscape = wide || (card.type === "episode" && !asShow);
|
||||
const name = asShow ? `${card.show_title} · ${card.title}` : card.title;
|
||||
const titled: PlexCard = asShow
|
||||
? { ...card, type: "show", id: card.show_id!, title: card.show_title! }
|
||||
: card;
|
||||
const ep = card.type === "show" ? (next ?? null) : null;
|
||||
// movies/episodes play; a show opens its drill-down — unless it plays on (`next`)
|
||||
const isPlayable = card.type !== "show" || !!ep;
|
||||
@@ -385,7 +394,7 @@ export function PlexPosterCard({
|
||||
</PlexPicture>
|
||||
}
|
||||
title={
|
||||
<CardTitle onClick={onOpen} shelf={shelf}>
|
||||
<CardTitle onClick={onPage && (() => onPage(titled))} shelf={shelf}>
|
||||
{asShow ? card.show_title : card.title}
|
||||
</CardTitle>
|
||||
}
|
||||
@@ -655,6 +664,8 @@ export function RelatedStrip({
|
||||
shelf
|
||||
navIndex={i}
|
||||
onOpen={() => onOpen(r)}
|
||||
// A related title is a show: its page is what both its picture and its title open.
|
||||
onPage={onOpen}
|
||||
menuProps={menuProps}
|
||||
menuButtonProps={menuButtonProps}
|
||||
menuOpen={openId === r.id}
|
||||
@@ -677,9 +688,12 @@ export function EpisodeCard({
|
||||
menuButtonProps,
|
||||
menuOpen = false,
|
||||
navIndex,
|
||||
onPage,
|
||||
}: {
|
||||
ep: PlexCard;
|
||||
onPlay: () => void;
|
||||
/** The episode's info page: the title's link (the picture plays). */
|
||||
onPage?: () => void;
|
||||
/** Play the episode from its start, not its resume point (C5). */
|
||||
onRestart?: () => void;
|
||||
onAdd?: () => void;
|
||||
@@ -732,7 +746,7 @@ export function EpisodeCard({
|
||||
</PlexPicture>
|
||||
}
|
||||
title={
|
||||
<CardTitle onClick={onPlay}>
|
||||
<CardTitle onClick={onPage}>
|
||||
{!withShowTitle && ep.episode_number != null && (
|
||||
<span className="text-muted mr-1">{ep.episode_number}.</span>
|
||||
)}
|
||||
|
||||
@@ -108,7 +108,15 @@ const ALL = ["e11", "e12", "e21", "e22"];
|
||||
describe("the restart button on a show and a season", () => {
|
||||
it("a season's names the season and its first episode, and starts that episode at 0", async () => {
|
||||
const onPlay = vi.fn();
|
||||
wrap(<PlexSeasonView showId="show" seasonId="s2" onBack={() => {}} onPlay={onPlay} />);
|
||||
wrap(
|
||||
<PlexSeasonView
|
||||
showId="show"
|
||||
seasonId="s2"
|
||||
onBack={() => {}}
|
||||
onPlay={onPlay}
|
||||
onInfoCard={() => {}}
|
||||
/>,
|
||||
);
|
||||
const btn = await screen.findByRole("button", { name: /plex\.series\.seasonFromStart/ });
|
||||
expect(btn.textContent).toContain("S2 · E1");
|
||||
fireEvent.click(btn);
|
||||
|
||||
@@ -78,6 +78,9 @@ export function PlexInfoView({
|
||||
onPlayItem,
|
||||
onFilter,
|
||||
onOpenPerson,
|
||||
onOpenShow,
|
||||
onOpenSeason,
|
||||
onOpenInfo,
|
||||
}: {
|
||||
id: string;
|
||||
onBack: () => void;
|
||||
@@ -85,6 +88,9 @@ export function PlexInfoView({
|
||||
onPlayItem: (id: string, fromStart?: boolean) => void;
|
||||
onFilter: (patch: Partial<PlexFilters>) => void;
|
||||
onOpenPerson: (name: string) => void;
|
||||
onOpenShow: (showId: string) => void;
|
||||
onOpenSeason: (showId: string, seasonId: string) => void;
|
||||
onOpenInfo: (id: string) => void;
|
||||
}) {
|
||||
const { t, i18n } = useTranslation();
|
||||
const lang = plexLang(i18n.language);
|
||||
@@ -113,6 +119,9 @@ export function PlexInfoView({
|
||||
onPlayItem={onPlayItem}
|
||||
onFilter={onFilter}
|
||||
onOpenPerson={onOpenPerson}
|
||||
onOpenShow={onOpenShow}
|
||||
onOpenSeason={onOpenSeason}
|
||||
onOpenInfo={onOpenInfo}
|
||||
// No onStateChange: PlexInfo.setState already invalidates qk.plexItem(id) (this very
|
||||
// query), so q refetches automatically — a manual refetch here just double-fetched.
|
||||
/>
|
||||
@@ -238,6 +247,7 @@ export function PlexPersonView({
|
||||
navIndex={i}
|
||||
card={c}
|
||||
onOpen={() => onOpenCard(c)}
|
||||
onPage={onInfoCard}
|
||||
onToggleWatched={toggle}
|
||||
{...cardMenu(c)}
|
||||
/>
|
||||
@@ -372,6 +382,7 @@ export function PlexPersonView({
|
||||
card={c}
|
||||
navIndex={i}
|
||||
onOpen={() => onOpenCard(c)}
|
||||
onPage={onInfoCard}
|
||||
onToggleWatched={toggle}
|
||||
{...cardMenu(c)}
|
||||
/>
|
||||
@@ -765,11 +776,13 @@ export function PlexSeasonView({
|
||||
seasonId,
|
||||
onBack,
|
||||
onPlay,
|
||||
onInfoCard,
|
||||
}: {
|
||||
showId: string;
|
||||
seasonId: string;
|
||||
onBack: () => void;
|
||||
onPlay: (epRk: string, queue: string[], fromStart?: boolean) => void;
|
||||
onInfoCard: (card: PlexCard) => void;
|
||||
}) {
|
||||
const { t, i18n } = useTranslation();
|
||||
const qc = useQueryClient();
|
||||
@@ -822,6 +835,7 @@ export function PlexSeasonView({
|
||||
{
|
||||
onOpen: (ep) => onPlay(ep.id, allKeys),
|
||||
onRestart: (ep) => onPlay(ep.id, allKeys, true),
|
||||
onInfo: onInfoCard,
|
||||
onToggleWatched: (ep) => void markEpisode(ep),
|
||||
},
|
||||
epMenu.open,
|
||||
@@ -918,6 +932,7 @@ export function PlexSeasonView({
|
||||
navIndex={i}
|
||||
ep={ep}
|
||||
onPlay={() => onPlay(ep.id, allKeys)}
|
||||
onPage={() => onInfoCard(ep)}
|
||||
onRestart={() => onPlay(ep.id, allKeys, true)}
|
||||
onAdd={() => episodes.add(ep)}
|
||||
onToggleWatched={() => markEpisode(ep)}
|
||||
|
||||
@@ -180,6 +180,8 @@ export interface PlexItemDetail {
|
||||
status: string;
|
||||
position_seconds: number;
|
||||
show_title?: string | null;
|
||||
show_id?: string | null; // an episode's show and season rating keys (its page links up to them)
|
||||
season_id?: string | null;
|
||||
season_number?: number | null;
|
||||
episode_number?: number | null;
|
||||
prev_id?: string | null;
|
||||
|
||||
Reference in New Issue
Block a user