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:
Generated
+24
@@ -1141,6 +1141,7 @@ dependencies = [
|
||||
"chrono",
|
||||
"csv",
|
||||
"futures",
|
||||
"objc2-web-kit",
|
||||
"quick-xml 0.42.0",
|
||||
"reqwest",
|
||||
"rusqlite",
|
||||
@@ -2597,6 +2598,16 @@ dependencies = [
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-javascript-core"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a1e6550c4caed348956ce3370c9ffeca70bb1dbed4fa96112e7c6170e074586"
|
||||
dependencies = [
|
||||
"objc2",
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-quartz-core"
|
||||
version = "0.3.2"
|
||||
@@ -2609,6 +2620,17 @@ dependencies = [
|
||||
"objc2-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-security"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a"
|
||||
dependencies = [
|
||||
"bitflags 2.13.1",
|
||||
"objc2",
|
||||
"objc2-core-foundation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc2-ui-kit"
|
||||
version = "0.3.2"
|
||||
@@ -2652,6 +2674,8 @@ dependencies = [
|
||||
"objc2-app-kit",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation",
|
||||
"objc2-javascript-core",
|
||||
"objc2-security",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -31,3 +31,6 @@ futures = "0.3.34"
|
||||
tauri-plugin-dialog = "2.7.2"
|
||||
reqwest = { version = "0.13.4", default-features = false, features = ["rustls", "http2", "charset", "stream", "gzip"] }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
objc2-web-kit = { version = "0.3.2", features = ["WKWebView", "WKWebViewConfiguration", "WKPreferences"] }
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
"core:default",
|
||||
"opener:default",
|
||||
"dialog:default",
|
||||
"core:window:allow-set-fullscreen",
|
||||
"core:window:allow-is-fullscreen"
|
||||
"core:window:allow-start-dragging"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"default":{"identifier":"default","description":"Capability for the main window","local":true,"windows":["main"],"permissions":["core:default","opener:default","dialog:default","core:window:allow-set-fullscreen","core:window:allow-is-fullscreen"]}}
|
||||
{"default":{"identifier":"default","description":"Capability for the main window","local":true,"windows":["main"],"permissions":["core:default","opener:default","dialog:default","core:window:allow-start-dragging"]}}
|
||||
+36
-22
@@ -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]
|
||||
|
||||
@@ -9,6 +9,28 @@ pub mod thumbs;
|
||||
|
||||
use tauri::Manager;
|
||||
|
||||
/// Turns on WKWebView's element fullscreen.
|
||||
///
|
||||
/// It is off by default in a Tauri window, which is why the native player has
|
||||
/// no full-screen button and why `requestFullscreen()` — and the right-click
|
||||
/// "Enter Full Screen" item — silently do nothing. Flipping this one preference
|
||||
/// puts the button back in the video's own transport bar, where it belongs.
|
||||
#[cfg(target_os = "macos")]
|
||||
fn enable_element_fullscreen(window: &tauri::WebviewWindow) {
|
||||
use objc2_web_kit::WKWebView;
|
||||
let _ = window.with_webview(|platform| unsafe {
|
||||
let ptr = platform.inner() as *const WKWebView;
|
||||
if ptr.is_null() {
|
||||
return;
|
||||
}
|
||||
let webview: &WKWebView = &*ptr;
|
||||
webview
|
||||
.configuration()
|
||||
.preferences()
|
||||
.setElementFullscreenEnabled(true);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
@@ -17,6 +39,12 @@ pub fn run() {
|
||||
.setup(|app| {
|
||||
let state = commands::build_state(&app.handle().clone())?;
|
||||
app.manage(state);
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
enable_element_fullscreen(&window);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
|
||||
Reference in New Issue
Block a user