Files
siftlode/frontend/src/components/copyItems.tsx
T
peter 75ae810866 feat(design): a video's one action list (C1) — the card's "…", the menu holds the rest (E28 S3e1)
- 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.
2026-09-26 01:23:44 +02:00

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")),
},
]
: []),
];
}