feat: tile view, in-app streaming, and self-painted title bar

Clicking an undownloaded video now plays it in the app instead of
handing off to the browser. YouTube's iframe embed cannot be used —
it rejects a tauri:// origin with Error 153 — so yt-dlp resolves
YouTube's HLS master playlist instead, whose H.264+AAC variants
AVFoundation streams natively in WKWebView, adaptive up to 1080p.

Adds an optional Tiles view alongside the list, remembered between
launches.

The title bar is now transparent with the title hidden, and the app
paints that strip itself in the page background so it matches instead
of showing macOS chrome beside the traffic lights.
This commit is contained in:
vincent
2026-08-29 03:24:24 +02:00
parent ccb25fad33
commit f47bc3dab1
9 changed files with 297 additions and 34 deletions
+55
View File
@@ -165,6 +165,61 @@ pub async fn import_takeout_csv(
db.replace_channels(&channels)
}
/// Resolves a directly playable URL for a video we have NOT downloaded, so it
/// can stream inside the app's own player.
///
/// YouTube's iframe embed is not an option here: it rejects a Tauri window with
/// "Error 153" because the page origin is `tauri://localhost` rather than an
/// http(s) origin it will accept. Instead we ask yt-dlp for YouTube's HLS master
/// playlist, which lists H.264 + AAC variants up to 1080p with separate audio
/// tracks — exactly the shape AVFoundation plays natively in WKWebView, with
/// adaptive bitrate for free.
#[tauri::command]
pub async fn resolve_stream(video_id: String) -> Result<String, String> {
let url = format!("https://www.youtube.com/watch?v={video_id}");
// The HLS master playlist. Every m3u8 format shares the same manifest_url,
// so any one of them yields the master.
if let Some(u) = yt_dlp_print(
&["-f", "bv*[protocol^=m3u8]", "--print", "%(manifest_url)s", &url],
)
.await
{
return Ok(u);
}
// Rare fallback: an old-style progressive muxed MP4.
if let Some(u) = yt_dlp_print(&[
"-f",
"b[ext=mp4][acodec!=none][vcodec!=none]",
"--print",
"%(url)s",
&url,
])
.await
{
return Ok(u);
}
Err("Could not find a playable stream for this video.".into())
}
/// Runs yt-dlp and returns its first non-empty stdout line, or None.
async fn yt_dlp_print(args: &[&str]) -> Option<String> {
let mut cmd = tokio::process::Command::new(bin("yt-dlp"));
cmd.args(["--no-warnings", "--no-playlist", "--simulate"]);
cmd.args(args);
let out = cmd.output().await.ok()?;
if !out.status.success() {
return None;
}
String::from_utf8_lossy(&out.stdout)
.lines()
.map(str::trim)
.find(|l| l.starts_with("http"))
.map(str::to_string)
}
#[tauri::command]
pub async fn list_channels(state: State<'_, AppState>) -> Result<Vec<ChannelWithCount>, String> {
state.db.lock().await.list_channels()
+1
View File
@@ -25,6 +25,7 @@ pub fn run() {
commands::preview_takeout_import,
commands::list_channels,
commands::list_feed,
commands::resolve_stream,
commands::refresh_feeds,
commands::download_video,
commands::cancel_download,
+3 -1
View File
@@ -17,7 +17,9 @@
"height": 780,
"minWidth": 900,
"minHeight": 560,
"center": true
"center": true,
"titleBarStyle": "Transparent",
"hiddenTitle": true
}
],
"security": {