feat: custom transport bar, live refresh, collapsible sidebar

Replace WebKit's native video controls with our own. WebKit puts
fullscreen, Picture-in-Picture and volume as overlay buttons in the
video's top corners with no way to move them; owning the bar is the only
way to get every control into one strip along the bottom. Right-click on
the player is suppressed — its menu acted on a video the app does not
control.

One spinner now covers both waits: resolving the stream URL and
buffering it. It no longer sticks after a resume-seek, which fires
'waiting' after 'playing'.

Also: Open on YouTube and a new Download are real buttons in the player;
refresh is a compact icon; Settings is a cog; the subscription sidebar
collapses and reappears on a left-edge hover; the feed auto-refreshes
every 10 minutes while online, skipped while the player is open; the
title-bar strip takes the panel colour, since it sits above panels
rather than the page; and downloaded-only, hide-Shorts and the sidebar
state are remembered between launches.
This commit is contained in:
vincent
2026-08-29 11:03:29 +02:00
parent 61db10b2c8
commit 211823f265
6 changed files with 448 additions and 56 deletions
+35 -6
View File
@@ -8,10 +8,14 @@ interface Props {
onOpenSettings: () => void;
totalVideos: number;
totalDownloaded: number;
onHide: () => void;
/** True when revealed by hover over the left edge rather than pinned open. */
floating?: boolean;
}
export default function Sidebar({
channels, activeChannel, onSelect, onOpenSettings, totalVideos, totalDownloaded,
onHide, floating,
}: Props) {
const row =
"flex w-full cursor-pointer items-center justify-between gap-2 rounded-lg px-2 py-1.5 " +
@@ -22,9 +26,14 @@ export default function Sidebar({
return (
<aside
className="flex max-h-[45vh] w-full shrink-0 flex-col overflow-hidden border-b
border-slate-300 bg-white lg:h-full lg:max-h-none lg:w-[280px]
lg:border-b-0 lg:border-r dark:border-slate-800 dark:bg-slate-900"
className={
"flex flex-col overflow-hidden border-slate-300 bg-white dark:border-slate-800 " +
"dark:bg-slate-900 " +
(floating
? "absolute inset-y-0 left-0 z-30 w-[280px] border-r shadow-2xl"
: "max-h-[45vh] w-full shrink-0 border-b lg:h-full lg:max-h-none lg:w-[280px] " +
"lg:border-b-0 lg:border-r")
}
>
<header
className="sticky top-0 z-20 flex items-center justify-between gap-2 border-b
@@ -35,9 +44,29 @@ export default function Sidebar({
<span aria-hidden className="text-sky-500"></span>
FlightTube
</span>
<button onClick={onOpenSettings} className={`${BTN_CHROME} cursor-pointer`}>
Settings
</button>
<div className="flex shrink-0 items-center gap-1">
<button
onClick={onOpenSettings}
title="Settings"
aria-label="Settings"
className={`${BTN_CHROME} grid size-7 cursor-pointer place-items-center`}
>
<svg viewBox="0 0 24 24" className="size-4" fill="none" stroke="currentColor" strokeWidth="1.8">
<circle cx="12" cy="12" r="3.2" />
<path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 11-2.83 2.83l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 11-4 0v-.09A1.65 1.65 0 008 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 11-2.83-2.83l.06-.06A1.65 1.65 0 004.6 15a1.65 1.65 0 00-1.51-1H3a2 2 0 110-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 112.83-2.83l.06.06A1.65 1.65 0 009 4.6a1.65 1.65 0 001-1.51V3a2 2 0 114 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 112.83 2.83l-.06.06A1.65 1.65 0 0019.4 9c.14.34.4.62.73.79.24.13.51.2.78.21H21a2 2 0 110 4h-.09c-.7.01-1.33.43-1.51 1z" />
</svg>
</button>
<button
onClick={onHide}
title="Hide subscriptions"
aria-label="Hide subscriptions"
className={`${BTN_CHROME} grid size-7 cursor-pointer place-items-center`}
>
<svg viewBox="0 0 24 24" className="size-4" fill="none" stroke="currentColor" strokeWidth="1.8">
<path strokeLinecap="round" strokeLinejoin="round" d="M15 6l-6 6 6 6" />
</svg>
</button>
</div>
</header>
<nav className="min-h-0 flex-1 overflow-y-auto px-2 py-3">