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
+43 -26
View File
@@ -15,8 +15,9 @@ import { type SortState } from "../lib/columnSort";
import { useConfirm } from "./confirmContext";
import { BackBtn, ActionBtn, DetailHero } from "./plexDetailUi";
import { PlexPosterCard } from "./plexCards";
import { PlexPinMenu } from "./ComfortPinMenu";
import { PlexMenu } from "./PlexMenu";
import { useContextMenu } from "./ui/useContextMenu";
import { usePlexCardActions } from "./usePlexCardActions";
import PlexLibraryPicker from "./PlexLibraryPicker";
import { toggleWatchState } from "../lib/plexWatch";
import { LoadingState, StateMessage } from "./ui/QueryState";
@@ -66,8 +67,8 @@ export default function PlexCollectionView({
const { t } = useTranslation();
const qc = useQueryClient();
const confirm = useConfirm();
// One comfort-pin menu for the member grid (see ui/ContextMenu).
const pinMenu = useContextMenu<PlexCard>();
// One menu for the member grid (see ui/ContextMenu).
const menu = useContextMenu<PlexCard>();
const isAdmin = qc.getQueryData<{ role?: string }>(qk.me())?.role === "admin";
// The member order is a PREFERENCE (server-synced, adopted on login), not an ephemeral filter — the
// same treatment the feed's view and the player's auto-advance get. It is deliberately its OWN key
@@ -183,6 +184,10 @@ export default function PlexCollectionView({
qc.invalidateQueries({ queryKey: qk.plexCollections("detail", collectionId) });
},
});
const actions = usePlexCardActions(
{ onOpen: onOpenCard, onInfo: onInfoCard, onToggleWatched: toggleWatched },
menu.open,
);
async function doRename() {
const title = name.trim();
@@ -416,36 +421,48 @@ export default function PlexCollectionView({
/>
</div>
</div>
<NavGrid className="grid grid-cols-3 gap-3 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6">
{/* Two columns on a phone, like every other poster grid (≥140px a card): three left
an ~85px card whose title shared its line with the touch "…" (E28 C1). */}
<NavGrid className="grid grid-cols-2 gap-3 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6">
{items.map((c, i) => (
<div key={c.id} className="relative">
<PlexPosterCard
card={c}
navIndex={i}
onOpen={() => onOpenCard(c)}
onInfo={() => onInfoCard(c)}
onToggleWatched={toggleWatched}
menuProps={pinMenu.triggerProps}
/>
{canEdit && (
<IconButton
variant="media"
onClick={() => doRemove(c)}
disabled={busy}
label={t("plex.collection.removeItem")}
className="absolute right-1.5 top-1.5 z-base"
>
<X />
</IconButton>
)}
</div>
<PlexPosterCard
key={c.id}
card={c}
navIndex={i}
onOpen={() => onOpenCard(c)}
onToggleWatched={toggleWatched}
onAdd={actions.add}
menuProps={menu.triggerProps}
menuButtonProps={menu.buttonProps}
menuOpen={menu.open && menu.target === c}
/>
))}
</NavGrid>
</>
)}
</>
)}
<PlexPinMenu menu={pinMenu} />
<PlexMenu
menu={menu}
actions={actions}
extra={(c) =>
canEdit
? [
{
// A write to the shared Plex library: a menu row since E28 C1 (the card's hover
// row owns its corner now), still behind the confirmation in `doRemove`.
id: "collection-remove",
label: t("plex.collection.removeItem"),
icon: <X className="w-4 h-4" />,
separatorBefore: true,
danger: true,
disabled: busy,
onSelect: () => void doRemove(c),
},
]
: []
}
/>
</div>
);
}