feat: embedded subtitles, resumed downloads, subtitle-only track menu

Verified in the running app and against the files on disk.

Subtitles are muxed into the download as a soft SubRip track rather
than written beside it. Without --write-subs, yt-dlp fetches them,
embeds them and removes the WebVTT, so a download is one file — checked
with ffprobe: video, audio, one mov_text track, no sidecar. WebKit
exposes it, so the player lists and shows it with no fetch at all,
which also means it works offline.

They are converted to SubRip on the way in. YouTube's WebVTT pins every
cue to the left edge and fills it with karaoke timing tags; the first
embed carried both and rendered clamped to the side of the picture.
SubRip carries neither. Only the plain language is embedded — "en" and
"en-orig" are usually the same captions, and nothing can collapse
duplicates once they are muxed in.

Downloads left unfinished when the app closes are picked up on the next
launch, at the quality and language they were asked for, which the
downloads table now records. Verified by leaving a row "running" with
no process behind it and restarting: it downloaded and completed.

The player's track menu is subtitles only — the audio renditions are
gone, the original-audio default still being fixed in the manifest —
and tracks read "English" rather than "en".

Turning subtitles off in one video now sticks for the next. The guard
that applies the preference keyed on the number of text tracks alone,
which does not change from one video to the next, so a choice of Off
was never re-applied; it keys on the video as well.

Embedding no longer depends on the on/off preference, the same trap
that kept fetching switched off: a file downloaded without a subtitle
track can never gain one offline.

Delete all is a trash icon.
This commit is contained in:
vincent
2026-08-29 17:12:21 +02:00
parent 62110b28fc
commit 2121f3e475
10 changed files with 188 additions and 99 deletions
+6 -2
View File
@@ -25,8 +25,12 @@ export const listFeed = (filter: FeedFilter) =>
export const refreshFeeds = () => invoke<RefreshSummary>("refresh_feeds");
export const downloadVideo = (videoId: string, quality: Quality, subLangs: string) =>
invoke<void>("download_video", { videoId, quality, subLangs });
export const downloadVideo = (videoId: string, quality: Quality, subLang: string) =>
invoke<void>("download_video", { videoId, quality, subLang });
/** Downloads still unfinished from a previous run, as (id, quality, subLang). */
export const interruptedDownloads = () =>
invoke<Array<[string, string, string]>>("interrupted_downloads");
/** Stops everything downloading or waiting to. Returns how many were stopped. */
export const cancelAllDownloads = () => invoke<number>("cancel_all_downloads");