feat: tile view, in-app streaming, and self-painted title bar
Clicking an undownloaded video now plays it in the app instead of handing off to the browser. YouTube's iframe embed cannot be used — it rejects a tauri:// origin with Error 153 — so yt-dlp resolves YouTube's HLS master playlist instead, whose H.264+AAC variants AVFoundation streams natively in WKWebView, adaptive up to 1080p. Adds an optional Tiles view alongside the list, remembered between launches. The title bar is now transparent with the title hidden, and the app paints that strip itself in the page background so it matches instead of showing macOS chrome beside the traffic lights.
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
import { thumbSrc } from "../api";
|
||||
import type { LiveDownload } from "../hooks/useDownloads";
|
||||
import type { FeedItem } from "../types";
|
||||
import DownloadButton from "./DownloadButton";
|
||||
import { compactViews, relativeTime } from "./format";
|
||||
|
||||
interface Props {
|
||||
item: FeedItem;
|
||||
live?: LiveDownload;
|
||||
online: boolean;
|
||||
onOpen: () => 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, 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-1.5 right-1.5 rounded bg-sky-500 px-1 py-0.5 text-[9px]
|
||||
font-bold uppercase tracking-widest leading-none text-white"
|
||||
>
|
||||
Offline
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<div className="mt-2 flex min-w-0 flex-1 flex-col">
|
||||
<button onClick={onOpen} className="cursor-pointer text-left">
|
||||
<h3 className="line-clamp-2 text-[13px] font-medium leading-snug">{item.title}</h3>
|
||||
</button>
|
||||
<div className="mt-1 truncate text-[12px] text-slate-500 dark:text-slate-400">
|
||||
{item.channel_title}
|
||||
</div>
|
||||
<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-2 text-[11px] text-red-600 dark:text-red-400">
|
||||
{live?.error ?? item.error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-2 flex">
|
||||
<DownloadButton item={item} live={live} online={online}
|
||||
onDownload={onDownload} onCancel={onCancel} onDelete={onDelete} />
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user