feat(design): a page's background menu on the five list pages (E28 S3e4c)
A right-click on the EMPTY area of a page opens a menu of that page's own toolbar: the feed (sort, reshuffle, view, watch state, content, source, clear filters), the Plex library grid (sort, watch state, clear), the channel manager (layout, the cards' sort, sync status, edit tags, clear the tag filter, read subscriptions), a playlist (play all, group by, reset view, save order, undo/redo, the list's own menu) and the notification center (mark all read). No destructive row and no backfill. PageShell listens on the page area; lib/backgroundTarget decides empty (not text, a control, a link, media, or a row/card with its own menu). Shift+right- click, a touch long-press and a right-click on selected words keep the browser's menu. ui/PageMenu registers while mounted, so a loading or error state keeps the browser's menu too. The rows mirror each control through ui/pageMenuItems (sortSubmenu via pickSort, choiceSubmenu, multiSubmenu). pageMenuScope.contract.test.ts pins the five pages.
This commit is contained in:
@@ -35,6 +35,8 @@ import { type ActionMenuItem } from "./ui/ActionMenu";
|
||||
import { useComfortPin, pinPlaylist, pinVideo } from "./comfortPin";
|
||||
import { ContextMenu } from "./ui/ContextMenu";
|
||||
import { useContextMenu } from "./ui/useContextMenu";
|
||||
import { usePageMenu } from "./ui/pageMenuSlot";
|
||||
import { menuGroups } from "./ui/menuGroups";
|
||||
import UndoToolbar from "./ui/UndoToolbar";
|
||||
import NavGrid from "./ui/NavGrid";
|
||||
import { PageToolbar } from "./ui/PageShell";
|
||||
@@ -268,6 +270,9 @@ export default function Playlists() {
|
||||
refetchOnWindowFocus: true,
|
||||
});
|
||||
const detail = detailQuery.data;
|
||||
// The page's background menu (DESIGN "Background menu"), live only while a list is on screen.
|
||||
const pageMenu = usePageMenu(selectedId != null && !!detail);
|
||||
const pagePin = useComfortPin(pageMenu.open);
|
||||
|
||||
// Destructured so the effect below can name what it calls: both are stable useCallbacks, while
|
||||
// the object around them is rebuilt every render (lib/useUndoable).
|
||||
@@ -704,6 +709,57 @@ export default function Playlists() {
|
||||
pin: headerPin.item(pinPlaylist(headerPl.id), plName(headerPl)),
|
||||
});
|
||||
|
||||
// The page's background menu mirrors the band: Play all, the view controls, the order's undo, and
|
||||
// the list's own "…" without its destructive rows (Delete, Revert) — a stray right-click on the
|
||||
// page's blank area must not be one click from those.
|
||||
const pageItems: ActionMenuItem[] = !detail
|
||||
? []
|
||||
: menuGroups(
|
||||
[
|
||||
{
|
||||
id: "playAll",
|
||||
label: t("playlists.playAll"),
|
||||
disabled: !shown.length,
|
||||
onSelect: () => shown.length && openPlayer(shown[0]!.id),
|
||||
},
|
||||
],
|
||||
items.length > 1 || viewActive
|
||||
? [
|
||||
{
|
||||
id: "groupBy",
|
||||
label: t("playlists.groupByChannel"),
|
||||
check: { kind: "many", checked: groupBy },
|
||||
onSelect: () => setGroupBy(!groupBy),
|
||||
},
|
||||
...(viewActive
|
||||
? [{ id: "clearView", label: t("playlists.clearView"), onSelect: clearView }]
|
||||
: []),
|
||||
...(canSaveOrder
|
||||
? [{ id: "saveOrder", label: t("playlists.saveOrder"), onSelect: saveOrder }]
|
||||
: []),
|
||||
]
|
||||
: [],
|
||||
canEdit && (order.canUndo || order.canRedo)
|
||||
? [
|
||||
...(order.canUndo
|
||||
? [{ id: "undo", label: t("common.undo"), onSelect: order.undo }]
|
||||
: []),
|
||||
...(order.canRedo
|
||||
? [{ id: "redo", label: t("common.redo"), onSelect: order.redo }]
|
||||
: []),
|
||||
]
|
||||
: [],
|
||||
plActions
|
||||
.items(detail, {
|
||||
onRename: () => {
|
||||
setRenameValue(detail.name);
|
||||
setRenaming(true);
|
||||
},
|
||||
pin: pagePin.item(pinPlaylist(detail.id), plName(detail)),
|
||||
})
|
||||
.filter((it) => !("danger" in it && it.danger)),
|
||||
);
|
||||
|
||||
// The open player, at ONE place in the tree whichever state the page below it is in. Selecting
|
||||
// another list under an open player (the command palette works over it) puts the page into
|
||||
// "Loading…" until that list arrives; with the player rendered only in the loaded branch, that
|
||||
@@ -966,6 +1022,11 @@ export default function Playlists() {
|
||||
{/* One surface for the whole table, outside the rows — it anchors to the pointer, not the row. */}
|
||||
<ContextMenu menu={rowMenu} ariaLabel={t("playlists.rowMenu")} items={ctxItems} />
|
||||
<ContextMenu menu={headerMenu} ariaLabel={t("playlists.railMenu")} items={headerItems} />
|
||||
<ContextMenu
|
||||
menu={pageMenu}
|
||||
ariaLabel={t("common.pageMenu")}
|
||||
items={pageMenu.open ? pageItems : []}
|
||||
/>
|
||||
</div>
|
||||
{player}
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user