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:
@@ -20,7 +20,7 @@ function clock(seconds: number): string {
|
||||
}
|
||||
|
||||
const btn =
|
||||
"grid size-8 shrink-0 place-items-center rounded-md text-white/90 cursor-pointer " +
|
||||
"grid size-10 shrink-0 place-items-center rounded-lg text-white/90 cursor-pointer " +
|
||||
"hover:bg-white/15 hover:text-white disabled:opacity-30 disabled:cursor-not-allowed";
|
||||
|
||||
/**
|
||||
@@ -113,17 +113,17 @@ export default function PlayerControls({ videoRef, stageRef, onActivity }: Props
|
||||
<div
|
||||
// Clicks here must not reach the video's own play/pause handler.
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="absolute inset-x-0 bottom-0 z-20 flex items-center gap-2 bg-gradient-to-t
|
||||
from-slate-950/85 via-slate-950/60 to-transparent px-4 pb-3 pt-8"
|
||||
className="absolute inset-x-0 bottom-0 z-20 flex items-center gap-2.5 bg-gradient-to-t
|
||||
from-slate-950/90 via-slate-950/65 to-transparent px-5 pb-4 pt-10"
|
||||
>
|
||||
<button onClick={togglePlay} className={btn} title={playing ? "Pause (space)" : "Play (space)"}>
|
||||
{playing ? (
|
||||
<svg viewBox="0 0 24 24" className="size-4" fill="currentColor">
|
||||
<svg viewBox="0 0 24 24" className="size-5" fill="currentColor">
|
||||
<rect x="6" y="5" width="4" height="14" rx="1" />
|
||||
<rect x="14" y="5" width="4" height="14" rx="1" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg viewBox="0 0 24 24" className="size-4" fill="currentColor">
|
||||
<svg viewBox="0 0 24 24" className="size-5" fill="currentColor">
|
||||
<path d="M8 5.5v13l11-6.5z" />
|
||||
</svg>
|
||||
)}
|
||||
@@ -134,7 +134,7 @@ export default function PlayerControls({ videoRef, stageRef, onActivity }: Props
|
||||
className={btn}
|
||||
title={`Back ${SKIP_S}s`}
|
||||
>
|
||||
<svg viewBox="0 0 24 24" className="size-4" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<svg viewBox="0 0 24 24" className="size-5" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M11 17l-5-5 5-5M18 17l-5-5 5-5" />
|
||||
</svg>
|
||||
</button>
|
||||
@@ -143,12 +143,12 @@ export default function PlayerControls({ videoRef, stageRef, onActivity }: Props
|
||||
className={btn}
|
||||
title={`Forward ${SKIP_S}s`}
|
||||
>
|
||||
<svg viewBox="0 0 24 24" className="size-4" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<svg viewBox="0 0 24 24" className="size-5" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M13 7l5 5-5 5M6 7l5 5-5 5" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<span className="shrink-0 font-mono text-[11px] tabular-nums text-white/80">{clock(time)}</span>
|
||||
<span className="shrink-0 font-mono text-[12px] tabular-nums text-white/85">{clock(time)}</span>
|
||||
|
||||
<input
|
||||
type="range"
|
||||
@@ -162,20 +162,20 @@ export default function PlayerControls({ videoRef, stageRef, onActivity }: Props
|
||||
onActivity?.();
|
||||
}}
|
||||
aria-label="Seek"
|
||||
className="h-1 min-w-0 flex-1 cursor-pointer appearance-none rounded-full bg-white/25 accent-sky-500"
|
||||
className="h-1.5 min-w-0 flex-1 cursor-pointer appearance-none rounded-full bg-white/25 accent-sky-500"
|
||||
style={{
|
||||
background: `linear-gradient(to right, var(--color-sky-500) ${pct}%, rgba(255,255,255,0.25) ${pct}%)`,
|
||||
}}
|
||||
/>
|
||||
|
||||
<span className="shrink-0 font-mono text-[11px] tabular-nums text-white/80">{clock(duration)}</span>
|
||||
<span className="shrink-0 font-mono text-[12px] tabular-nums text-white/85">{clock(duration)}</span>
|
||||
|
||||
<button
|
||||
onClick={act((v) => (v.muted = !v.muted))}
|
||||
className={btn}
|
||||
title={muted || volume === 0 ? "Unmute" : "Mute"}
|
||||
>
|
||||
<svg viewBox="0 0 24 24" className="size-4" fill="currentColor">
|
||||
<svg viewBox="0 0 24 24" className="size-5" fill="currentColor">
|
||||
<path d="M4 9v6h4l5 4V5L8 9H4z" />
|
||||
{muted || volume === 0 ? (
|
||||
<path d="M16 9l5 6M21 9l-5 6" stroke="currentColor" strokeWidth="2" fill="none" strokeLinecap="round" />
|
||||
@@ -204,7 +204,7 @@ export default function PlayerControls({ videoRef, stageRef, onActivity }: Props
|
||||
onActivity?.();
|
||||
}}
|
||||
aria-label="Volume"
|
||||
className="h-1 w-20 shrink-0 cursor-pointer appearance-none rounded-full accent-sky-500"
|
||||
className="h-1.5 w-24 shrink-0 cursor-pointer appearance-none rounded-full accent-sky-500"
|
||||
style={{
|
||||
background: `linear-gradient(to right, rgba(255,255,255,0.85) ${
|
||||
(muted ? 0 : volume) * 100
|
||||
@@ -213,14 +213,14 @@ export default function PlayerControls({ videoRef, stageRef, onActivity }: Props
|
||||
/>
|
||||
|
||||
<button onClick={togglePip} className={btn} title={pip ? "Leave Picture in Picture" : "Picture in Picture"}>
|
||||
<svg viewBox="0 0 24 24" className="size-4" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<svg viewBox="0 0 24 24" className="size-5" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<rect x="3" y="5" width="18" height="14" rx="2" />
|
||||
<rect x="12" y="11" width="7" height="6" rx="1" fill="currentColor" stroke="none" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button onClick={toggleFull} className={btn} title={full ? "Leave full screen (f)" : "Full screen (f)"}>
|
||||
<svg viewBox="0 0 24 24" className="size-4" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<svg viewBox="0 0 24 24" className="size-5" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
{full ? (
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 4v5H4M15 4v5h5M9 20v-5H4M15 20v-5h5" />
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user