- useContextMenu gains buttonProps (a row's visible "…" opens the same menu under the button; a second press closes it) and anchor (the element the menu came from, for a panel a row opens). - ui/MoreButton; ActionMenu rows can run inGesture (a clipboard write inside the click, Safari). - useVideoActions: one builder for a video's menu (play / restart, channel, watched, save, add to playlist, download, details, copy title / link, hide, reset, pin), composed by the feed and by Home's rails; the playlists and details panels open anchored beside the card (VideoFollowUp). - The card keeps watched / save / add to playlist and the "…": on the picture on hover (the duration steps aside), in the rows' action column; on touch only a 44px "…" beside the title. Download, hide, reset and the hover info card leave the card. - The title is selectable: a click ending a selection neither plays nor follows the link. - DESIGN.md: how C1 is built + the log row.
59 lines
2.0 KiB
TypeScript
59 lines
2.0 KiB
TypeScript
import { useTranslation } from "react-i18next";
|
|
import { Copy, Link2 } from "lucide-react";
|
|
|
|
import { copyText } from "../lib/clipboard";
|
|
import { notify } from "../lib/notifications";
|
|
import { type ActionMenuItem } from "./ui/ActionMenu";
|
|
|
|
// "Copy title" and "Copy link", the two rows every object's menu ends its middle group with (E28 C1,
|
|
// "Text stays selectable"): on touch the long-press belongs to the context menu, so the text a
|
|
// finger cannot select is copied from here. One builder, so every object says it the same way and
|
|
// the clipboard write always runs inside the gesture (`inGesture`, which Safari insists on).
|
|
//
|
|
// A feature file, not `ui/`: it speaks the app's notifications.
|
|
|
|
type Named = "title" | "name";
|
|
|
|
export function useCopyItems() {
|
|
const { t } = useTranslation();
|
|
const copy = (text: string, done: string) => {
|
|
void copyText(text).then((ok) =>
|
|
notify(
|
|
ok
|
|
? { level: "success", message: done }
|
|
: { level: "error", message: t("common.copy.failed") },
|
|
),
|
|
);
|
|
};
|
|
/** The rows for one object. `text` is its title (a video, a show) or its name (a channel, a
|
|
* playlist); a row whose value is missing is left out rather than copying an empty string. */
|
|
return (
|
|
text: string | null | undefined,
|
|
link: string | null | undefined,
|
|
named: Named = "title",
|
|
): ActionMenuItem[] => [
|
|
...(text
|
|
? [
|
|
{
|
|
id: "copy-text",
|
|
label: t(named === "title" ? "common.copy.title" : "common.copy.name"),
|
|
icon: <Copy className="w-4 h-4" />,
|
|
inGesture: true,
|
|
onSelect: () => copy(text, t("common.copy.done")),
|
|
},
|
|
]
|
|
: []),
|
|
...(link
|
|
? [
|
|
{
|
|
id: "copy-link",
|
|
label: t("common.copy.link"),
|
|
icon: <Link2 className="w-4 h-4" />,
|
|
inGesture: true,
|
|
onSelect: () => copy(link, t("common.copy.linkDone")),
|
|
},
|
|
]
|
|
: []),
|
|
];
|
|
}
|