feat: feed UI, in-app player, offline mode, settings

Adds an end-to-end integration test that runs the real pipeline against
live YouTube Atom feeds, verifying the merged feed is newest-first
across channels.
This commit is contained in:
vincent
2026-08-29 02:34:14 +02:00
parent a30423e4b5
commit e75d896933
16 changed files with 1059 additions and 2 deletions
+61
View File
@@ -0,0 +1,61 @@
import type { ChannelWithCount } from "../types";
interface Props {
channels: ChannelWithCount[];
activeChannel: string | null;
onSelect: (id: string | null) => void;
onOpenSettings: () => void;
totalVideos: number;
}
export default function Sidebar({
channels, activeChannel, onSelect, onOpenSettings, totalVideos,
}: Props) {
const rowBase =
"w-full text-left px-3 py-2 rounded-lg text-sm flex items-center justify-between gap-2 transition-colors cursor-pointer";
return (
<aside className="w-64 shrink-0 border-r border-edge flex flex-col bg-ink">
<div className="px-4 py-4 flex items-center gap-2">
<span className="text-xl"></span>
<span className="font-semibold tracking-tight">FlightTube</span>
</div>
<nav className="flex-1 overflow-y-auto px-2 pb-2 space-y-0.5">
<button onClick={() => onSelect(null)}
className={`${rowBase} ${
activeChannel === null ? "bg-raised text-white" : "text-muted hover:bg-surface"
}`}>
<span className="font-medium">All subscriptions</span>
<span className="text-xs tabular-nums opacity-70">{totalVideos}</span>
</button>
{channels.length > 0 && (
<div className="pt-3 pb-1 px-3 text-[11px] uppercase tracking-wider text-muted/70">
Channels
</div>
)}
{channels.map((c) => (
<button key={c.id} onClick={() => onSelect(c.id)} title={c.title}
className={`${rowBase} ${
activeChannel === c.id ? "bg-raised text-white" : "text-muted hover:bg-surface"
}`}>
<span className="truncate">{c.title}</span>
<span className="text-xs tabular-nums opacity-70 shrink-0">
{c.downloaded_count > 0 && (
<span className="text-emerald-400">{c.downloaded_count}/</span>
)}
{c.video_count}
</span>
</button>
))}
</nav>
<button onClick={onOpenSettings}
className="m-2 px-3 py-2 rounded-lg text-sm text-muted hover:bg-surface text-left cursor-pointer">
Settings
</button>
</aside>
);
}