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
+28 -7
View File
@@ -13,7 +13,11 @@ import { useAppearance } from "./hooks/useAppearance";
import { useConnectivity } from "./hooks/useConnectivity";
import { useDownloads } from "./hooks/useDownloads";
import { useFeed } from "./hooks/useFeed";
import { QUALITIES, type FeedFilter, type FeedItem, type Quality, type RefreshProgress } from "./types";
import { useWindowFullscreen } from "./hooks/useWindowFullscreen";
import {
QUALITIES, STREAM_QUALITIES,
type FeedFilter, type FeedItem, type Quality, type RefreshProgress,
} from "./types";
const TOAST_MS = 2400;
/** How often to pull new videos while online, so the feed stays live. */
@@ -44,6 +48,14 @@ export default function App() {
const [showSettings, setShowSettings] = useState(false);
// Index into the current feed, so the player can step through it.
const [playingIndex, setPlayingIndex] = useState<number | null>(null);
const [streamQuality, setStreamQuality] = useState<Quality>(() => {
try {
const stored = localStorage.getItem("flighttube.streamQuality");
return STREAM_QUALITIES.some((q) => q.value === stored) ? (stored as Quality) : "best";
} catch {
return "best";
}
});
const [quality, setQuality] = useState<Quality>(() => {
try {
const stored = localStorage.getItem("flighttube.quality");
@@ -58,6 +70,7 @@ export default function App() {
const [failure, setFailure] = useState<string | null>(null);
const { mode, setMode } = useAppearance();
const windowFullscreen = useWindowFullscreen();
const { online, reachable, forcedOffline, setForcedOffline, probe } = useConnectivity();
// A toast reports success and fades; a modal reports a failure or asks a
@@ -74,13 +87,14 @@ export default function App() {
try {
localStorage.setItem("flighttube.view", view);
localStorage.setItem("flighttube.quality", quality);
localStorage.setItem("flighttube.streamQuality", streamQuality);
localStorage.setItem("flighttube.downloadedOnly", downloadedOnly ? "1" : "0");
localStorage.setItem("flighttube.hideShorts", hideShorts ? "1" : "0");
localStorage.setItem("flighttube.sidebarHidden", sidebarHidden ? "1" : "0");
} catch {
/* storage blocked */
}
}, [view, quality, downloadedOnly, hideShorts, sidebarHidden]);
}, [view, quality, streamQuality, downloadedOnly, hideShorts, sidebarHidden]);
// Offline, the only videos that can be played are the ones already on disk,
// so the feed collapses to those regardless of the toggle.
@@ -203,11 +217,15 @@ export default function App() {
return (
<div className="flex h-screen flex-col">
{/* The webview paints the title bar itself. It sits directly above the
sidebar and top bar, so it takes the panel colour, not the page's. */}
<div
data-tauri-drag-region
className="h-9 shrink-0 bg-white dark:bg-slate-900"
/>
sidebar and top bar, so it takes the panel colour, not the page's.
In macOS window fullscreen the traffic lights are gone, so the strip
would just be a blank bar — it collapses instead. */}
{!windowFullscreen && (
<div
data-tauri-drag-region
className="h-9 shrink-0 bg-white dark:bg-slate-900"
/>
)}
<div className="relative flex min-h-0 flex-1 flex-col lg:flex-row">
{/* With the sidebar hidden, a thin strip along the left edge brings it
@@ -322,6 +340,7 @@ export default function App() {
path={playing.path}
index={playingIndex}
total={items.length}
maxHeight={streamQuality === "best" ? null : Number(streamQuality)}
onPrev={
stepFrom(playingIndex, -1) != null
? () => setPlayingIndex(stepFrom(playingIndex, -1))
@@ -361,6 +380,8 @@ export default function App() {
onAppearance={setMode}
quality={quality}
onQuality={setQuality}
streamQuality={streamQuality}
onStreamQuality={setStreamQuality}
onError={setFailure}
onImported={(n) => {
reload();