Files
FlightTube/src/types.ts
T
vincent ca93cc65ca 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.
2026-08-29 17:45:19 +02:00

183 lines
5.1 KiB
TypeScript

// Mirrors of the Rust structs in src-tauri/src/models.rs. Field names match
// serde's output exactly so no mapping layer is needed.
export type DownloadState =
| "queued"
| "running"
| "done"
| "failed"
| "cancelled";
export interface ChannelWithCount {
id: string;
title: string;
url: string;
video_count: number;
downloaded_count: number;
/** Why this channel's last refresh failed, if it did. */
last_error: string | null;
}
export interface FeedItem {
id: string;
channel_id: string;
channel_title: string;
title: string;
description: string;
/** Unix seconds. */
published: number;
thumb_url: string;
thumb_path: string | null;
views: number;
is_short: boolean;
state: DownloadState | null;
path: string | null;
pct: number | null;
error: string | null;
/** Seconds watched, and the video's length — drives the feed progress bar. */
position: number | null;
duration: number | null;
}
export interface FeedFilter {
channel_id?: string | null;
search?: string | null;
downloaded_only: boolean;
hide_shorts: boolean;
limit?: number | null;
}
export interface Prereqs {
yt_dlp: string | null;
ffmpeg: string | null;
library_path: string;
}
export interface RefreshSummary {
channels: number;
new_videos: number;
failures: string[];
}
export interface RefreshProgress {
done: number;
total: number;
channel: string;
}
export interface DownloadProgressEvent {
video_id: string;
pct: number | null;
bytes_done: number;
bytes_total: number | null;
speed: number | null;
eta: number | null;
}
export interface DownloadStateEvent {
video_id: string;
state: DownloadState;
error: string | null;
path: string | null;
}
export interface ImportPreview {
incoming: number;
removed_channels: number;
removed_videos: number;
removed_downloads: number;
}
/** "best" or a maximum height in pixels. */
export type Quality = "best" | "2160" | "1440" | "1080" | "720" | "480";
export interface Stream {
url: string | null;
playlist: string | null;
}
export const QUALITIES: Array<{ value: Quality; label: string }> = [
{ value: "best", label: "Best available (up to 4K)" },
{ value: "2160", label: "2160p — 4K" },
{ value: "1440", label: "1440p — 2K" },
{ value: "1080", label: "1080p" },
{ value: "720", label: "720p" },
{ value: "480", label: "480p" },
];
/** Streaming tops out at 1080p — YouTube's HLS carries nothing higher. */
export const STREAM_QUALITIES: Array<{ value: Quality; label: string }> = [
{ value: "best", label: "Best available (adaptive)" },
{ value: "1080", label: "1080p" },
{ value: "720", label: "720p" },
{ 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.
*/
export const BULK_LIMITS: Array<{ value: number; label: string }> = [
{ value: 5, label: "5 videos" },
{ value: 10, label: "10 videos" },
{ value: 25, label: "25 videos" },
{ value: 50, label: "50 videos" },
{ value: 100, label: "100 videos" },
{ value: 0, label: "No limit" },
];
export const DEFAULT_BULK_LIMIT = 25;
/** Preferred subtitle language: shown when available, and downloaded. */
/** On by default: "None" means no captions anywhere, which is rarely wanted. */
export const DEFAULT_SUB_LANG = "en";
export const SUB_LANGS: Array<{ value: string; label: string }> = [
{ value: "off", label: "Off — pick per video" },
{ value: "en", label: "English" },
{ value: "nl", label: "Nederlands" },
{ value: "de", label: "Deutsch" },
{ value: "fr", label: "Français" },
{ value: "es", label: "Español" },
{ value: "it", label: "Italiano" },
{ value: "pt", label: "Português" },
];
export interface UpdateStatus {
current: string | null;
latest: string | null;
up_to_date: boolean;
}