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
+14 -3
View File
@@ -12,6 +12,7 @@ import type {
Quality,
RefreshProgress,
RefreshSummary,
Stream,
} from "./types";
export const checkPrereqs = () => invoke<Prereqs>("check_prereqs");
@@ -37,9 +38,19 @@ export const deleteDownload = (videoId: string) =>
export const getConnectivity = () => invoke<boolean>("get_connectivity");
/** A directly playable URL (HLS master playlist) for an undownloaded video. */
export const resolveStream = (videoId: string) =>
invoke<string>("resolve_stream", { videoId });
/**
* A directly playable URL for an undownloaded video. When a quality cap is set
* the backend serves a rewritten playlist from its own loopback server, so this
* is always a plain URL either way.
*/
export async function resolveStream(
videoId: string,
maxHeight: number | null,
): Promise<string> {
const s = await invoke<Stream>("resolve_stream", { videoId, maxHeight });
if (!s.url) throw new Error("No playable stream was returned.");
return s.url;
}
export const setLibraryPath = (path: string) =>
invoke<string>("set_library_path", { path });