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:
+28
-7
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user