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:
+61
-36
@@ -1,4 +1,5 @@
|
||||
import type { ChannelWithCount } from "../types";
|
||||
import { BTN_CHROME, HEADING } from "./ui";
|
||||
|
||||
interface Props {
|
||||
channels: ChannelWithCount[];
|
||||
@@ -6,56 +7,80 @@ interface Props {
|
||||
onSelect: (id: string | null) => void;
|
||||
onOpenSettings: () => void;
|
||||
totalVideos: number;
|
||||
totalDownloaded: number;
|
||||
}
|
||||
|
||||
export default function Sidebar({
|
||||
channels, activeChannel, onSelect, onOpenSettings, totalVideos,
|
||||
channels, activeChannel, onSelect, onOpenSettings, totalVideos, totalDownloaded,
|
||||
}: 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";
|
||||
const row =
|
||||
"flex w-full cursor-pointer items-center justify-between gap-2 rounded-lg px-2 py-1.5 " +
|
||||
"text-[13px] transition-colors";
|
||||
const inactive =
|
||||
"text-slate-600 hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-800";
|
||||
const active = "bg-slate-900 text-white dark:bg-white dark:text-slate-900";
|
||||
|
||||
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>
|
||||
<aside
|
||||
className="flex max-h-[45vh] w-full shrink-0 flex-col overflow-hidden border-b
|
||||
border-slate-300 bg-white lg:h-screen lg:max-h-none lg:w-[280px]
|
||||
lg:border-b-0 lg:border-r dark:border-slate-800 dark:bg-slate-900"
|
||||
>
|
||||
<header
|
||||
className="sticky top-0 z-20 flex items-center justify-between gap-2 border-b
|
||||
border-slate-200 bg-white/95 px-4 py-3 backdrop-blur
|
||||
dark:border-slate-800 dark:bg-slate-900/95"
|
||||
>
|
||||
<span className="flex items-center gap-2 text-[15px] font-semibold tracking-tight">
|
||||
<span aria-hidden className="text-sky-500">✈</span>
|
||||
FlightTube
|
||||
</span>
|
||||
<button onClick={onOpenSettings} className={`${BTN_CHROME} cursor-pointer`}>
|
||||
Settings
|
||||
</button>
|
||||
</header>
|
||||
|
||||
<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"
|
||||
}`}>
|
||||
<nav className="min-h-0 flex-1 overflow-y-auto px-2 py-3">
|
||||
<button
|
||||
onClick={() => onSelect(null)}
|
||||
className={`${row} ${activeChannel === null ? active : inactive}`}
|
||||
>
|
||||
<span className="font-medium">All subscriptions</span>
|
||||
<span className="text-xs tabular-nums opacity-70">{totalVideos}</span>
|
||||
<span className="shrink-0 font-mono text-[11px] tabular-nums opacity-70">
|
||||
{totalDownloaded > 0 && `${totalDownloaded}/`}
|
||||
{totalVideos}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{channels.length > 0 && (
|
||||
<div className="pt-3 pb-1 px-3 text-[11px] uppercase tracking-wider text-muted/70">
|
||||
Channels
|
||||
</div>
|
||||
<h2 className={`${HEADING} px-2 pb-1 pt-4`}>Channels</h2>
|
||||
)}
|
||||
|
||||
{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>
|
||||
<ul className="space-y-0.5">
|
||||
{channels.map((c) => (
|
||||
<li key={c.id}>
|
||||
<button
|
||||
onClick={() => onSelect(c.id)}
|
||||
title={c.title}
|
||||
className={`${row} ${activeChannel === c.id ? active : inactive}`}
|
||||
>
|
||||
<span className="truncate">{c.title}</span>
|
||||
<span className="shrink-0 font-mono text-[11px] tabular-nums opacity-70">
|
||||
{c.downloaded_count > 0 && `${c.downloaded_count}/`}
|
||||
{c.video_count}
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<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>
|
||||
{channels.length === 0 && (
|
||||
<p className="px-2 py-3 text-[11px] leading-snug text-slate-500 dark:text-slate-400">
|
||||
No subscriptions yet. Open Settings for a step-by-step guide to exporting
|
||||
them from Google Takeout.
|
||||
</p>
|
||||
)}
|
||||
</nav>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user