Files
FlightTube/src/components/VideoTile.tsx
T
vincent caea8bad8f feat: jump to a channel by name, and queue the whole channel
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.
2026-08-29 16:01:16 +02:00

108 lines
3.8 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;
}
/** Grid cell: thumbnail on top, metadata beneath — the familiar YouTube shape. */
export default function VideoTile({
item, live, online, onOpen, onOpenChannel, onDownload, onCancel, onDelete,
}: Props) {
const downloaded = (live?.state ?? item.state) === "done";
const src = thumbSrc(item, online);
return (
<li className="group flex flex-col">
<button
onClick={onOpen}
className="relative aspect-video w-full cursor-pointer overflow-hidden rounded-lg
border border-slate-200 bg-slate-200 dark:border-slate-800 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-2 text-center text-[10px] text-slate-400">
No thumbnail
</span>
)}
{item.is_short && (
<span
className="absolute left-1.5 top-1.5 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-2 left-1.5 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>
{/* Every text block is a fixed height and every line is clamped, so tiles
stay on a shared baseline no matter how long a title runs. */}
<div className="mt-2 flex min-w-0 flex-col">
<button onClick={onOpen} className="cursor-pointer text-left">
<h3
className="h-[1.25rem] truncate text-[13px] font-medium leading-5"
title={item.title}
>
{item.title}
</h3>
</button>
<button
onClick={onOpenChannel}
title={`Show only ${item.channel_title}`}
className={CHANNEL_LINK + " mt-1 h-4 leading-4"}
>
{item.channel_title}
</button>
<div className="mt-0.5 h-4 truncate text-[11px] leading-4 text-slate-400 dark:text-slate-500">
{[compactViews(item.views), relativeTime(item.published)].filter(Boolean).join(" · ")}
</div>
<div className="mt-2 flex h-7 items-start">
<DownloadButton item={item} live={live} online={online}
onDownload={onDownload} onCancel={onCancel} onDelete={onDelete} />
</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"
title={live?.error ?? item.error ?? undefined}
>
{live?.error ?? item.error}
</div>
)}
</div>
</li>
);
}