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:
parent
14b8eb6084
commit
d33a109ea2
22 changed files with 445 additions and 49 deletions
|
|
@ -19,11 +19,13 @@ import { formatDuration, formatViews, relativeTime } from "../lib/format";
|
|||
function Actions({
|
||||
video,
|
||||
onState,
|
||||
onResetState,
|
||||
onToggleSave,
|
||||
onChannelFilter,
|
||||
}: {
|
||||
video: Video;
|
||||
onState: (id: string, status: string) => void;
|
||||
onResetState?: (id: string) => void;
|
||||
onToggleSave: (id: string, saved: boolean) => void;
|
||||
onChannelFilter?: (channelId: string, channelName: string) => void;
|
||||
}) {
|
||||
|
|
@ -33,6 +35,9 @@ function Actions({
|
|||
e.stopPropagation();
|
||||
onState(video.id, video.status === status ? "new" : status);
|
||||
};
|
||||
// Pristine = never opened: default status and no resume position. The reset clears the
|
||||
// whole state (incl. an in-progress position the un-watch toggle can't touch).
|
||||
const resettable = video.status !== "new" || video.position_seconds > 0;
|
||||
const toggleSave = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
|
@ -79,6 +84,19 @@ function Actions({
|
|||
<EyeOff className="w-4 h-4" />
|
||||
)}
|
||||
</button>
|
||||
{onResetState && resettable && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onResetState(video.id);
|
||||
}}
|
||||
title={t("card.resetState")}
|
||||
className="p-1.5 rounded-md hover:bg-surface text-muted hover:text-fg"
|
||||
>
|
||||
<RotateCcw className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
{onChannelFilter && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
|
|
@ -224,6 +242,7 @@ function VideoCard({
|
|||
video,
|
||||
view,
|
||||
onState,
|
||||
onResetState,
|
||||
onToggleSave,
|
||||
onChannelFilter,
|
||||
onOpen,
|
||||
|
|
@ -231,6 +250,7 @@ function VideoCard({
|
|||
video: Video;
|
||||
view: "grid" | "list";
|
||||
onState: (id: string, status: string) => void;
|
||||
onResetState?: (id: string) => void;
|
||||
onToggleSave: (id: string, saved: boolean) => void;
|
||||
onChannelFilter?: (channelId: string, channelName: string) => void;
|
||||
onOpen?: (v: Video, startAt?: number | null) => void;
|
||||
|
|
@ -294,7 +314,7 @@ function VideoCard({
|
|||
</a>
|
||||
<div className="text-xs text-muted mt-0.5">{meta}</div>
|
||||
</div>
|
||||
<Actions video={video} onState={onState} onToggleSave={onToggleSave} onChannelFilter={onChannelFilter} />
|
||||
<Actions video={video} onState={onState} onResetState={onResetState} onToggleSave={onToggleSave} onChannelFilter={onChannelFilter} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -325,7 +345,7 @@ function VideoCard({
|
|||
{video.channel_title}
|
||||
</a>
|
||||
<div className="text-xs text-muted mt-0.5">{meta}</div>
|
||||
<Actions video={video} onState={onState} onToggleSave={onToggleSave} onChannelFilter={onChannelFilter} />
|
||||
<Actions video={video} onState={onState} onResetState={onResetState} onToggleSave={onToggleSave} onChannelFilter={onChannelFilter} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue