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:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user