feat(tags): tag UX overhaul + per-card video reset

- Channel-row tags show only what's attached; a '+' opens a per-channel picker. Clicking a tag
  filters the feed by it; a 'Your tags' sidebar widget makes that filter visible/clearable.
- Tag manager dialog (add/rename/delete; delete only confirms when the tag is in use), reachable
  from the Channel manager and the feed sidebar; hovering a tag's count lists its channels, each a
  link that focuses it in the Channel manager (DataTable gains an external filter input).
- Video cards get a reset action that clears all watch state (incl. an in-progress position).
- api.req() now raises the error dialog on 5xx and 400/409/422 with the server's reason.
This commit is contained in:
npeter83 2026-06-18 01:17:31 +02:00
parent b55a944e7c
commit 1e530d23a6
22 changed files with 445 additions and 49 deletions

View file

@ -36,6 +36,7 @@ import {
} from "../lib/sidebarLayout";
import { shareUrl } from "../lib/urlState";
import { notify } from "../lib/notifications";
import TagManager from "./TagManager";
// Filter ids; display labels resolved at render time via t("sidebar.sort.<id>") etc.
const SORT_IDS = [
@ -114,11 +115,13 @@ export default function Sidebar({
setFilters,
layout,
setLayout,
onFocusChannel,
}: {
filters: FeedFilters;
setFilters: (f: FeedFilters) => void;
layout: SidebarLayout;
setLayout: (l: SidebarLayout) => void;
onFocusChannel: (name: string) => void;
}) {
const { t } = useTranslation();
const tagsQuery = useQuery({ queryKey: ["tags"], queryFn: api.tags });
@ -172,7 +175,11 @@ export default function Sidebar({
: t("sidebar.thisChannel"));
const languages = tags.filter((t) => t.category === "language");
const topics = tags.filter((t) => t.category === "topic");
// The user's own (non-system) tags — those assigned to channels in the Channel manager.
// Facets only cover language/topic, so these show their static channel_count.
const userTags = tags.filter((t) => !t.system);
const [customDates, setCustomDates] = useState(false);
const [tagManagerOpen, setTagManagerOpen] = useState(false);
const [editing, setEditing] = useState(false);
const sensors = useSensors(
@ -217,6 +224,7 @@ export default function Sidebar({
date: true,
language: languages.length > 0,
topic: topics.length > 0,
tags: userTags.length > 0,
};
function toggleCollapse(id: WidgetId) {
@ -393,6 +401,28 @@ export default function Sidebar({
</>
);
}
case "tags":
return (
<div className="flex flex-wrap items-center gap-1.5">
{userTags.map((tg) => (
<TagChip
key={tg.id}
tag={tg}
count={tg.channel_count}
active={filters.tags.includes(tg.id)}
onClick={() => toggleTag(tg.id)}
/>
))}
<button
onClick={() => setTagManagerOpen(true)}
title={t("sidebar.manageTags")}
className="inline-flex items-center gap-1 text-xs px-2 py-1 rounded-full border border-dashed border-border text-muted hover:text-accent hover:border-accent transition"
>
<Pencil className="w-3 h-3" />
{t("sidebar.manageTags")}
</button>
</div>
);
}
}
@ -496,6 +526,9 @@ export default function Sidebar({
</div>
</SortableContext>
</DndContext>
{tagManagerOpen && (
<TagManager onClose={() => setTagManagerOpen(false)} onFocusChannel={onFocusChannel} />
)}
</aside>
);
}