feat: Phase N power-user surface (terminal, palette, per-item, sort, url-templates)

- Built-in yt-dlp terminal: src/main/terminal.ts streams raw yt-dlp runs
  (binary fixed, gated on customCommandEnabled), new TerminalView + sidebar tab
- Command palette (Ctrl/Cmd+K), portal-free CommandPalette.tsx wired in App
- Per-playlist-item editing: per-row video/audio toggle + All video/All audio
  batch; addPlaylist enqueues via addMany with per-item kind
- Weighted format sorting: DownloadOptions.formatSort -> raw -S (overrides codec)
- URL-regex template auto-matching: CommandTemplate.urlPattern + selectExtraArgs
  matchesUrl, threaded through resolveExtraArgs; field in TemplateManager

typecheck + test (192) + electron-vite build all clean. Roadmap: Phase N COMPLETE.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-26 11:10:22 -04:00
parent 9ac11ceb6c
commit da37690b42
18 changed files with 820 additions and 58 deletions
+19 -1
View File
@@ -28,7 +28,9 @@ import {
type IndexProgress,
type IndexSourceResult,
type SyncResult,
type ScheduledSyncStatus
type ScheduledSyncStatus,
type TerminalEvent,
type TerminalRunResult
} from '@shared/ipc'
// The surface exposed to the renderer. Keep this thin: it only forwards to IPC.
@@ -209,6 +211,22 @@ const api = {
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)
}
}