fix(plex): a collection sibling is a full Plex card, and the player's info overlay plays it
- PlexInfo's collection strip gives each card the related strip's action set: the "…" menu (with Restart for a touch reader), the watched toggle and the playlist add. - `onPlayItem` is required, so no variant draws a Play that does nothing; the player's info overlay plays the sibling in the running player (`go(id, _, fromZero)` for its Restart).
This commit is contained in:
@@ -3,7 +3,7 @@ import { qk } from "../lib/queryKeys";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { Check, ExternalLink, FolderPlus, ListPlus, Play, RotateCcw, Star, X } from "lucide-react";
|
||||
import { api, type PlexFilters, type PlexItemDetail } from "../lib/api";
|
||||
import { api, type PlexCard, type PlexFilters, type PlexItemDetail } from "../lib/api";
|
||||
import { formatRuntime } from "../lib/format";
|
||||
import { notifyActionError } from "../lib/actionErrors";
|
||||
import { DetailCustomizeMenu, Filterable, PrefToggle } from "./plexDetailUi";
|
||||
@@ -24,6 +24,10 @@ import { IconLink } from "./ui/LinkButton";
|
||||
import { PageHeading } from "./ui/Headings";
|
||||
import clsx from "clsx";
|
||||
import { PlexPosterCard } from "./plexCards";
|
||||
import { PlexMenu } from "./PlexMenu";
|
||||
import { useContextMenu } from "./ui/useContextMenu";
|
||||
import { usePlexCardActions } from "./usePlexCardActions";
|
||||
import { toggleWatchState } from "../lib/plexWatch";
|
||||
|
||||
// The rich "media info" surface for a Plex item — poster/art, IMDb score + link, content rating,
|
||||
// genres, director, cast (with photos), summary. Reused in TWO places: a full-page view opened from
|
||||
@@ -346,8 +350,10 @@ type Props = {
|
||||
onFilter?: (patch: Partial<PlexFilters>) => void;
|
||||
// When provided, a cast member's photo/name opens their filmography page instead of filtering.
|
||||
onOpenPerson?: (name: string) => void;
|
||||
// Play a sibling title from a collection strip (opens its player).
|
||||
onPlayItem?: (id: string, fromStart?: boolean) => void;
|
||||
// Play a sibling title from a collection strip: the page opens its player, the overlay plays it
|
||||
// in the running one. Required, so no variant draws a collection card whose Play does nothing
|
||||
// (E28 review D1).
|
||||
onPlayItem: (id: string, fromStart?: boolean) => void;
|
||||
// The item's library key — enables the admin "Manage collections" editor (page variant, movies).
|
||||
library?: string;
|
||||
};
|
||||
@@ -416,6 +422,39 @@ export default function PlexInfo({
|
||||
onStateChange?.();
|
||||
};
|
||||
|
||||
// A collection sibling is a Plex card like any other (E28 review D2): the related strip's menu, its
|
||||
// watched toggle and its playlist add. The flip lands on this item's cached detail, whose strip it
|
||||
// is; the resync corrects it to the truth either way.
|
||||
const colMenu = useContextMenu<PlexCard>();
|
||||
const toggleColWatched = (card: PlexCard) =>
|
||||
toggleWatchState(card, t, {
|
||||
optimistic: (next) =>
|
||||
qc.setQueriesData<PlexItemDetail>({ queryKey: qk.plexItem(detail.id) }, (old) =>
|
||||
old
|
||||
? {
|
||||
...old,
|
||||
collections: old.collections?.map((col) => ({
|
||||
...col,
|
||||
items: col.items.map((m) => (m.id === card.id ? { ...m, status: next } : m)),
|
||||
})),
|
||||
}
|
||||
: old,
|
||||
),
|
||||
resync: () => {
|
||||
qc.invalidateQueries({ queryKey: qk.plexItem(detail.id) });
|
||||
qc.invalidateQueries({ queryKey: qk.plexItem(card.id) });
|
||||
qc.invalidateQueries({ queryKey: qk.plexLibrary() });
|
||||
},
|
||||
});
|
||||
const colActions = usePlexCardActions(
|
||||
{
|
||||
onOpen: (c) => onPlayItem(c.id),
|
||||
onRestart: (c) => onPlayItem(c.id, true),
|
||||
onToggleWatched: toggleColWatched,
|
||||
},
|
||||
colMenu.open,
|
||||
);
|
||||
|
||||
const cast = showCast ? detail.cast : [];
|
||||
const overlay = variant === "overlay";
|
||||
const mutedCls = overlay ? "text-white/70" : "text-muted";
|
||||
@@ -710,7 +749,7 @@ export default function PlexInfo({
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<Carousel ariaLabel={col.title} gap="gap-3" keyboardNav={!!onPlayItem}>
|
||||
<Carousel ariaLabel={col.title} gap="gap-3" keyboardNav>
|
||||
{col.items.map((m, i) => (
|
||||
// The one media card, as on every other strip (E28 S3h). A grid cell, so the
|
||||
// card fills the slot the carousel stretches to its tallest card.
|
||||
@@ -719,8 +758,13 @@ export default function PlexInfo({
|
||||
card={{ ...m, type: "movie" }}
|
||||
shelf
|
||||
navIndex={i}
|
||||
onOpen={() => onPlayItem?.(m.id)}
|
||||
onRestart={onPlayItem && (() => onPlayItem(m.id, true))}
|
||||
onOpen={() => onPlayItem(m.id)}
|
||||
onRestart={() => onPlayItem(m.id, true)}
|
||||
onToggleWatched={toggleColWatched}
|
||||
onAdd={colActions.add}
|
||||
menuProps={colMenu.triggerProps}
|
||||
menuButtonProps={colMenu.buttonProps}
|
||||
menuOpen={colMenu.open && colMenu.target?.id === m.id}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
@@ -857,6 +901,7 @@ export default function PlexInfo({
|
||||
onClose={() => setPlaylistOpen(false)}
|
||||
/>
|
||||
)}
|
||||
<PlexMenu menu={colMenu} actions={colActions} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user