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:
@@ -34,6 +34,8 @@ interface Props {
|
||||
stageRef: React.RefObject<HTMLDivElement | null>;
|
||||
/** Nudges the auto-hide timer whenever the user does something. */
|
||||
onActivity?: () => void;
|
||||
/** Preferred subtitle language, or "off" to start with none. */
|
||||
subLang: string;
|
||||
}
|
||||
|
||||
const SKIP_S = 10;
|
||||
@@ -59,7 +61,7 @@ const btn =
|
||||
* Owning the bar is the only way to get every control into one strip along the
|
||||
* bottom, so the native ones are switched off entirely.
|
||||
*/
|
||||
export default function PlayerControls({ videoRef, stageRef, onActivity }: Props) {
|
||||
export default function PlayerControls({ videoRef, stageRef, onActivity, subLang }: Props) {
|
||||
const [playing, setPlaying] = useState(false);
|
||||
const [time, setTime] = useState(0);
|
||||
const [duration, setDuration] = useState(0);
|
||||
@@ -83,15 +85,24 @@ export default function PlayerControls({ videoRef, stageRef, onActivity }: Props
|
||||
};
|
||||
read();
|
||||
|
||||
// Nothing translated gets forced on. YouTube marks its subtitle track
|
||||
// AUTOSELECT=YES and WebKit will switch it on when it matches the system
|
||||
// language; that is the same unwanted auto-selection as a dubbed audio
|
||||
// track, so subtitles start off and stay a deliberate choice.
|
||||
const silenceSubs = () => {
|
||||
for (const t of Array.from(v.textTracks)) t.mode = "disabled";
|
||||
// Subtitles follow the language chosen in Settings and nothing else.
|
||||
// WebKit will otherwise switch on whatever matches the system language,
|
||||
// which is the same unwanted auto-selection as a dubbed audio track.
|
||||
const applyPreference = () => {
|
||||
const tracks = Array.from(v.textTracks);
|
||||
const wanted =
|
||||
subLang === "off"
|
||||
? undefined
|
||||
: tracks.find((t) =>
|
||||
(t.language || "").toLowerCase().startsWith(subLang.toLowerCase()),
|
||||
);
|
||||
for (const t of tracks) t.mode = t === wanted ? "showing" : "disabled";
|
||||
read();
|
||||
};
|
||||
v.addEventListener("loadedmetadata", silenceSubs);
|
||||
applyPreference();
|
||||
v.addEventListener("loadedmetadata", applyPreference);
|
||||
// HLS subtitle renditions arrive after metadata, so re-apply as they land.
|
||||
v.textTracks.addEventListener?.("addtrack", applyPreference);
|
||||
|
||||
v.addEventListener("loadedmetadata", read);
|
||||
const at = (v as VideoWithTracks).audioTracks;
|
||||
@@ -101,14 +112,15 @@ export default function PlayerControls({ videoRef, stageRef, onActivity }: Props
|
||||
const id = setInterval(read, 1000);
|
||||
const stop = setTimeout(() => clearInterval(id), 8000);
|
||||
return () => {
|
||||
v.removeEventListener("loadedmetadata", silenceSubs);
|
||||
v.removeEventListener("loadedmetadata", applyPreference);
|
||||
v.textTracks.removeEventListener?.("addtrack", applyPreference);
|
||||
v.removeEventListener("loadedmetadata", read);
|
||||
at?.removeEventListener?.("addtrack", read);
|
||||
v.textTracks.removeEventListener?.("addtrack", read);
|
||||
clearInterval(id);
|
||||
clearTimeout(stop);
|
||||
};
|
||||
}, [videoRef]);
|
||||
}, [videoRef, subLang]);
|
||||
|
||||
// Close the menu on any outside click.
|
||||
useEffect(() => {
|
||||
|
||||
Reference in New Issue
Block a user