feat: subtitles, delete-all, auto refresh, and menu cleanup
Subtitles: a language preference in Settings drives what is shown and what is downloaded. yt-dlp saves WebVTT sidecars next to each download, including YouTube's auto-generated track, and the player attaches them as <track> elements so they work offline. Sidecars are removed with their video, and by Delete all. WebVTT rather than muxed subtitle streams because WebKit reads a <track> reliably and largely ignores subtitle tracks inside an MP4. Downloads in progress now appear under the downloaded-only filter, so a download you just started does not vanish from the list you are watching it in. That view also gains a Delete all, behind a confirmation naming what goes. The feed refreshes on launch and whenever the player closes, so the Refresh button is only for staleness. Player: Delete moved to the footer and shortened, Open on YouTube is now an external-link icon. Removes the Edit and Help menus. Cut/Copy/Paste move to the app menu, without which their shortcuts would stop working in the search field. The webview context menu is suppressed outside text fields — its Reload and Back items act on a page the app does not present as one. Settings now warns that 4K AV1 plays back with artefacts: the files decode cleanly in ffmpeg, so it is the built-in decoder, not the download.
This commit is contained in:
+53
-1
@@ -369,7 +369,9 @@ impl Db {
|
||||
args.push(Box::new(pat));
|
||||
}
|
||||
if f.downloaded_only {
|
||||
sql.push_str(" AND d.state = 'done'");
|
||||
// Queued and running count: a download you started should not
|
||||
// vanish from the very list you are watching it in.
|
||||
sql.push_str(" AND d.state IN ('done','queued','running')");
|
||||
}
|
||||
if f.hide_shorts {
|
||||
sql.push_str(" AND v.is_short = 0");
|
||||
@@ -498,6 +500,26 @@ impl Db {
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Paths of every completed download, for deleting them all at once.
|
||||
pub fn all_download_paths(&self) -> Result<Vec<String>, String> {
|
||||
let mut stmt = self
|
||||
.conn
|
||||
.prepare("SELECT path FROM downloads WHERE path IS NOT NULL")
|
||||
.map_err(|e| e.to_string())?;
|
||||
let rows = stmt
|
||||
.query_map([], |r| r.get::<_, String>(0))
|
||||
.map_err(|e| e.to_string())?;
|
||||
rows.collect::<Result<Vec<_>, _>>().map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
pub fn clear_all_downloads(&self) -> Result<usize, String> {
|
||||
let n = self
|
||||
.conn
|
||||
.execute("DELETE FROM downloads", [])
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(n)
|
||||
}
|
||||
|
||||
pub fn clear_download(&self, video_id: &str) -> Result<(), String> {
|
||||
self.conn
|
||||
.execute("DELETE FROM downloads WHERE video_id = ?1", params![video_id])
|
||||
@@ -737,6 +759,36 @@ mod tests {
|
||||
assert_eq!(feed[0].id, "b");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn downloads_in_progress_still_show_in_the_downloaded_filter() {
|
||||
let db = seeded();
|
||||
db.set_download_state("a", DownloadState::Running, None).unwrap();
|
||||
db.set_download_state("c", DownloadState::Queued, None).unwrap();
|
||||
db.set_download_state("b", DownloadState::Failed, Some("x")).unwrap();
|
||||
|
||||
let feed = db
|
||||
.list_feed(&FeedFilter { downloaded_only: true, ..Default::default() })
|
||||
.unwrap();
|
||||
let ids: Vec<&str> = feed.iter().map(|f| f.id.as_str()).collect();
|
||||
assert!(ids.contains(&"a"), "running should be listed");
|
||||
assert!(ids.contains(&"c"), "queued should be listed");
|
||||
assert!(!ids.contains(&"b"), "failed should not be");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clearing_all_downloads_empties_the_filter() {
|
||||
let db = seeded();
|
||||
db.set_download_state("a", DownloadState::Done, None).unwrap();
|
||||
db.set_download_path("a", "/movies/a.mp4").unwrap();
|
||||
db.set_download_state("b", DownloadState::Done, None).unwrap();
|
||||
assert_eq!(db.all_download_paths().unwrap(), vec!["/movies/a.mp4".to_string()]);
|
||||
db.clear_all_downloads().unwrap();
|
||||
assert!(db
|
||||
.list_feed(&FeedFilter { downloaded_only: true, ..Default::default() })
|
||||
.unwrap()
|
||||
.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hide_shorts_filter_excludes_shorts() {
|
||||
let db = seeded();
|
||||
|
||||
Reference in New Issue
Block a user