feat: durations, clean subtitles, icon buttons

Subtitles rendered pinned to the left edge and clipped, in half-grey
karaoke text: YouTube's auto-captions carry align:start position:0% on
every cue plus inline <00:00:12.480><c>word</c> timing tags. Both are
now stripped after download, and existing downloads are tidied the first
time they are listed.

Thumbnails show video length. The Atom feed carries no duration, so it
is read from the watch page and cached — but only a trickle. The first
version fetched 24 pages every 12 seconds at six concurrent, roughly two
requests a second sustained, and YouTube answered by challenging the
whole IP: 'Sign in to confirm you are not a bot', which broke streaming
and downloads too. It is now four pages every five minutes, one at a
time, and stops for the session the moment a batch is refused.

A subtitle chosen in the player becomes the stored preference, so the
next video matches without going back to Settings.

The back and download buttons are square icon buttons; the back glyph
was off-centre because px-2 beat the p-0 meant to clear it.
This commit is contained in:
vincent
2026-08-29 14:07:17 +02:00
parent e080715f3b
commit 7aae080e46
10 changed files with 350 additions and 14 deletions
+8 -1
View File
@@ -36,6 +36,8 @@ interface Props {
onActivity?: () => void;
/** Preferred subtitle language, or "off" to start with none. */
subLang: string;
/** Persists a choice made here, so the next video matches. */
onSubLang: (l: string) => void;
}
const SKIP_S = 10;
@@ -61,7 +63,9 @@ 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, subLang }: Props) {
export default function PlayerControls({
videoRef, stageRef, onActivity, subLang, onSubLang,
}: Props) {
const [playing, setPlaying] = useState(false);
const [time, setTime] = useState(0);
const [duration, setDuration] = useState(0);
@@ -148,6 +152,9 @@ export default function PlayerControls({ videoRef, stageRef, onActivity, subLang
for (const t of Array.from(v.textTracks)) {
t.mode = t === track ? "showing" : "disabled";
}
// The choice becomes the preference, so the next video matches without
// going back to Settings.
onSubLang(track ? (track.language || "off").split("-")[0] : "off");
bump((n) => n + 1);
onActivity?.();
};