feat(design): the download, notification and Scheduler action lists (C1) — the rows' "…", the band's and the strip's "…" (E28 S3e3)
A download row keeps play (pause / resume / retry in the queue) and the save link; the editors, sharing, re-download, the copies and cancel / delete / remove moved into its "…" (useDownloadActions). A link inside the row keeps the browser's own right-click menu. A notification row keeps its link and its unread mark and shows its dismiss on hover; an activity notice's Find in feed and Undo are in its "…". The inbox band keeps Mark all read and puts Clear everything, now behind a confirm, in its "…". The Scheduler strip keeps Run all and Pause and puts Purge discovery in its "…". Polled lists key their menus by id (keyOf). ui/menuGroups draws a rule only between two non-empty groups. Find in feed also drops the tag, search and age filters that could hide the video it looks for.
This commit is contained in:
@@ -1,12 +1,10 @@
|
||||
import { useState, type ReactNode } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { CalendarClock, Copy, File, Link2, Scissors } from "lucide-react";
|
||||
import { CalendarClock, File, Link2, Scissors } from "lucide-react";
|
||||
import clsx from "clsx";
|
||||
import { qk } from "../lib/queryKeys";
|
||||
import { useLiveQuery } from "../lib/useLiveQuery";
|
||||
import { api, type DownloadJob } from "../lib/api";
|
||||
import { notify } from "../lib/notifications";
|
||||
import { copyText } from "../lib/clipboard";
|
||||
import {
|
||||
formatBytes,
|
||||
formatDateTime,
|
||||
@@ -161,16 +159,14 @@ function displayUrl(url: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
// "Downloaded from" reference: opens the original page in a new tab, or copies the link.
|
||||
// A link inside the row keeps the BROWSER's own right-click menu (open in a new tab, copy the
|
||||
// address): the row's menu answers everywhere else on it (E28 C1).
|
||||
const nativeMenu = { onContextMenu: (e: React.MouseEvent) => e.stopPropagation() };
|
||||
|
||||
// "Downloaded from" reference: opens the original page in a new tab. Its copy moved into the row's
|
||||
// menu ("Copy link"); an extra link copies through the browser's own link menu.
|
||||
function SourceRef({ url }: { url: string }) {
|
||||
const { t } = useTranslation();
|
||||
const copy = async () => {
|
||||
if (await copyText(url)) {
|
||||
notify({ level: "success", message: t("downloads.source.copied") });
|
||||
} else {
|
||||
notify({ level: "error", message: t("downloads.source.copyFailed") });
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className="flex items-center gap-1 mt-1 text-sm text-muted min-w-0">
|
||||
<Link2 className="w-3.5 h-3.5 shrink-0" />
|
||||
@@ -180,12 +176,10 @@ function SourceRef({ url }: { url: string }) {
|
||||
rel="noopener noreferrer"
|
||||
title={`${t("downloads.source.open")} — ${url}`}
|
||||
className="truncate leading-6 hover:text-accent hover:underline pointer-coarse:leading-(--touch-target)"
|
||||
{...nativeMenu}
|
||||
>
|
||||
{displayUrl(url)}
|
||||
</a>
|
||||
<IconButton onClick={copy} label={t("downloads.source.copy")}>
|
||||
<Copy />
|
||||
</IconButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -202,6 +196,7 @@ function ChannelLine({ job }: { job: DownloadJob }) {
|
||||
rel="noopener noreferrer"
|
||||
title={url}
|
||||
className="text-muted hover:text-accent hover:underline"
|
||||
{...nativeMenu}
|
||||
>
|
||||
{name}
|
||||
</a>
|
||||
@@ -215,12 +210,18 @@ export function DownloadRow({
|
||||
job,
|
||||
onShowError,
|
||||
navIndex,
|
||||
rowProps,
|
||||
more,
|
||||
children,
|
||||
}: {
|
||||
job: DownloadJob;
|
||||
onShowError?: (job: DownloadJob) => void;
|
||||
/** Its position in a keyboard-walkable list (lib/listNav). */
|
||||
navIndex?: number;
|
||||
/** The list's menu triggers (right-click, long-press, Shift+F10): `useContextMenu().triggerProps`. */
|
||||
rowProps?: Record<string, unknown>;
|
||||
/** The row's "…", last in its action strip (E28 C1). */
|
||||
more?: ReactNode;
|
||||
children?: ReactNode;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
@@ -228,7 +229,8 @@ export function DownloadRow({
|
||||
return (
|
||||
<div
|
||||
{...(navIndex !== undefined && navItemAttrs(navIndex, jobTitle(job)))}
|
||||
className={`${SURFACE.card} flex gap-3 p-2.5 glass-hover transition`}
|
||||
{...rowProps}
|
||||
className={`${SURFACE.card} group flex gap-3 p-2.5 glass-hover transition`}
|
||||
>
|
||||
<Thumb job={job} />
|
||||
<div className="min-w-0 flex-1">
|
||||
@@ -302,10 +304,13 @@ export function DownloadRow({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* Row action buttons: the md rung here (the row stands alone), the sm rung in the modals, one
|
||||
IconBtn for both. `shrink-0` + no wrap keeps them on the title's line; the text column
|
||||
shrinks instead. */}
|
||||
<div className="flex items-start gap-1 shrink-0">{children}</div>
|
||||
{/* The row's visible actions — its primary one and the save link — then its "…": the md rung
|
||||
here (the row stands alone), the sm rung in the modals, one IconBtn for both. `shrink-0` + no
|
||||
wrap keeps them on the title's line; the text column shrinks instead. */}
|
||||
<div className="flex items-start gap-1 shrink-0">
|
||||
{children}
|
||||
{more}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user