feat: Takeout walkthrough, design-system restyle, replacing import
Adds a seven-step in-app guide to exporting subscriptions from Google Takeout, with the real URLs opened in the system browser. Restyles the app onto the supplied design system: slate/sky palette, 9-15px type ladder, outline-first controls, tiered radii, borders for separation and shadows only for elevation. Light and dark are both designed, with a System/Light/Dark control and a pre-paint script so the window does not flash light on a dark machine. Importing now replaces the subscription list rather than merging, per request. Because that can delete downloaded files, a confirmation dialog names exactly what will go first.
This commit is contained in:
+92
-51
@@ -1,3 +1,5 @@
|
||||
import { BTN_PRIMARY, INPUT } from "./ui";
|
||||
|
||||
interface Props {
|
||||
search: string;
|
||||
onSearch: (v: string) => void;
|
||||
@@ -12,8 +14,10 @@ interface Props {
|
||||
onRefresh: () => void;
|
||||
refreshing: boolean;
|
||||
refreshProgress: { done: number; total: number } | null;
|
||||
resultCount: number;
|
||||
}
|
||||
|
||||
/** Neutral outline until active; active is the one filled state. */
|
||||
function Toggle({
|
||||
active, onClick, children, title, disabled,
|
||||
}: {
|
||||
@@ -24,10 +28,19 @@ function Toggle({
|
||||
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`}>
|
||||
<button
|
||||
onClick={onClick}
|
||||
title={title}
|
||||
disabled={disabled}
|
||||
className={
|
||||
"cursor-pointer whitespace-nowrap rounded-lg border px-2.5 py-1.5 text-[11px] " +
|
||||
"font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-40 " +
|
||||
(active
|
||||
? "border-sky-500 bg-sky-500 text-white hover:bg-sky-400"
|
||||
: "border-slate-300 text-slate-500 hover:border-sky-500 hover:text-sky-600 " +
|
||||
"dark:border-slate-700 dark:text-slate-400 dark:hover:border-sky-500 dark:hover:text-sky-400")
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
@@ -36,57 +49,85 @@ function Toggle({
|
||||
export default function TopBar({
|
||||
search, onSearch, downloadedOnly, onDownloadedOnly, hideShorts, onHideShorts,
|
||||
online, reachable, forcedOffline, onToggleForcedOffline,
|
||||
onRefresh, refreshing, refreshProgress,
|
||||
onRefresh, refreshing, refreshProgress, resultCount,
|
||||
}: Props) {
|
||||
const pct = refreshProgress && refreshProgress.total > 0
|
||||
? (refreshProgress.done / refreshProgress.total) * 100
|
||||
: null;
|
||||
|
||||
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)}
|
||||
<div
|
||||
className="sticky top-0 z-20 border-b border-slate-200 bg-white/95 backdrop-blur
|
||||
dark:border-slate-800 dark:bg-slate-900/95"
|
||||
>
|
||||
<div className="flex flex-wrap items-center gap-2 px-4 py-3">
|
||||
<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" />
|
||||
className={`${INPUT} min-w-48 max-w-sm flex-1 py-1.5`}
|
||||
/>
|
||||
|
||||
<span className="font-mono text-[11px] tabular-nums text-slate-400 dark:text-slate-500">
|
||||
{resultCount}
|
||||
</span>
|
||||
|
||||
<div className="flex-1" />
|
||||
|
||||
<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={onToggleForcedOffline}
|
||||
title={
|
||||
!reachable
|
||||
? "No connection detected"
|
||||
: forcedOffline
|
||||
? "Offline mode is forced on — click to go back online"
|
||||
: "Simulate being offline"
|
||||
}
|
||||
className="flex cursor-pointer items-center gap-1.5 whitespace-nowrap rounded-lg border
|
||||
border-slate-300 px-2.5 py-1.5 text-[11px] font-medium text-slate-500
|
||||
hover:border-sky-500 hover:text-sky-600
|
||||
dark:border-slate-700 dark:text-slate-400 dark:hover:border-sky-500
|
||||
dark:hover:text-sky-400"
|
||||
>
|
||||
<span
|
||||
className={`size-1.5 rounded-full ${online ? "bg-sky-500" : "bg-amber-500"}`}
|
||||
/>
|
||||
{online ? "Online" : forcedOffline ? "Offline (forced)" : "Offline"}
|
||||
</button>
|
||||
|
||||
<button onClick={onRefresh} disabled={refreshing || !online}
|
||||
title={online ? "Fetch the latest videos from every channel" : "Refreshing needs a connection"}
|
||||
className={`${BTN_PRIMARY} cursor-pointer py-1.5 font-mono text-[11px] tabular-nums`}>
|
||||
{refreshing
|
||||
? refreshProgress
|
||||
? `${refreshProgress.done}/${refreshProgress.total}`
|
||||
: "Refreshing"
|
||||
: "Refresh"}
|
||||
</button>
|
||||
</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>
|
||||
{refreshing && (
|
||||
<div className="h-0.5 overflow-hidden bg-slate-200 dark:bg-slate-700">
|
||||
<div
|
||||
className={`h-full bg-sky-500 transition-[width] duration-100 ${
|
||||
pct == null ? "w-1/3 animate-pulse" : ""
|
||||
}`}
|
||||
style={pct == null ? undefined : { width: `${pct}%` }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user