feat(feed): the card menu carries the card's own actions
Right-click, long-press or Shift+F10 on a feed card offered only the comfort-shelf pin; watched, saved, hidden and the progress reset were reachable only by hovering or Tabbing through every card.
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Bookmark, BookmarkX, Check, Eye, EyeOff, RotateCcw, Undo2 } from "lucide-react";
|
||||
|
||||
import { type PlexCard, type Video } from "../lib/api";
|
||||
import { type PlexCard, type Video, type VideoStatus } from "../lib/api";
|
||||
import { pinPlex, pinVideo, useComfortPin, type PinTarget } from "./comfortPin";
|
||||
import { type ActionMenuItem } from "./ui/ActionMenu";
|
||||
import { ContextMenu } from "./ui/ContextMenu";
|
||||
import { type ContextMenuController } from "./ui/useContextMenu";
|
||||
|
||||
@@ -44,7 +46,64 @@ export function PlexPinMenu({ menu }: { menu: ContextMenuController<PlexCard> })
|
||||
return <ComfortPinMenu menu={menu} target={pinPlex} title={(c) => c.title} />;
|
||||
}
|
||||
|
||||
/** The YouTube video lists (the feed grid, the Home rails). */
|
||||
export function VideoPinMenu({ menu }: { menu: ContextMenuController<Video> }) {
|
||||
return <ComfortPinMenu menu={menu} target={(v) => pinVideo(v.id)} title={(v) => v.title ?? ""} />;
|
||||
/** The feed's menu (the grid and a channel page): the card's own state actions — the hover row's
|
||||
* buttons, which a keyboard or a touch user otherwise reaches only by Tabbing through every card —
|
||||
* then the pin. Playlists and downloads stay on the card: each opens its own anchored dialog. */
|
||||
export function FeedVideoMenu({
|
||||
menu,
|
||||
onState,
|
||||
onToggleSave,
|
||||
onResetState,
|
||||
}: {
|
||||
menu: ContextMenuController<Video>;
|
||||
onState: (id: string, status: VideoStatus) => void;
|
||||
onToggleSave: (id: string, saved: boolean) => void;
|
||||
onResetState: (id: string) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const pin = useComfortPin(menu.open);
|
||||
const v = menu.target;
|
||||
const icon = "w-4 h-4";
|
||||
const items: ActionMenuItem[] = v
|
||||
? [
|
||||
v.status === "watched"
|
||||
? {
|
||||
id: "watched",
|
||||
label: t("card.menu.unmarkWatched"),
|
||||
icon: <Undo2 className={icon} />,
|
||||
onSelect: () => onState(v.id, "new"),
|
||||
}
|
||||
: {
|
||||
id: "watched",
|
||||
label: t("card.menu.markWatched"),
|
||||
icon: <Check className={icon} />,
|
||||
onSelect: () => onState(v.id, "watched"),
|
||||
},
|
||||
{
|
||||
id: "save",
|
||||
label: v.saved ? t("card.menu.unsave") : t("card.menu.save"),
|
||||
icon: v.saved ? <BookmarkX className={icon} /> : <Bookmark className={icon} />,
|
||||
onSelect: () => onToggleSave(v.id, !v.saved),
|
||||
},
|
||||
{
|
||||
id: "hide",
|
||||
label: v.status === "hidden" ? t("card.menu.unhide") : t("card.menu.hide"),
|
||||
icon: v.status === "hidden" ? <Eye className={icon} /> : <EyeOff className={icon} />,
|
||||
onSelect: () => onState(v.id, v.status === "hidden" ? "new" : "hidden"),
|
||||
},
|
||||
// The card offers the reset only on a card that has something to reset; so does this.
|
||||
...(v.status !== "new" || v.position_seconds > 0
|
||||
? [
|
||||
{
|
||||
id: "reset",
|
||||
label: t("card.menu.reset"),
|
||||
icon: <RotateCcw className={icon} />,
|
||||
onSelect: () => onResetState(v.id),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...pin.item(pinVideo(v.id), v.title ?? ""),
|
||||
]
|
||||
: [];
|
||||
return <ContextMenu menu={menu} ariaLabel={t("card.menu.label")} items={items} />;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user