fix: native video fullscreen, window dragging, quality picker, spinner

Enable WKWebView's elementFullscreenEnabled at startup. It is off by
default in a Tauri window, which is why the native player had no
full-screen button and why right-click 'Enter Full Screen' did nothing.
With it on, WebKit's own control appears on the video and gives the real
system fullscreen player, so the app-level workaround is gone.

Restore window dragging: data-tauri-drag-region needs
core:window:allow-start-dragging, which was missing, leaving the
title-bar strip inert.

Download quality is now a resolution picker (best/2160/1440/1080/720/
480) instead of a two-way toggle; every selector still pins AAC audio
because Opus in MP4 is silent in WebKit.

Tile titles truncate to a single line so rows stay uniform, and the
player shows a spinner while the video buffers, not just while the
stream URL resolves.
This commit is contained in:
vincent
2026-08-29 10:38:35 +02:00
parent 359873cff1
commit 61db10b2c8
11 changed files with 143 additions and 106 deletions
+36 -22
View File
@@ -1,20 +1,23 @@
//! Driving `yt-dlp` and interpreting its progress output.
/// Highest resolution available, which on YouTube means VP9 or AV1 above 1080p.
/// Audio is still pinned to AAC (`m4a`): YouTube pairs those codecs with Opus,
/// which WebKit will not decode inside an MP4 container, so taking Opus would
/// yield a silent file.
/// Audio is pinned to AAC (`m4a`) in every selector: YouTube pairs those codecs
/// with Opus, which WebKit will not decode inside an MP4 container, so taking
/// Opus would yield a silent file.
pub const FORMAT_BEST: &str = "bv*+ba[ext=m4a]/bv*+ba/b";
/// H.264 video plus AAC audio. Caps at 1080p — YouTube serves H.264 no higher —
/// but is guaranteed to decode in WKWebView on any Mac.
pub const FORMAT_COMPATIBLE: &str =
"bv*[vcodec^=avc1]+ba[acodec^=mp4a]/bv*[vcodec^=avc1]+ba/b[ext=mp4]/bv*+ba/b";
pub fn format_selector(quality: &str) -> &'static str {
match quality {
"compatible" => FORMAT_COMPATIBLE,
_ => FORMAT_BEST,
/// Builds a format selector for a chosen quality.
///
/// `quality` is either "best" or a maximum height in pixels ("2160", "1080", …).
/// Anything unrecognised falls back to best, so a stale stored preference can
/// never leave downloads broken.
pub fn format_selector(quality: &str) -> String {
match quality.parse::<u32>() {
Ok(height) if (144..=4320).contains(&height) => format!(
"bv*[height<={h}]+ba[ext=m4a]/bv*[height<={h}]+ba/b[height<={h}]/b",
h = height
),
_ => FORMAT_BEST.to_string(),
}
}
@@ -79,7 +82,7 @@ pub fn parse_progress_line(line: &str) -> Option<Progress> {
pub fn build_args(video_id: &str, out_template: &str, quality: &str) -> Vec<String> {
vec![
"-f".into(),
format_selector(quality).into(),
format_selector(quality),
"--merge-output-format".into(),
"mp4".into(),
"--no-playlist".into(),
@@ -173,26 +176,37 @@ mod tests {
}
#[test]
fn compatible_quality_pins_h264_and_aac() {
let args = build_args("abc123", "/tmp/out.%(ext)s", "compatible");
assert!(args.contains(&FORMAT_COMPATIBLE.to_string()));
fn best_quality_takes_the_highest_available() {
let args = build_args("abc123", "/tmp/out.%(ext)s", "best");
assert!(args.contains(&FORMAT_BEST.to_string()));
assert!(args.contains(&"mp4".to_string()));
assert!(args.contains(&"https://www.youtube.com/watch?v=abc123".to_string()));
assert!(args.contains(&"--no-playlist".to_string()));
}
#[test]
fn best_quality_still_pins_aac_audio() {
let args = build_args("abc123", "/tmp/out.%(ext)s", "best");
assert!(args.contains(&FORMAT_BEST.to_string()));
fn every_selector_pins_aac_audio() {
// Opus in MP4 would be silent in WebKit, so the audio half stays m4a.
assert!(FORMAT_BEST.contains("ba[ext=m4a]"));
for q in ["best", "2160", "1080", "480"] {
assert!(format_selector(q).contains("ba[ext=m4a]"), "quality {q}");
}
}
#[test]
fn unknown_quality_falls_back_to_best() {
fn a_numeric_quality_caps_the_height() {
let sel = format_selector("1080");
assert!(sel.contains("height<=1080"));
assert!(!sel.contains("height<=2160"));
assert!(build_args("x", "o", "1080").contains(&sel));
}
#[test]
fn nonsense_or_out_of_range_quality_falls_back_to_best() {
assert_eq!(format_selector("nonsense"), FORMAT_BEST);
assert_eq!(format_selector("compatible"), FORMAT_COMPATIBLE);
assert_eq!(format_selector(""), FORMAT_BEST);
assert_eq!(format_selector("0"), FORMAT_BEST);
assert_eq!(format_selector("99999"), FORMAT_BEST);
assert_eq!(format_selector("best"), FORMAT_BEST);
}
#[test]