feat(design): the channel menu's Tags and Priority submenus (C1, E28 S3e3b)

Tags A-Z as in-place checks (the menu stays open for the next); Priority
as the fixed steps 10 5 3 2 1 0, an off-scale current value ticked in its
place. A pick re-sorts the list at once, moving only that row
(placeByPriority, the server's order), and the table / card pager and
the focus follow it (DataTable and useCardPager take a reveal seq,
listNav.focusNavItem lands on the row's own "…", a virtualized card
revealed first). ActionMenu rows take check: many | one. The menu reads
the row fresh from the cache and keys its target by id.
This commit is contained in:
2026-09-26 17:34:47 +02:00
parent 2b3094ffc5
commit 5e5c8a42a9
15 changed files with 467 additions and 28 deletions
+57 -3
View File
@@ -1,8 +1,9 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useTranslation } from "react-i18next";
import { EyeOff, ExternalLink, History, Tv, UserMinus } from "lucide-react";
import { ArrowUpDown, EyeOff, ExternalLink, History, Tags, Tv, UserMinus } from "lucide-react";
import { qk } from "../lib/queryKeys";
import { api, type ManagedChannel } from "../lib/api";
import { api, type ManagedChannel, type Tag } from "../lib/api";
import { priorityChoices } from "../lib/channelOrder";
import { channelYouTubeUrl } from "../lib/format";
import { notify } from "../lib/notifications";
import { notifyYouTubeActionError } from "../lib/youtubeErrors";
@@ -14,14 +15,24 @@ import { type ActionMenuItem } from "./ui/ActionMenu";
// A subscribed channel's ONE action list (E28 C1): the manager row's "…", its right-click, long-press
// and Shift+F10, in the table and in the card layout alike. The row keeps what the reader looks at
// or tunes all the time (priority, tags, the stored count's ✓); everything that was a button on the
// row — full history, hide, unsubscribe — is here, beside open, the pin and the copies.
// row — full history, hide, unsubscribe — is here, beside open, the pin and the copies. Tags and
// priority are here TOO, as submenus (owner, 2026-09-26): tagging a run of channels, or bubbling one
// up the list, from the menu the hand is already in.
export function useChannelActions({
onView,
onOpenWizard,
tags,
onToggleTag,
onPickPriority,
}: {
onView: (c: ManagedChannel) => void;
onOpenWizard: () => void;
/** The reader's own tags (no system tags) — the Tags submenu lists them A–Z. */
tags: readonly Tag[];
onToggleTag: (c: ManagedChannel, tagId: number) => void;
/** A priority step picked: the caller re-sorts its list and follows the row. */
onPickPriority: (c: ManagedChannel, priority: number) => void;
}) {
const qc = useQueryClient();
const { t } = useTranslation();
@@ -130,6 +141,47 @@ export function useChannelActions({
};
}
/** Tags A–Z, one check each, toggled in place (the menu stays open for the next one). With no tag
* yet the row stays, off, and says where tags are made. */
function tagsItem(c: ManagedChannel): ActionMenuItem {
const base = {
id: "tags",
label: t("channels.tags.label"),
icon: <Tags className="w-4 h-4" />,
};
if (tags.length === 0)
return { ...base, disabled: true, note: t("channels.row.noTagsYet"), onSelect: () => {} };
const on = new Set(c.tag_ids);
return {
...base,
items: [...tags]
.sort((a, b) => a.name.localeCompare(b.name))
.map((tg) => ({
id: `tag-${tg.id}`,
label: tg.name,
check: { kind: "many" as const, checked: on.has(tg.id) },
onSelect: () => onToggleTag(c, tg.id),
})),
};
}
/** The fixed priority steps, the current one ticked; a pick re-sorts the list (the caller). */
function priorityItem(c: ManagedChannel): ActionMenuItem {
return {
id: "priority",
label: t("channels.row.priority"),
icon: <ArrowUpDown className="w-4 h-4" />,
items: priorityChoices(c.priority).map((p) => ({
id: `priority-${p}`,
label: String(p),
check: { kind: "one" as const, checked: p === c.priority },
onSelect: () => {
if (p !== c.priority) onPickPriority(c, p);
},
})),
};
}
/** The channel's menu rows; `pin` is the caller's comfort-shelf row (its "is it pinned?" read
* follows the caller's own menu). */
function items(c: ManagedChannel, { pin }: { pin: ActionMenuItem[] }): ActionMenuItem[] {
@@ -142,6 +194,8 @@ export function useChannelActions({
onSelect: () => onView(c),
},
...pin,
{ ...tagsItem(c), separatorBefore: true },
priorityItem(c),
...[
{
id: "open-youtube",