feat: uniform control height, chrome auto-hide, streaming quality

Every control in the app now shares one height (CONTROL_H), with width
still growing to fit its label. The player transport is larger, Back is
an icon, and the footer buttons match the prev/next size.

Player chrome — transport and edge arrows — fades after 2.6s of
inactivity and never hides while paused.

The title-bar strip collapses in macOS window fullscreen, where the
traffic lights are gone and it was just a blank white bar.

Streaming quality is now selectable alongside download quality. A cap is
applied by rewriting YouTube's HLS master playlist down to the best
variant at or below the chosen height, keeping its audio group. That
playlist is served from a small loopback HTTP server: Safari's native
HLS is backed by AVFoundation, which cannot read blob: or custom-scheme
URLs, so a Blob URL silently fails to play.

Settings closes with an icon button and its selects match the shared
control height.
This commit is contained in:
vincent
2026-08-29 11:28:05 +02:00
parent 211823f265
commit 9bb7b71225
16 changed files with 503 additions and 92 deletions
+12 -15
View File
@@ -1,4 +1,4 @@
import { INPUT, Segmented } from "./ui";
import { CONTROL_H, ICON_BTN, INPUT, Segmented } from "./ui";
export type ViewMode = "list" | "grid";
@@ -39,7 +39,7 @@ function Toggle({
title={title}
disabled={disabled}
className={
"cursor-pointer whitespace-nowrap rounded-lg border px-2.5 py-1.5 text-[11px] " +
`inline-flex ${CONTROL_H} cursor-pointer items-center whitespace-nowrap rounded-lg border px-2.5 text-[12px] ` +
"font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-40 " +
(active
? "border-sky-500 bg-sky-500 text-white hover:bg-sky-400"
@@ -73,10 +73,9 @@ export default function TopBar({
onClick={onShowSidebar}
title="Show subscriptions"
aria-label="Show subscriptions"
className="grid size-[30px] shrink-0 cursor-pointer place-items-center rounded-lg
border 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"
className={`${ICON_BTN} border 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`}
>
<svg viewBox="0 0 24 24" className="size-4" fill="none" stroke="currentColor" strokeWidth="1.8">
<path strokeLinecap="round" strokeLinejoin="round" d="M4 6h16M4 12h16M4 18h16" />
@@ -87,7 +86,7 @@ export default function TopBar({
value={search}
onChange={(e) => onSearch(e.target.value)}
placeholder="Search videos and channels"
className={`${INPUT} min-w-72 max-w-sm flex-1 py-1.5`}
className={`${INPUT} min-w-72 max-w-sm flex-1`}
/>
<span className="font-mono text-[11px] tabular-nums text-slate-400 dark:text-slate-500">
@@ -125,11 +124,11 @@ export default function TopBar({
? "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"
className={`inline-flex ${CONTROL_H} cursor-pointer items-center gap-1.5 whitespace-nowrap
rounded-lg border border-slate-300 px-2.5 text-[12px] 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"}`}
@@ -146,9 +145,7 @@ export default function TopBar({
: "Refreshing needs a connection"
}
aria-label="Refresh"
className="grid size-[30px] shrink-0 cursor-pointer place-items-center rounded-lg
bg-sky-500 text-white hover:bg-sky-400 disabled:cursor-not-allowed
disabled:opacity-40">
className={`${ICON_BTN} bg-sky-500 text-white hover:bg-sky-400`}>
<svg viewBox="0 0 24 24" className={`size-4 ${refreshing ? "animate-spin" : ""}`}
fill="none" stroke="currentColor" strokeWidth="2">
<path strokeLinecap="round" strokeLinejoin="round"