feat: subtitle size, face and placement, set from the player

One fixed size cannot suit everyone — the default was too big, then
too small. The subtitle menu in the transport bar now carries an
Appearance section: Size (S/M/L/XL), Font (Sans/Serif/Mono) and Place
(Bottom/Raised/Top). Choices apply at once and are remembered across
videos and launches, like every other preference.

Size and face are written into a style element rather than the
stylesheet: ::cue takes no custom properties, so the values have to be
literal.

Placement is not a CSS property at all — WebVTT carries position on the
cue — so it is rewritten into the cues themselves and the track re-cut.
Every source is text now, including the sidecars beside older
downloads, so all three can be placed the same way.

The default lands at 85%, between WebKit's own size and the 58% that
replaced it.

Re-cutting a track exposed a bug in the guard that applies the
subtitle preference: it keyed on how many text tracks there were, and
re-cutting one leaves that unchanged, so the fresh track would have
stayed disabled and the subtitles would have vanished on any change of
Place. It keys on the tracks themselves.
This commit is contained in:
vincent
2026-08-29 17:45:19 +02:00
parent 8dade59bb4
commit ca93cc65ca
6 changed files with 175 additions and 25 deletions
+33
View File
@@ -113,6 +113,39 @@ export const STREAM_QUALITIES: Array<{ value: Quality; label: string }> = [
{ value: "480", label: "480p" },
];
/** How subtitles look, set from the player's subtitle menu. */
export interface SubStyle {
/** Percentage of the player's default cue size. */
size: number;
font: "sans" | "serif" | "mono";
place: "bottom" | "raised" | "top";
}
export const DEFAULT_SUB_STYLE: SubStyle = { size: 85, font: "sans", place: "bottom" };
export const SUB_SIZES: Array<{ value: number; label: string }> = [
{ value: 65, label: "S" },
{ value: 85, label: "M" },
{ value: 110, label: "L" },
{ value: 145, label: "XL" },
];
export const SUB_FONTS: Array<{ value: SubStyle["font"]; label: string; stack: string }> = [
{ value: "sans", label: "Sans", stack: "system-ui, -apple-system, Helvetica, sans-serif" },
{ value: "serif", label: "Serif", stack: "Georgia, 'Times New Roman', serif" },
{ value: "mono", label: "Mono", stack: "ui-monospace, SFMono-Regular, Menlo, monospace" },
];
/**
* Where the cues sit, as a WebVTT `line` percentage down the picture.
* Null leaves the player's own placement, which is the bottom.
*/
export const SUB_PLACES: Array<{ value: SubStyle["place"]; label: string; line: number | null }> = [
{ value: "bottom", label: "Bottom", line: null },
{ value: "raised", label: "Raised", line: 78 },
{ value: "top", label: "Top", line: 8 },
];
/**
* How many videos one "Download all" may queue. All subscriptions can be
* hundreds of videos, which is not something to set going by accident.