fa78b13cac
Adds theme presets (Toffee/Slate/Evergreen/Lavender) with a "follow system" mode and high-contrast awareness, a first-run welcome screen, and the Windows analog of Android's share sheet for a Win32 app: an aerofetch:// protocol handler plus an Explorer "Send to AeroFetch" entry, both routed through a single-instance lock so a second launch hands its link to the already-running window instead of opening a duplicate. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
406 lines
14 KiB
TypeScript
406 lines
14 KiB
TypeScript
/**
|
|
* Shared contract between main and renderer.
|
|
* IPC channel names + payload types live here so both sides stay in sync.
|
|
*/
|
|
|
|
export const IpcChannels = {
|
|
ytdlpVersion: 'ytdlp:version',
|
|
probe: 'media:probe',
|
|
downloadStart: 'download:start',
|
|
downloadCancel: 'download:cancel',
|
|
defaultFolder: 'download:default-folder',
|
|
chooseFolder: 'download:choose-folder',
|
|
openPath: 'shell:open-path',
|
|
showInFolder: 'shell:show-in-folder',
|
|
clipboardRead: 'clipboard:read',
|
|
settingsGet: 'settings:get',
|
|
settingsSet: 'settings:set',
|
|
historyList: 'history:list',
|
|
historyAdd: 'history:add',
|
|
historyRemove: 'history:remove',
|
|
historyRemoveMany: 'history:remove-many',
|
|
historyClear: 'history:clear',
|
|
/** main → renderer push channel for live download events */
|
|
downloadEvent: 'download:event',
|
|
cookiesLogin: 'cookies:login',
|
|
cookiesStatus: 'cookies:status',
|
|
cookiesClear: 'cookies:clear',
|
|
templatesList: 'templates:list',
|
|
templatesSave: 'templates:save',
|
|
templatesRemove: 'templates:remove',
|
|
commandPreview: 'command:preview',
|
|
ytdlpUpdate: 'ytdlp:update',
|
|
errorLogList: 'errorlog:list',
|
|
errorLogClear: 'errorlog:clear',
|
|
backupExport: 'backup:export',
|
|
backupImport: 'backup:import',
|
|
systemThemeGet: 'system-theme:get',
|
|
/** main → renderer push channel for OS theme/contrast changes (nativeTheme 'updated') */
|
|
systemThemeUpdate: 'system-theme:update',
|
|
openHighContrastSettings: 'shell:open-high-contrast-settings',
|
|
/** main → renderer push channel: a URL handed to AeroFetch from outside the app
|
|
* (the aerofetch:// protocol, or a .url file via Explorer's "Send to" menu) */
|
|
externalUrl: 'external-url'
|
|
} as const
|
|
|
|
/** Sentinel format id meaning "let yt-dlp pick the best video+audio". */
|
|
export const BEST_FORMAT_ID = '__best__'
|
|
|
|
export type MediaKind = 'video' | 'audio'
|
|
|
|
// --- Post-processing / format options ---------------------------------------
|
|
|
|
/** Audio extraction target. 'best' keeps the source codec without re-encoding. */
|
|
export type AudioFormat = 'best' | 'mp3' | 'm4a' | 'opus' | 'flac' | 'wav' | 'aac'
|
|
export const AUDIO_FORMATS: AudioFormat[] = ['best', 'mp3', 'm4a', 'opus', 'flac', 'wav', 'aac']
|
|
|
|
/** Container the merged video is muxed into (`--merge-output-format`). */
|
|
export type VideoContainer = 'mp4' | 'mkv' | 'webm'
|
|
export const VIDEO_CONTAINERS: VideoContainer[] = ['mp4', 'mkv', 'webm']
|
|
|
|
/** Preferred video codec, applied as a `-S vcodec:…` sort tiebreaker. */
|
|
export type VideoCodecPref = 'any' | 'h264' | 'vp9' | 'av1'
|
|
export const VIDEO_CODECS: VideoCodecPref[] = ['any', 'h264', 'vp9', 'av1']
|
|
|
|
/** Whether SponsorBlock cuts the segments out or just marks them as chapters. */
|
|
export type SponsorBlockMode = 'remove' | 'mark'
|
|
|
|
/**
|
|
* Where yt-dlp's auth cookies come from. 'browser' reads a logged-in browser's
|
|
* cookie store directly; 'login' uses a cookie file exported from AeroFetch's
|
|
* own built-in sign-in window (see src/main/cookies.ts).
|
|
*/
|
|
export type CookieSource = 'none' | 'browser' | 'login'
|
|
|
|
/** Browsers yt-dlp's --cookies-from-browser supports that ship on Windows. */
|
|
export const COOKIE_BROWSERS = ['chrome', 'edge', 'firefox', 'brave', 'opera', 'vivaldi'] as const
|
|
export type CookieBrowser = (typeof COOKIE_BROWSERS)[number]
|
|
|
|
/** Accent color presets for the brand ramp (see src/renderer/src/theme.ts). */
|
|
export const ACCENT_COLORS = ['toffee', 'slate', 'evergreen', 'lavender'] as const
|
|
export type AccentColor = (typeof ACCENT_COLORS)[number]
|
|
|
|
/** UI color theme: an explicit mode, or 'system' to follow the OS preference. */
|
|
export type ThemeMode = 'light' | 'dark' | 'system'
|
|
|
|
/** OS-level theme signal, read from Electron's nativeTheme in the main process. */
|
|
export interface SystemThemeInfo {
|
|
/** whether Windows is currently in dark mode */
|
|
shouldUseDarkColors: boolean
|
|
/** whether a Windows high-contrast theme is active */
|
|
shouldUseHighContrastColors: boolean
|
|
}
|
|
|
|
/** SponsorBlock segment categories (subset of yt-dlp's, the ones Seal exposes). */
|
|
export const SPONSORBLOCK_CATEGORIES = [
|
|
'sponsor',
|
|
'intro',
|
|
'outro',
|
|
'selfpromo',
|
|
'preview',
|
|
'filler',
|
|
'interaction',
|
|
'music_offtopic'
|
|
] as const
|
|
export type SponsorBlockCategory = (typeof SPONSORBLOCK_CATEGORIES)[number]
|
|
|
|
/**
|
|
* Post-processing / format choices applied to a download. The persisted
|
|
* defaults live in `Settings.downloadOptions`; an individual download may
|
|
* override them via `StartDownloadOptions.options`.
|
|
*/
|
|
export interface DownloadOptions {
|
|
/** audio extraction target (audio downloads only) */
|
|
audioFormat: AudioFormat
|
|
/** container for merged video downloads */
|
|
videoContainer: VideoContainer
|
|
/** preferred video codec (sort tiebreaker, not a hard filter) */
|
|
preferredVideoCodec: VideoCodecPref
|
|
/** download + embed subtitles into video */
|
|
embedSubtitles: boolean
|
|
/** comma-separated yt-dlp sub-lang selectors, e.g. 'en' or 'en.*,es' */
|
|
subtitleLanguages: string
|
|
/** also pull auto-generated (ASR) captions */
|
|
autoSubtitles: boolean
|
|
/** enable SponsorBlock */
|
|
sponsorBlock: boolean
|
|
sponsorBlockMode: SponsorBlockMode
|
|
sponsorBlockCategories: SponsorBlockCategory[]
|
|
/** embed chapter markers */
|
|
embedChapters: boolean
|
|
/** embed metadata (title/artist/etc.) */
|
|
embedMetadata: boolean
|
|
/** embed the thumbnail (cover art for audio, poster for mp4/mkv video) */
|
|
embedThumbnail: boolean
|
|
/** crop embedded audio artwork to a centred square (Seal's "crop artwork") */
|
|
cropThumbnail: boolean
|
|
}
|
|
|
|
export const DEFAULT_DOWNLOAD_OPTIONS: DownloadOptions = {
|
|
audioFormat: 'mp3',
|
|
videoContainer: 'mp4',
|
|
preferredVideoCodec: 'any',
|
|
embedSubtitles: false,
|
|
subtitleLanguages: 'en',
|
|
autoSubtitles: false,
|
|
sponsorBlock: false,
|
|
sponsorBlockMode: 'remove',
|
|
sponsorBlockCategories: ['sponsor'],
|
|
embedChapters: false,
|
|
embedMetadata: true,
|
|
embedThumbnail: true,
|
|
cropThumbnail: false
|
|
}
|
|
|
|
export interface YtdlpVersionResult {
|
|
ok: boolean
|
|
/** yt-dlp version string, present when ok is true */
|
|
version?: string
|
|
/** human-readable error, present when ok is false */
|
|
error?: string
|
|
}
|
|
|
|
/** yt-dlp's self-update release channel (`--update-to <channel>`). */
|
|
export type YtdlpUpdateChannel = 'stable' | 'nightly'
|
|
|
|
export interface YtdlpUpdateResult {
|
|
ok: boolean
|
|
/** yt-dlp's own update output, e.g. "Updated to ... " or "yt-dlp is up to date" */
|
|
output?: string
|
|
error?: string
|
|
}
|
|
|
|
/** A single selectable output format, derived from `yt-dlp -J`. */
|
|
export interface FormatOption {
|
|
/** yt-dlp format_id, or BEST_FORMAT_ID for the auto-best option */
|
|
id: string
|
|
/** human label, e.g. '1080p60 · mp4 · 124 MB' */
|
|
label: string
|
|
height?: number
|
|
ext?: string
|
|
filesizeLabel?: string
|
|
/** true when this format already carries an audio track */
|
|
hasAudio: boolean
|
|
}
|
|
|
|
/** Metadata + available formats returned by a probe. */
|
|
export interface MediaInfo {
|
|
title: string
|
|
channel?: string
|
|
durationLabel?: string
|
|
thumbnail?: string
|
|
/** video format options, best-first (audio is transcoded, so not listed) */
|
|
formats: FormatOption[]
|
|
}
|
|
|
|
/** One entry of a probed playlist (flat — no per-entry format list). */
|
|
export interface PlaylistEntry {
|
|
/** 1-based position in the playlist */
|
|
index: number
|
|
id: string
|
|
title: string
|
|
/** canonical URL for the entry, enqueued as its own single-video download */
|
|
url: string
|
|
durationLabel?: string
|
|
uploader?: string
|
|
}
|
|
|
|
/** A probed playlist: lightweight metadata for each entry. */
|
|
export interface PlaylistInfo {
|
|
title: string
|
|
uploader?: string
|
|
count: number
|
|
entries: PlaylistEntry[]
|
|
}
|
|
|
|
/**
|
|
* A single probe can resolve to either one video (with formats) or a playlist
|
|
* (with entries). `kind` discriminates which field is populated.
|
|
*/
|
|
export interface ProbeResult {
|
|
ok: boolean
|
|
kind?: 'video' | 'playlist'
|
|
info?: MediaInfo
|
|
playlist?: PlaylistInfo
|
|
error?: string
|
|
}
|
|
|
|
/** Sent renderer → main to kick off a download. The renderer owns the id. */
|
|
export interface StartDownloadOptions {
|
|
id: string
|
|
url: string
|
|
kind: MediaKind
|
|
/** one of the UI quality labels (e.g. '1080p', 'Best (MP3)') */
|
|
quality: string
|
|
/** absolute output directory; main falls back to the OS Downloads folder */
|
|
outputDir?: string
|
|
/** exact yt-dlp format_id chosen from a probe; omit for the preset path */
|
|
formatId?: string
|
|
/** whether the chosen format already includes audio (skips +bestaudio) */
|
|
formatHasAudio?: boolean
|
|
/** per-download post-processing override; main falls back to settings defaults */
|
|
options?: DownloadOptions
|
|
/**
|
|
* Per-download custom-command override: raw extra yt-dlp flags appended to
|
|
* the generated argv (see CommandTemplate). Omit to fall back to the
|
|
* persisted settings default (customCommandEnabled + defaultTemplateId);
|
|
* pass an empty string to explicitly run with no extra args for just this
|
|
* download even when the settings default is enabled.
|
|
*/
|
|
extraArgs?: string
|
|
}
|
|
|
|
export interface StartDownloadResult {
|
|
ok: boolean
|
|
error?: string
|
|
}
|
|
|
|
export interface DownloadMeta {
|
|
title?: string
|
|
channel?: string
|
|
durationLabel?: string
|
|
}
|
|
|
|
export interface DownloadProgress {
|
|
/** yt-dlp progress status: 'downloading' | 'finished' | … */
|
|
status: string
|
|
/** 0..1 (0 when total size is unknown) */
|
|
progress: number
|
|
/** human-readable, e.g. '4.7 MB/s' */
|
|
speed?: string
|
|
/** human-readable, e.g. '1:12' */
|
|
eta?: string
|
|
/** human-readable total size, e.g. '512 MB' */
|
|
sizeLabel?: string
|
|
}
|
|
|
|
/** Discriminated union pushed on IpcChannels.downloadEvent. */
|
|
export type DownloadEvent =
|
|
| { type: 'meta'; id: string; meta: DownloadMeta }
|
|
| { type: 'progress'; id: string; progress: DownloadProgress }
|
|
| { type: 'done'; id: string; filePath?: string }
|
|
| { type: 'error'; id: string; error: string }
|
|
|
|
/** Persisted user settings (electron-store). */
|
|
export interface Settings {
|
|
/** absolute output directory (empty string resolves to the OS Downloads folder) */
|
|
outputDir: string
|
|
defaultKind: MediaKind
|
|
defaultVideoQuality: string
|
|
defaultAudioQuality: string
|
|
/** how many downloads run at once */
|
|
maxConcurrent: number
|
|
/** yt-dlp output template, e.g. '%(title)s.%(ext)s' */
|
|
filenameTemplate: string
|
|
/** UI color theme: explicit light/dark, or 'system' to follow the OS */
|
|
theme: ThemeMode
|
|
/** brand accent preset (buttons, links, selected nav item) */
|
|
accentColor: AccentColor
|
|
/** auto-detect video links from the clipboard on window focus */
|
|
clipboardWatch: boolean
|
|
/** default post-processing / format options for new downloads */
|
|
downloadOptions: DownloadOptions
|
|
/** yt-dlp --proxy value, e.g. 'socks5://127.0.0.1:1080'. Empty disables it. */
|
|
proxy: string
|
|
/** yt-dlp --limit-rate value, e.g. '2M' or '500K'. Empty means unlimited. */
|
|
rateLimit: string
|
|
/** use bundled aria2c (resources/bin/aria2c.exe) as the external downloader */
|
|
useAria2c: boolean
|
|
/** where auth cookies come from: none, a browser's store, or the sign-in window */
|
|
cookieSource: CookieSource
|
|
/** which browser to read cookies from when cookieSource is 'browser' */
|
|
cookiesBrowser: CookieBrowser
|
|
/** sanitize output filenames to a portable ASCII-only subset (--restrict-filenames) */
|
|
restrictFilenames: boolean
|
|
/** record completed downloads in a yt-dlp --download-archive file to skip repeats */
|
|
downloadArchive: boolean
|
|
/** apply a named custom-command template's extra args to every new download */
|
|
customCommandEnabled: boolean
|
|
/** id of the CommandTemplate applied when customCommandEnabled is on; null = none picked yet */
|
|
defaultTemplateId: string | null
|
|
/** show a native OS notification when a download finishes or fails */
|
|
notifyOnComplete: boolean
|
|
/** false until the first-run welcome screen has been dismissed */
|
|
hasCompletedOnboarding: boolean
|
|
}
|
|
|
|
/**
|
|
* A named, reusable set of extra yt-dlp CLI flags (Phase C "custom commands"),
|
|
* e.g. name: 'Write thumbnail + no mtime', args: '--write-thumbnail --no-mtime'.
|
|
* Shell-split (see parseExtraArgs) and appended to the generated argv just
|
|
* before the `--` URL terminator, so they can override earlier flags.
|
|
*/
|
|
export interface CommandTemplate {
|
|
id: string
|
|
name: string
|
|
args: string
|
|
}
|
|
|
|
/** Result of building the exact yt-dlp command line for the current form state, without running it. */
|
|
export interface CommandPreviewResult {
|
|
ok: boolean
|
|
/** full command line (binary + quoted args), for display/copy only */
|
|
command?: string
|
|
error?: string
|
|
}
|
|
|
|
/** Whether the built-in sign-in window has ever saved a cookie file, and when. */
|
|
export interface CookiesStatus {
|
|
exists: boolean
|
|
/** epoch milliseconds the cookie file was last written */
|
|
savedAt?: number
|
|
}
|
|
|
|
/** Result of a built-in sign-in window session, resolved once the window closes. */
|
|
export interface CookiesLoginResult {
|
|
ok: boolean
|
|
/** number of cookies captured into the exported file */
|
|
cookieCount?: number
|
|
error?: string
|
|
}
|
|
|
|
/** A completed download, persisted to history.json (newest-first). */
|
|
export interface HistoryEntry {
|
|
id: string
|
|
title: string
|
|
channel?: string
|
|
url: string
|
|
kind: MediaKind
|
|
quality: string
|
|
filePath?: string
|
|
sizeLabel?: string
|
|
thumbnail?: string
|
|
/** epoch milliseconds */
|
|
completedAt: number
|
|
}
|
|
|
|
/**
|
|
* A failed download or download-start attempt, persisted to errorlog.json
|
|
* (newest-first) so the report survives the queue item being cleared —
|
|
* Seal's "debug report" equivalent.
|
|
*/
|
|
export interface ErrorLogEntry {
|
|
id: string
|
|
/** resolved video title when known; falls back to the URL in the UI */
|
|
title?: string
|
|
url: string
|
|
kind: MediaKind
|
|
error: string
|
|
/** epoch milliseconds */
|
|
occurredAt: number
|
|
}
|
|
|
|
/** Result of exporting settings + custom-command templates to a JSON file. */
|
|
export interface BackupExportResult {
|
|
ok: boolean
|
|
/** absolute path written to, present when ok is true */
|
|
path?: string
|
|
error?: string
|
|
}
|
|
|
|
/** Result of restoring settings + custom-command templates from a JSON file. */
|
|
export interface BackupImportResult {
|
|
ok: boolean
|
|
error?: string
|
|
}
|