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
+92
View File
@@ -0,0 +1,92 @@
interface Props {
search: string;
onSearch: (v: string) => void;
downloadedOnly: boolean;
onDownloadedOnly: (v: boolean) => void;
hideShorts: boolean;
onHideShorts: (v: boolean) => void;
online: boolean;
reachable: boolean;
forcedOffline: boolean;
onToggleForcedOffline: () => void;
onRefresh: () => void;
refreshing: boolean;
refreshProgress: { done: number; total: number } | null;
}
function Toggle({
active, onClick, children, title, disabled,
}: {
active: boolean;
onClick: () => void;
children: React.ReactNode;
title?: string;
disabled?: boolean;
}) {
return (
<button onClick={onClick} title={title} disabled={disabled}
className={`rounded-full px-3 py-1.5 text-xs font-medium transition-colors cursor-pointer whitespace-nowrap ${
active ? "bg-white text-black" : "bg-surface text-muted hover:bg-raised hover:text-white"
} disabled:opacity-40 disabled:cursor-not-allowed`}>
{children}
</button>
);
}
export default function TopBar({
search, onSearch, downloadedOnly, onDownloadedOnly, hideShorts, onHideShorts,
online, reachable, forcedOffline, onToggleForcedOffline,
onRefresh, refreshing, refreshProgress,
}: Props) {
return (
<header className="border-b border-edge px-5 py-3 flex items-center gap-3 flex-wrap bg-ink">
<div className="relative flex-1 min-w-52 max-w-md">
<input value={search} onChange={(e) => onSearch(e.target.value)}
placeholder="Search videos and channels"
className="w-full rounded-full bg-surface border border-edge px-4 py-2 text-sm
placeholder:text-muted/70 focus:outline-none focus:border-accent" />
</div>
<Toggle active={downloadedOnly} onClick={() => onDownloadedOnly(!downloadedOnly)}
disabled={!online}
title={online ? "Show only downloaded videos" : "Offline: showing downloads only"}>
Downloaded only
</Toggle>
<Toggle active={hideShorts} onClick={() => onHideShorts(!hideShorts)}
title="Hide Shorts from the feed">
Hide Shorts
</Toggle>
<button onClick={onRefresh} disabled={refreshing || !online}
title={online ? "Fetch the latest videos" : "Refreshing needs a connection"}
className="rounded-full bg-accent/90 hover:bg-accent text-black px-4 py-1.5 text-xs
font-semibold transition-colors cursor-pointer disabled:opacity-40
disabled:cursor-not-allowed tabular-nums whitespace-nowrap">
{refreshing
? refreshProgress
? `${refreshProgress.done}/${refreshProgress.total}`
: "Refreshing…"
: "Refresh"}
</button>
<button onClick={onToggleForcedOffline}
title={
!reachable
? "No connection detected"
: forcedOffline
? "Offline mode is forced on — click to go back online"
: "Simulate being offline"
}
className={`rounded-full px-3 py-1.5 text-xs font-medium flex items-center gap-1.5
cursor-pointer transition-colors whitespace-nowrap ${
online
? "bg-surface text-emerald-300 hover:bg-raised"
: "bg-amber-500/20 text-amber-300 hover:bg-amber-500/30"
}`}>
<span className={`size-2 rounded-full ${online ? "bg-emerald-400" : "bg-amber-400"}`} />
{online ? "Online" : forcedOffline ? "Offline (forced)" : "Offline"}
</button>
</header>
);
}