A video's channel name under the thumbnail is now a link — click it in either list or tile view and the feed narrows to that channel. It clears the search box on the way, so arriving at a channel shows the channel rather than whatever you had been searching for. On a channel page, an icon button to the right of Local queues every video listed there. The backend already runs two downloads at a time and parks the rest, so the whole channel goes into the queue at once and comes down in order. The button is only there when there is something left to fetch, and a video that fails reports it on its own row rather than raising a dialog per failure. Cancelling a queued download now actually cancels it. Before, a video waiting for a slot ignored the cancel and started anyway once its turn came — barely reachable with one-at-a-time downloading, unmissable when a whole channel is queued. A download re-reads its own state after claiming a slot and stands down if it is no longer wanted.
98 lines
3.4 KiB
TypeScript
98 lines
3.4 KiB
TypeScript
import { thumbSrc } from "../api";
|
|
import type { LiveDownload } from "../hooks/useDownloads";
|
|
import type { FeedItem } from "../types";
|
|
import DownloadButton from "./DownloadButton";
|
|
import WatchBar from "./WatchBar";
|
|
import { clockDuration, compactViews, relativeTime } from "./format";
|
|
import { CHANNEL_LINK } from "./ui";
|
|
|
|
interface Props {
|
|
item: FeedItem;
|
|
live?: LiveDownload;
|
|
online: boolean;
|
|
onOpen: () => void;
|
|
/** Jump to this video's channel. */
|
|
onOpenChannel: () => void;
|
|
onDownload: () => void;
|
|
onCancel: () => void;
|
|
onDelete: () => void;
|
|
}
|
|
|
|
export default function VideoRow({
|
|
item, live, online, onOpen, onOpenChannel, onDownload, onCancel, onDelete,
|
|
}: Props) {
|
|
const downloaded = (live?.state ?? item.state) === "done";
|
|
const src = thumbSrc(item, online);
|
|
|
|
return (
|
|
<li
|
|
className="flex items-center gap-3 rounded-lg border border-slate-200 bg-slate-50 p-2
|
|
hover:border-slate-300 dark:border-slate-800 dark:bg-slate-800/50
|
|
dark:hover:border-slate-700"
|
|
>
|
|
<button
|
|
onClick={onOpen}
|
|
className="relative aspect-video w-32 shrink-0 cursor-pointer overflow-hidden rounded
|
|
bg-slate-200 dark:bg-slate-800"
|
|
>
|
|
{src ? (
|
|
<img src={src} alt="" loading="lazy" className="size-full object-cover" />
|
|
) : (
|
|
<span className="grid size-full place-items-center px-1 text-center text-[10px] text-slate-400">
|
|
No thumbnail
|
|
</span>
|
|
)}
|
|
{item.is_short && (
|
|
<span
|
|
className="absolute left-1 top-1 rounded bg-slate-950/75 px-1 py-0.5 text-[9px]
|
|
font-bold uppercase tracking-widest leading-none text-white"
|
|
>
|
|
Short
|
|
</span>
|
|
)}
|
|
{downloaded && (
|
|
<span
|
|
className="absolute bottom-1.5 left-1 rounded bg-sky-500 px-1 py-0.5 text-[9px]
|
|
font-bold uppercase tracking-widest leading-none text-white"
|
|
>
|
|
Offline
|
|
</span>
|
|
)}
|
|
{clockDuration(item.duration) && (
|
|
<span
|
|
className="absolute bottom-1.5 right-1.5 rounded bg-slate-950/80 px-1 py-0.5
|
|
font-mono text-[10px] leading-none tabular-nums text-white"
|
|
>
|
|
{clockDuration(item.duration)}
|
|
</span>
|
|
)}
|
|
<WatchBar item={item} />
|
|
</button>
|
|
|
|
<div className="min-w-0 flex-1">
|
|
<button onClick={onOpen} className="w-full cursor-pointer text-left">
|
|
<h3 className="line-clamp-2 text-[13px] font-medium leading-snug">{item.title}</h3>
|
|
</button>
|
|
<button
|
|
onClick={onOpenChannel}
|
|
title={`Show only ${item.channel_title}`}
|
|
className={CHANNEL_LINK + " mt-1"}
|
|
>
|
|
{item.channel_title}
|
|
</button>
|
|
<div className="mt-0.5 text-[11px] text-slate-400 dark:text-slate-500">
|
|
{[compactViews(item.views), relativeTime(item.published)].filter(Boolean).join(" · ")}
|
|
</div>
|
|
{(live?.error ?? item.error) && (live?.state ?? item.state) === "failed" && (
|
|
<div className="mt-1 line-clamp-1 text-[11px] text-red-600 dark:text-red-400">
|
|
{live?.error ?? item.error}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<DownloadButton item={item} live={live} online={online}
|
|
onDownload={onDownload} onCancel={onCancel} onDelete={onDelete} />
|
|
</li>
|
|
);
|
|
}
|