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.
108 lines
4.3 KiB
TypeScript
108 lines
4.3 KiB
TypeScript
import { useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { Check, Info, ListPlus, Play, Tv2, Undo2 } from "lucide-react";
|
|
|
|
import { type PlexCard } from "../lib/api";
|
|
import { pinPlex, useComfortPin } from "./comfortPin";
|
|
import { useCopyItems } from "./copyItems";
|
|
import PlexPlaylistAdd, { type PlexAddTarget } from "./PlexPlaylistAdd";
|
|
import { type ActionMenuItem } from "./ui/ActionMenu";
|
|
|
|
// E28 C1 — a Plex title's ONE action list (docs/DESIGN.md "Action placement"): the right-click, the
|
|
// long-press, Shift+F10 and the card's "…" open a menu built here, so every poster grid and rail
|
|
// lists the same actions in the same order. The card keeps its state (watched, progress) at rest and
|
|
// the everyday toggles (watched, add to playlist) on hover; details, copy and the pin live here.
|
|
|
|
export interface PlexActionHandlers {
|
|
onOpen: (c: PlexCard) => void;
|
|
/** The media-info page. Omitted where a surface has none (the episode lists). */
|
|
onInfo?: (c: PlexCard) => void;
|
|
onToggleWatched?: (c: PlexCard) => void;
|
|
}
|
|
|
|
const icon = "w-4 h-4";
|
|
|
|
/** The "add to playlist" row and button: an episode says which it adds, as the season page does. */
|
|
export const plexAddLabelKey = (c: PlexCard): string =>
|
|
c.type === "episode" ? "plex.playlist.addEpisode" : "plex.playlistAdd.manage";
|
|
|
|
/** A show opens its drill-down; a movie or an episode plays. */
|
|
const isPlexPlayable = (c: PlexCard): boolean => c.type !== "show";
|
|
|
|
/** `open` is the host menu's own flag (the shelf is fetched only once a menu opened). Returns the
|
|
* item builder, `add` for the card's own hover button, and the dialog to render beside the menu
|
|
* (one per list, so a card does not mount a modal of its own). */
|
|
export function usePlexCardActions(handlers: PlexActionHandlers, open: boolean) {
|
|
const { t } = useTranslation();
|
|
const pin = useComfortPin(open);
|
|
const copy = useCopyItems();
|
|
const addLabel = (c: PlexCard) => t(plexAddLabelKey(c));
|
|
const [adding, setAdding] = useState<PlexAddTarget | null>(null);
|
|
// A whole show needs its episode keys, which only the show page has ("Add whole show" there), so
|
|
// only a leaf is added from a card.
|
|
const add = (c: PlexCard) => setAdding({ kind: "single", ratingKey: c.id, title: c.title });
|
|
|
|
/** `play: false` where the host menu leads with its own Play row (Home's shelf resolves `next`);
|
|
* `pin: false` where the host carries its own shelf rows or the object is not a pin kind. */
|
|
const items = (
|
|
c: PlexCard,
|
|
{ play = true, pin: withPin = true }: { play?: boolean; pin?: boolean } = {},
|
|
): ActionMenuItem[] => {
|
|
const { onOpen, onInfo, onToggleWatched } = handlers;
|
|
const playable = isPlexPlayable(c);
|
|
const watched = c.status === "watched";
|
|
return [
|
|
...(play
|
|
? [
|
|
{
|
|
id: "play",
|
|
label: playable ? t("plex.play") : t("plex.openShow"),
|
|
icon: playable ? <Play className={icon} /> : <Tv2 className={icon} />,
|
|
onSelect: () => onOpen(c),
|
|
},
|
|
]
|
|
: []),
|
|
...(playable && onToggleWatched
|
|
? [
|
|
{
|
|
id: "watched",
|
|
label: watched ? t("plex.markUnwatched") : t("plex.markWatched"),
|
|
icon: watched ? <Undo2 className={icon} /> : <Check className={icon} />,
|
|
separatorBefore: true,
|
|
onSelect: () => onToggleWatched(c),
|
|
},
|
|
]
|
|
: []),
|
|
...(playable
|
|
? [
|
|
{
|
|
id: "playlist",
|
|
label: addLabel(c),
|
|
icon: <ListPlus className={icon} />,
|
|
separatorBefore: !onToggleWatched,
|
|
onSelect: () => add(c),
|
|
},
|
|
]
|
|
: []),
|
|
...(playable && onInfo
|
|
? [
|
|
{
|
|
id: "details",
|
|
label: t("plex.info.title"),
|
|
icon: <Info className={icon} />,
|
|
separatorBefore: true,
|
|
onSelect: () => onInfo(c),
|
|
},
|
|
]
|
|
: []),
|
|
...copy(c.title, null).map((it) => ({ ...it, separatorBefore: !(playable && onInfo) })),
|
|
...(withPin ? pin.item(pinPlex(c), c.title) : []),
|
|
];
|
|
};
|
|
|
|
const panel = adding && <PlexPlaylistAdd target={adding} onClose={() => setAdding(null)} />;
|
|
return { items, add, panel };
|
|
}
|
|
|
|
export type PlexCardActions = ReturnType<typeof usePlexCardActions>;
|