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.
183 lines
5.1 KiB
TypeScript
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;
|
|
}
|