feat(design): a Plex title's one action list (C1) — the poster's "…", details and the pin in its menu (E28 S3e2)

Every Plex poster list (library, Discover rails, person, show, season, collection, Home's rails)
now opens one menu per list from the right-click, the long-press, Shift+F10 and the card's "…":
play/open, watched, add to playlist, media info, copy title, the pin (usePlexCardActions +
PlexMenu, replacing the pin-only PlexPinMenu). The card keeps its state at rest and shows
watched / add to playlist / "…" on its picture on hover (ui/reveal.ts, shared with the video
card); on touch the toggles are gone and a 44px "…" sits beside the title, while the watched pill
stays. The hover ⓘ left the card; an episode's add button moved from its caption to the hover
row. Seasons get their own short menu (open, watched, add, copy), labelled "Open season" (the
card's overlay said "Open show"). Related shows are ordinary poster cards now. A collection's
admin "Remove" X moved into its menu, still behind the confirmation. The collection grid is two
columns on a phone. The unused .reveal-idle rule is gone.
This commit is contained in:
2026-09-26 01:51:53 +02:00
parent 75ae810866
commit a4737b92e4
17 changed files with 1003 additions and 465 deletions
+25 -7
View File
@@ -40,8 +40,9 @@ import { usePlexActiveFilters, usePlexClearFilters } from "./usePlexActiveFilter
import { usePlex, usePlexActions } from "./plexContext";
import { usePublishPlexView } from "./plexViewContext";
import { EpisodeCard, PlexPosterCard } from "./plexCards";
import { PlexPinMenu } from "./ComfortPinMenu";
import { PlexMenu } from "./PlexMenu";
import { useContextMenu } from "./ui/useContextMenu";
import { usePlexCardActions } from "./usePlexCardActions";
import { PlexInfoView, PlexPersonView, PlexSeasonView, PlexShowView } from "./plexViews";
import { dispatch, useKeyScope } from "../lib/keymap";
import NavGrid from "./ui/NavGrid";
@@ -139,7 +140,8 @@ export default function PlexBrowse() {
}, [openPlaylist, sub, setPlaylistOpen]);
// The header search typeahead picked a title → open it the same way a card click would (a movie's
// info page like the card's ⓘ, a show's drill-down), then clear the hand-off so it doesn't re-open.
// info page like the card menu's "Media info", a show's drill-down), then clear the hand-off so it
// doesn't re-open.
useEffect(() => {
if (jumpTo != null) {
if (jumpTo.type !== "show") infoScrollRef.current = 0; // a jumped-to info page starts at the top
@@ -197,8 +199,9 @@ export default function PlexBrowse() {
// makes it safe, and nothing here reads the value during render.
// eslint-disable-next-line react-hooks/immutability
const rememberInfoScroll = (px: number) => void (infoScrollRef.current = px);
// One comfort-pin menu for the titles grid (see ui/ContextMenu).
const pinMenu = useContextMenu<PlexCard>();
// One menu for the titles grid and one for the search's episodes (see ui/ContextMenu).
const menu = useContextMenu<PlexCard>();
const epMenu = useContextMenu<PlexCard>();
const scroller = () => document.querySelector("main");
useLayoutEffect(() => {
const el = scroller();
@@ -363,6 +366,14 @@ export default function PlexBrowse() {
optimistic: (next) => optimisticStatus(card.id, next),
resync: () => qc.invalidateQueries({ queryKey: qk.plexLibrary() }),
});
const actions = usePlexCardActions(
{ onOpen: onCard, onInfo, onToggleWatched: toggleWatched },
menu.open,
);
const epActions = usePlexCardActions(
{ onOpen: onCard, onToggleWatched: toggleWatched },
epMenu.open,
);
// Apply a metadata filter (clicked on an info page / show hero / cast) then show the unified grid.
// Array filters (genres/people/studios) UNION with what's set; scalars replace. Clicking a person
// (cast/crew) widens to the 'both' scope so their movies AND shows show up together (mixed feed).
@@ -642,9 +653,11 @@ export default function PlexBrowse() {
navIndex={i}
card={c}
onOpen={() => onCard(c)}
onInfo={() => onInfo(c)}
onToggleWatched={toggleWatched}
menuProps={pinMenu.triggerProps}
onAdd={actions.add}
menuProps={menu.triggerProps}
menuButtonProps={menu.buttonProps}
menuOpen={menu.open && menu.target === c}
/>
))}
</NavGrid>
@@ -663,6 +676,10 @@ export default function PlexBrowse() {
withShowTitle
onPlay={() => onCard(ep)}
onToggleWatched={() => toggleWatched(ep)}
onAdd={() => epActions.add(ep)}
menuProps={epMenu.triggerProps}
menuButtonProps={epMenu.buttonProps}
menuOpen={epMenu.open && epMenu.target === ep}
/>
))}
</NavGrid>
@@ -671,7 +688,8 @@ export default function PlexBrowse() {
</>
)}
<PlexPinMenu menu={pinMenu} />
<PlexMenu menu={menu} actions={actions} />
<PlexMenu menu={epMenu} actions={epActions} pin={false} />
<div ref={setSentinelEl} className="h-8" />
{isFetchingNextPage && (
<p className="text-center text-xs text-muted pb-4">{t("plex.loading")}</p>