3df32d0505
L22: DownloadOptionsForm accepts optional `kind` prop. When set, hides
audio-specific fields (Audio format) for video downloads and vice versa
(Video container, Preferred codec) for audio downloads. Settings passes
defaultKind so the defaults card shows only relevant options.
L24: Replace lone window.open('htmlUrl', '_blank') with a new openUrl IPC
channel (shell:open-url) that calls shell.openExternal after validating
the protocol is http/https. Consistent with how all other external opens
work in the app. Preload + main handler + mock updated.
L27: DownloadBar URL-field Enter is now smart -- if formats/playlist are
already loaded (or probe errored), Enter triggers download; otherwise
it probes. Eliminates the keyboard dead-end where Enter never downloads.
typecheck + 242 tests + eslint green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
266 lines
12 KiB
TypeScript
266 lines
12 KiB
TypeScript
import { contextBridge, ipcRenderer, type IpcRendererEvent } from 'electron'
|
|
import {
|
|
IpcChannels,
|
|
type AppUpdateInfo,
|
|
type AppUpdateDownload,
|
|
type AppUpdateProgress,
|
|
type YtdlpVersionResult,
|
|
type YtdlpUpdateChannel,
|
|
type YtdlpUpdateResult,
|
|
type YtdlpAutoUpdateStatus,
|
|
type FfmpegVersionResult,
|
|
type ProbeResult,
|
|
type StartDownloadOptions,
|
|
type StartDownloadResult,
|
|
type DownloadEvent,
|
|
type Settings,
|
|
type HistoryEntry,
|
|
type CookiesStatus,
|
|
type CookiesLoginResult,
|
|
type CommandTemplate,
|
|
type CommandPreviewResult,
|
|
type ErrorLogEntry,
|
|
type BackupExportResult,
|
|
type BackupImportResult,
|
|
type SystemThemeInfo,
|
|
type Source,
|
|
type MediaItem,
|
|
type IndexProgress,
|
|
type IndexSourceResult,
|
|
type SyncResult,
|
|
type ScheduledSyncStatus,
|
|
type TerminalEvent,
|
|
type TerminalRunResult,
|
|
type TaskbarProgress
|
|
} from '@shared/ipc'
|
|
|
|
// The surface exposed to the renderer. Keep this thin: it only forwards to IPC.
|
|
const api = {
|
|
/** AeroFetch's own version string (e.g. '0.3.1'). */
|
|
getAppVersion: (): Promise<string> => ipcRenderer.invoke(IpcChannels.appVersion),
|
|
|
|
/** Check the configured Gitea repo for a newer AeroFetch release. */
|
|
checkForAppUpdate: (): Promise<AppUpdateInfo> => ipcRenderer.invoke(IpcChannels.appUpdateCheck),
|
|
|
|
/** Download the latest release's installer to a temp file. */
|
|
downloadAppUpdate: (url: string): Promise<AppUpdateDownload> =>
|
|
ipcRenderer.invoke(IpcChannels.appUpdateDownload, url),
|
|
|
|
/** Launch a downloaded installer and quit so it can replace the app. */
|
|
runAppUpdate: (filePath: string): Promise<{ ok: boolean; error?: string }> =>
|
|
ipcRenderer.invoke(IpcChannels.appUpdateRun, filePath),
|
|
|
|
/** Subscribe to installer download progress. Returns an unsubscribe function. */
|
|
onAppUpdateProgress: (cb: (p: AppUpdateProgress) => void): (() => void) => {
|
|
const listener = (_e: IpcRendererEvent, p: AppUpdateProgress): void => cb(p)
|
|
ipcRenderer.on(IpcChannels.appUpdateProgress, listener)
|
|
return () => ipcRenderer.removeListener(IpcChannels.appUpdateProgress, listener)
|
|
},
|
|
|
|
getYtdlpVersion: (): Promise<YtdlpVersionResult> => ipcRenderer.invoke(IpcChannels.ytdlpVersion),
|
|
|
|
/** Versions of the bundled ffmpeg + ffprobe (display-only; not auto-updated). */
|
|
getFfmpegVersions: (): Promise<FfmpegVersionResult> =>
|
|
ipcRenderer.invoke(IpcChannels.ffmpegVersion),
|
|
|
|
probe: (url: string): Promise<ProbeResult> => ipcRenderer.invoke(IpcChannels.probe, url),
|
|
|
|
startDownload: (opts: StartDownloadOptions): Promise<StartDownloadResult> =>
|
|
ipcRenderer.invoke(IpcChannels.downloadStart, opts),
|
|
|
|
cancelDownload: (id: string): Promise<void> => ipcRenderer.invoke(IpcChannels.downloadCancel, id),
|
|
|
|
pauseDownload: (id: string): Promise<void> => ipcRenderer.invoke(IpcChannels.downloadPause, id),
|
|
|
|
chooseFolder: (current?: string): Promise<string | null> =>
|
|
ipcRenderer.invoke(IpcChannels.chooseFolder, current),
|
|
|
|
openPath: (path: string): Promise<string> => ipcRenderer.invoke(IpcChannels.openPath, path),
|
|
|
|
openUrl: (url: string): Promise<void> => ipcRenderer.invoke(IpcChannels.openUrl, url),
|
|
|
|
showInFolder: (path: string): Promise<void> => ipcRenderer.invoke(IpcChannels.showInFolder, path),
|
|
|
|
readClipboard: (): Promise<string> => ipcRenderer.invoke(IpcChannels.clipboardRead),
|
|
|
|
getSettings: (): Promise<Settings> => ipcRenderer.invoke(IpcChannels.settingsGet),
|
|
|
|
setSettings: (partial: Partial<Settings>): Promise<Settings> =>
|
|
ipcRenderer.invoke(IpcChannels.settingsSet, partial),
|
|
|
|
listHistory: (): Promise<HistoryEntry[]> => ipcRenderer.invoke(IpcChannels.historyList),
|
|
|
|
addHistory: (entry: HistoryEntry): Promise<HistoryEntry[]> =>
|
|
ipcRenderer.invoke(IpcChannels.historyAdd, entry),
|
|
|
|
removeHistory: (id: string): Promise<HistoryEntry[]> =>
|
|
ipcRenderer.invoke(IpcChannels.historyRemove, id),
|
|
|
|
removeManyHistory: (ids: string[]): Promise<HistoryEntry[]> =>
|
|
ipcRenderer.invoke(IpcChannels.historyRemoveMany, ids),
|
|
|
|
clearHistory: (): Promise<HistoryEntry[]> => ipcRenderer.invoke(IpcChannels.historyClear),
|
|
|
|
/** Open the built-in sign-in window; resolves once the user closes it. */
|
|
cookiesLogin: (url: string): Promise<CookiesLoginResult> =>
|
|
ipcRenderer.invoke(IpcChannels.cookiesLogin, url),
|
|
|
|
cookiesStatus: (): Promise<CookiesStatus> => ipcRenderer.invoke(IpcChannels.cookiesStatus),
|
|
|
|
cookiesClear: (): Promise<void> => ipcRenderer.invoke(IpcChannels.cookiesClear),
|
|
|
|
listTemplates: (): Promise<CommandTemplate[]> => ipcRenderer.invoke(IpcChannels.templatesList),
|
|
|
|
saveTemplate: (template: CommandTemplate): Promise<CommandTemplate[]> =>
|
|
ipcRenderer.invoke(IpcChannels.templatesSave, template),
|
|
|
|
removeTemplate: (id: string): Promise<CommandTemplate[]> =>
|
|
ipcRenderer.invoke(IpcChannels.templatesRemove, id),
|
|
|
|
/** Build the exact yt-dlp command line for the given form state, without running it. */
|
|
previewCommand: (opts: StartDownloadOptions): Promise<CommandPreviewResult> =>
|
|
ipcRenderer.invoke(IpcChannels.commandPreview, opts),
|
|
|
|
updateYtdlp: (channel: YtdlpUpdateChannel): Promise<YtdlpUpdateResult> =>
|
|
ipcRenderer.invoke(IpcChannels.ytdlpUpdate, channel),
|
|
|
|
/** Subscribe to background yt-dlp auto-update status. Returns an unsubscribe function. */
|
|
onYtdlpAutoUpdateStatus: (cb: (s: YtdlpAutoUpdateStatus) => void): (() => void) => {
|
|
const listener = (_e: IpcRendererEvent, s: YtdlpAutoUpdateStatus): void => cb(s)
|
|
ipcRenderer.on(IpcChannels.ytdlpAutoUpdateStatus, listener)
|
|
return () => ipcRenderer.removeListener(IpcChannels.ytdlpAutoUpdateStatus, listener)
|
|
},
|
|
|
|
listErrorLog: (): Promise<ErrorLogEntry[]> => ipcRenderer.invoke(IpcChannels.errorLogList),
|
|
|
|
clearErrorLog: (): Promise<ErrorLogEntry[]> => ipcRenderer.invoke(IpcChannels.errorLogClear),
|
|
|
|
/** Opens a save dialog and writes settings + templates to the chosen JSON file. */
|
|
exportBackup: (): Promise<BackupExportResult> => ipcRenderer.invoke(IpcChannels.backupExport),
|
|
|
|
/** Opens an open dialog and restores settings + templates from the chosen JSON file. */
|
|
importBackup: (): Promise<BackupImportResult> => ipcRenderer.invoke(IpcChannels.backupImport),
|
|
|
|
/** Subscribe to live download events. Returns an unsubscribe function. */
|
|
onDownloadEvent: (cb: (ev: DownloadEvent) => void): (() => void) => {
|
|
const listener = (_e: IpcRendererEvent, ev: DownloadEvent): void => cb(ev)
|
|
ipcRenderer.on(IpcChannels.downloadEvent, listener)
|
|
return () => ipcRenderer.removeListener(IpcChannels.downloadEvent, listener)
|
|
},
|
|
|
|
getSystemTheme: (): Promise<SystemThemeInfo> => ipcRenderer.invoke(IpcChannels.systemThemeGet),
|
|
|
|
/** Subscribe to OS theme/contrast changes. Returns an unsubscribe function. */
|
|
onSystemThemeUpdate: (cb: (info: SystemThemeInfo) => void): (() => void) => {
|
|
const listener = (_e: IpcRendererEvent, info: SystemThemeInfo): void => cb(info)
|
|
ipcRenderer.on(IpcChannels.systemThemeUpdate, listener)
|
|
return () => ipcRenderer.removeListener(IpcChannels.systemThemeUpdate, listener)
|
|
},
|
|
|
|
/** Opens Windows' "Contrast themes" accessibility settings page. */
|
|
openHighContrastSettings: (): Promise<void> =>
|
|
ipcRenderer.invoke(IpcChannels.openHighContrastSettings),
|
|
|
|
/** Subscribe to links handed to AeroFetch from outside (aerofetch:// or a
|
|
* "Send to" .url file). Returns an unsubscribe function. */
|
|
onExternalUrl: (cb: (url: string) => void): (() => void) => {
|
|
const listener = (_e: IpcRendererEvent, url: string): void => cb(url)
|
|
ipcRenderer.on(IpcChannels.externalUrl, listener)
|
|
return () => ipcRenderer.removeListener(IpcChannels.externalUrl, listener)
|
|
},
|
|
|
|
// --- Media-manager sources (Pinchflat-style; see ROADMAP-PINCHFLAT.md) ---
|
|
|
|
listSources: (): Promise<Source[]> => ipcRenderer.invoke(IpcChannels.sourcesList),
|
|
|
|
/** Index (or re-index) a channel/playlist URL into the persisted source list. */
|
|
indexSource: (url: string): Promise<IndexSourceResult> =>
|
|
ipcRenderer.invoke(IpcChannels.sourceIndex, url),
|
|
|
|
/** Re-index an existing source by id (refreshes its media-item list). */
|
|
reindexSource: (id: string): Promise<IndexSourceResult> =>
|
|
ipcRenderer.invoke(IpcChannels.sourceReindex, id),
|
|
|
|
removeSource: (id: string): Promise<Source[]> => ipcRenderer.invoke(IpcChannels.sourceRemove, id),
|
|
|
|
listSourceItems: (sourceId: string): Promise<MediaItem[]> =>
|
|
ipcRenderer.invoke(IpcChannels.sourceItems, sourceId),
|
|
|
|
/** Persist that a media item has finished downloading (drives incremental sync). */
|
|
markSourceItemDownloaded: (id: string, filePath?: string): Promise<MediaItem[]> =>
|
|
ipcRenderer.invoke(IpcChannels.sourceItemDownloaded, id, filePath),
|
|
|
|
/** Toggle whether a source is watched for new uploads. */
|
|
setSourceWatched: (id: string, watched: boolean): Promise<Source[]> =>
|
|
ipcRenderer.invoke(IpcChannels.sourceSetWatched, id, watched),
|
|
|
|
/** Re-index all watched sources; resolves with the videos found new. */
|
|
syncSources: (): Promise<SyncResult> => ipcRenderer.invoke(IpcChannels.sourcesSync),
|
|
|
|
/** Read / write the Windows daily-sync scheduled task. */
|
|
getScheduledSync: (): Promise<ScheduledSyncStatus> =>
|
|
ipcRenderer.invoke(IpcChannels.scheduledSyncGet),
|
|
setScheduledSync: (enabled: boolean): Promise<ScheduledSyncStatus> =>
|
|
ipcRenderer.invoke(IpcChannels.scheduledSyncSet, enabled),
|
|
|
|
/** Subscribe to live indexing progress. Returns an unsubscribe function. */
|
|
onIndexProgress: (cb: (p: IndexProgress) => void): (() => void) => {
|
|
const listener = (_e: IpcRendererEvent, p: IndexProgress): void => cb(p)
|
|
ipcRenderer.on(IpcChannels.indexProgress, listener)
|
|
return () => ipcRenderer.removeListener(IpcChannels.indexProgress, listener)
|
|
},
|
|
|
|
// --- Built-in yt-dlp terminal (Phase N) ---
|
|
|
|
/** Run the bundled yt-dlp with raw args; output streams via onTerminalOutput. */
|
|
runTerminal: (id: string, args: string): Promise<TerminalRunResult> =>
|
|
ipcRenderer.invoke(IpcChannels.terminalRun, id, args),
|
|
|
|
cancelTerminal: (id: string): Promise<void> => ipcRenderer.invoke(IpcChannels.terminalCancel, id),
|
|
|
|
/** Subscribe to live terminal output. Returns an unsubscribe function. */
|
|
onTerminalOutput: (cb: (ev: TerminalEvent) => void): (() => void) => {
|
|
const listener = (_e: IpcRendererEvent, ev: TerminalEvent): void => cb(ev)
|
|
ipcRenderer.on(IpcChannels.terminalOutput, listener)
|
|
return () => ipcRenderer.removeListener(IpcChannels.terminalOutput, listener)
|
|
},
|
|
|
|
/** Reflect overall queue progress on the Windows taskbar. */
|
|
setTaskbarProgress: (p: TaskbarProgress): Promise<void> =>
|
|
ipcRenderer.invoke(IpcChannels.taskbarProgress, p),
|
|
|
|
/** Open a YouTube WebView to automatically extract a PO token (Phase P). */
|
|
mintPoToken: (): Promise<string | null> =>
|
|
ipcRenderer.invoke(IpcChannels.youtubePoTokenMint) as Promise<string | null>
|
|
}
|
|
|
|
export type Api = typeof api
|
|
|
|
// A minimal presence marker. The renderer only checks `window.electron` for
|
|
// truthiness to tell the real app apart from the browser UI preview, so there's
|
|
// no reason to expose the full ipcRenderer/webFrame/process surface here.
|
|
const electron = { isElectron: true } as const
|
|
export type ElectronMarker = typeof electron
|
|
|
|
if (process.contextIsolated) {
|
|
try {
|
|
contextBridge.exposeInMainWorld('electron', electron)
|
|
contextBridge.exposeInMainWorld('api', api)
|
|
} catch (error) {
|
|
// M30: a broken bridge causes the renderer to silently run in preview/mock mode.
|
|
// Notify the main process so it can show a hard error dialog — without this the
|
|
// failure is completely invisible to the user.
|
|
console.error('[AeroFetch preload] contextBridge failed:', error)
|
|
try {
|
|
ipcRenderer.send(IpcChannels.preloadBridgeFailure, String(error))
|
|
} catch {
|
|
/* if IPC itself is broken there is nothing more we can do */
|
|
}
|
|
}
|
|
} else {
|
|
// @ts-ignore window.electron is declared in index.d.ts
|
|
window.electron = electron
|
|
// @ts-ignore window.api is declared in index.d.ts
|
|
window.api = api
|
|
}
|