feat(plex): S4 actor detail page — filmography endpoint + person subview
Backend: new GET /api/plex/person/{name} returns a cast/crew member's cached headshot plus
the movies (PlexItem, kind=movie) and shows (PlexShow) in the enabled libraries whose
cast_names OR directors include the name — the inverse of the cast → filter link. Introduce a
shared _content_libs(db) helper (enabled-library partition) and reuse it in people_cards;
S5 will extend it with the HU-overlay exclusion. Watch-state comes from PlexState / rollup
(reusing _movie_card / _show_card / state_map). DB-lane tests cover cast+director credits,
library scoping, the kind filter, unknown-name empties, and the partition helper.
Frontend: a new `person` history-subview. A cast-row click (PlexInfo cast row + the show
page's CastStrip) now opens the person's filmography page instead of filtering; the filter
gesture moves to a "Filter library by {name}" button on the page itself. PlexPersonView
mirrors PlexInfoView's fetch-wrapper + glass hero (headshot + name + credit counts) and
renders Movies / Shows as PlexPosterCard grids, with an optimistic per-card watch toggle on
the person cache. New plexPerson query key + api client + PlexPersonDetail type; HU/EN copy.
This commit is contained in:
@@ -31,6 +31,8 @@ type Props = {
|
||||
// When provided, metadata (year/genre/director/cast/studio/rating) becomes clickable and sets
|
||||
// the corresponding Plex filter (then the opener navigates to the filtered grid).
|
||||
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) => void;
|
||||
// The item's library key — enables the admin "Manage collections" editor (page variant, movies).
|
||||
@@ -44,6 +46,7 @@ export default function PlexInfo({
|
||||
onClose,
|
||||
onStateChange,
|
||||
onFilter,
|
||||
onOpenPerson,
|
||||
onPlayItem,
|
||||
library,
|
||||
}: Props) {
|
||||
@@ -341,12 +344,13 @@ export default function PlexInfo({
|
||||
</h2>
|
||||
<div className="flex gap-3 overflow-x-auto pb-2">
|
||||
{cast.map((c, i) => {
|
||||
const clickable = !!(onOpenPerson || onFilter);
|
||||
const inner = (
|
||||
<>
|
||||
<div
|
||||
className={`mx-auto mb-1 h-20 w-20 overflow-hidden rounded-full border ${
|
||||
overlay ? "border-white/15 bg-white/10" : "border-border bg-surface"
|
||||
} ${onFilter ? "group-hover/cast:border-accent" : ""}`}
|
||||
} ${clickable ? "group-hover/cast:border-accent" : ""}`}
|
||||
>
|
||||
{c.thumb ? (
|
||||
<img src={c.thumb} alt="" className="h-full w-full object-cover" />
|
||||
@@ -358,7 +362,7 @@ export default function PlexInfo({
|
||||
</div>
|
||||
<div
|
||||
className={`truncate text-xs font-medium ${overlay ? "text-white/90" : ""} ${
|
||||
onFilter ? "group-hover/cast:text-accent" : ""
|
||||
clickable ? "group-hover/cast:text-accent" : ""
|
||||
}`}
|
||||
>
|
||||
{c.name}
|
||||
@@ -372,11 +376,17 @@ export default function PlexInfo({
|
||||
)}
|
||||
</>
|
||||
);
|
||||
return onFilter ? (
|
||||
return clickable ? (
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => onFilter({ actors: [c.name] })}
|
||||
title={t("plex.info.filterActor", { name: c.name })}
|
||||
onClick={() =>
|
||||
onOpenPerson ? onOpenPerson(c.name) : onFilter?.({ actors: [c.name] })
|
||||
}
|
||||
title={
|
||||
onOpenPerson
|
||||
? t("plex.info.viewPerson", { name: c.name })
|
||||
: t("plex.info.filterActor", { name: c.name })
|
||||
}
|
||||
className="group/cast w-20 shrink-0 text-center"
|
||||
>
|
||||
{inner}
|
||||
|
||||
Reference in New Issue
Block a user