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 14b8eb6084
commit d33a109ea2
22 changed files with 445 additions and 49 deletions

View file

@ -1,4 +1,6 @@
import { notify } from "./notifications";
import i18n from "../i18n";
import { reportError } from "./errorDialog";
export interface Me {
id: number;
@ -196,9 +198,14 @@ async function req(url: string, opts: RequestInit = {}): Promise<any> {
} catch {
/* no JSON body */
}
// Server faults are worth surfacing; 401/403/404 etc. are handled by callers.
// Surface anything the server definitively refused as a self-explanatory dialog the
// user must acknowledge: 5xx (a fault) and 400/409/422 (validation/conflict, with the
// server's own reason). 401/403/404 are control flow handled by callers (auth, demo
// gating, not-found), so they just throw.
if (r.status >= 500) {
notifyErrorThrottled(`Server error ${r.status}`, `${method} ${url}`);
reportError(detail || `${i18n.t("errors.server")} (${r.status})`);
} else if (r.status === 400 || r.status === 409 || r.status === 422) {
reportError(detail);
}
throw new HttpError(r.status, detail);
}
@ -365,6 +372,7 @@ export const api = {
req(`/api/facets?${filterParams(f).toString()}`),
setState: (id: string, status: string) =>
req(`/api/videos/${id}/state`, { method: "POST", body: JSON.stringify({ status }) }),
clearState: (id: string) => req(`/api/videos/${id}/state`, { method: "DELETE" }),
saveProgress: (id: string, positionSeconds: number, durationSeconds: number) =>
req(`/api/videos/${id}/progress`, {
method: "POST",