From 3df32d0505bbbd36f220a3225d116ffeeed18b24 Mon Sep 17 00:00:00 2001 From: debont80 Date: Tue, 30 Jun 2026 14:16:15 -0400 Subject: [PATCH] Fix L22/L24/L27: kind-aware form, shell openUrl, Enter-to-download 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 --- CODE-AUDIT.md | 6 +- src/main/index.ts | 8 +++ src/preload/index.ts | 2 + src/renderer/src/components/DownloadBar.tsx | 30 +++++---- .../src/components/DownloadOptionsForm.tsx | 64 +++++++++++-------- src/renderer/src/components/SettingsView.tsx | 3 +- src/renderer/src/main.tsx | 1 + src/shared/ipc.ts | 1 + 8 files changed, 70 insertions(+), 45 deletions(-) diff --git a/CODE-AUDIT.md b/CODE-AUDIT.md index 63aaa71..e737ce0 100644 --- a/CODE-AUDIT.md +++ b/CODE-AUDIT.md @@ -420,18 +420,18 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n - [ ] **L21 — Inconsistent in-component error handling.** SettingsView funnels the app-update not-ok result into a dedicated `appUpdError` slot but keeps not-ok duals for version/update/export/import — two patterns in one file. -- [ ] **L22 — DownloadOptionsForm shows both audio + video controls** regardless of the chosen +- [x] **L22 — DownloadOptionsForm shows both audio + video controls** regardless of the chosen kind in the per-download override panel (audio format shown for a video download, etc.). - [x] **L23 — Diagnostics shows first 20 errors** with no "showing 20 of N" hint (the copied report includes all). -- [ ] **L24 — `window.open(htmlUrl, '_blank')`** in SettingsView is the only `window.open` in the +- [x] **L24 — `window.open(htmlUrl, '_blank')`** in SettingsView is the only `window.open` in the app (relies on the window-open handler); elsewhere external opens go through IPC/`shell`. - [x] **L25 — Taskbar effect over-fires.** `App.tsx` subscribes to the whole downloads store and recomputes `summarizeQueue` + sends the taskbar IPC on *every* store change (each progress tick); throttle or dedupe by computed value. - [ ] **L26 — Third URL-sniffing path.** DownloadBar's drag-drop `firstUrl` is a separate "first http(s) line" extractor alongside `useClipboardLink.looksLikeUrl` and deeplink's scan. -- [ ] **L27 — DownloadBar Enter probes, not downloads.** Enter in the URL field triggers +- [x] **L27 — DownloadBar Enter probes, not downloads.** Enter in the URL field triggers `fetchFormats`; there's no keyboard path to actually start the download (LibraryView's Enter runs its primary action). Inconsistent. - [ ] **L28 — Default kind/quality applied once.** DownloadBar seeds from settings via a one-shot diff --git a/src/main/index.ts b/src/main/index.ts index 73e2708..89bfe03 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -325,6 +325,14 @@ function registerIpcHandlers(): void { }) ipcMain.handle(IpcChannels.openPath, (_e, p: string) => safeOpenPath(p)) + ipcMain.handle(IpcChannels.openUrl, (_e, url: string) => { + try { + const { protocol } = new URL(url) + if (protocol === 'http:' || protocol === 'https:') void shell.openExternal(url) + } catch { + // Ignore malformed URLs. + } + }) ipcMain.handle(IpcChannels.showInFolder, (_e, p: string) => safeShowInFolder(p)) ipcMain.handle(IpcChannels.clipboardRead, () => clipboard.readText()) diff --git a/src/preload/index.ts b/src/preload/index.ts index 7655631..4ed7209 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -77,6 +77,8 @@ const api = { openPath: (path: string): Promise => ipcRenderer.invoke(IpcChannels.openPath, path), + openUrl: (url: string): Promise => ipcRenderer.invoke(IpcChannels.openUrl, url), + showInFolder: (path: string): Promise => ipcRenderer.invoke(IpcChannels.showInFolder, path), readClipboard: (): Promise => ipcRenderer.invoke(IpcChannels.clipboardRead), diff --git a/src/renderer/src/components/DownloadBar.tsx b/src/renderer/src/components/DownloadBar.tsx index edb0a64..a5a2b47 100644 --- a/src/renderer/src/components/DownloadBar.tsx +++ b/src/renderer/src/components/DownloadBar.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef } from 'react' +import { useState, useEffect, useRef } from 'react' import { Input, Textarea, @@ -254,7 +254,7 @@ const useStyles = makeStyles({ }) // Format a Date as the `YYYY-MM-DDTHH:mm` value a datetime-local input expects, -// in LOCAL time — used as the picker's `min` so a past time can't be chosen and +// in LOCAL time -- used as the picker's `min` so a past time can't be chosen and // then silently download immediately (L156/L57). function toLocalDatetimeValue(d: Date): string { const pad = (n: number): string => String(n).padStart(2, '0') @@ -302,7 +302,7 @@ export function DownloadBar(): React.JSX.Element { onUrlChange(fromText) return } - // A dropped Windows .url Internet Shortcut — read its URL= line. + // A dropped Windows .url Internet Shortcut -- read its URL= line. const file = Array.from(dt.files).find((f) => f.name.toLowerCase().endsWith('.url')) if (file) { file @@ -342,7 +342,7 @@ export function DownloadBar(): React.JSX.Element { // Clipboard auto-detect and links handed to AeroFetch from outside (the // aerofetch:// protocol or a "Send to" .url file) share one suggestion banner, // driven by the same hook the library's add-source field uses. An external link - // always takes priority over a clipboard guess — it's a direct request. + // always takes priority over a clipboard guess -- it's a direct request. const { suggestion, source: suggestionSource, @@ -376,7 +376,7 @@ export function DownloadBar(): React.JSX.Element { function onUrlChange(next: string): void { setUrl(next) - // Any probed info — or a duplicate warning — is stale once the URL changes. + // Any probed info -- or a duplicate warning -- is stale once the URL changes. if (info || probeError || playlist) clearProbe() if (dup) setDup(null) confirmDup.current = false @@ -418,7 +418,7 @@ export function DownloadBar(): React.JSX.Element { const text = await navigator.clipboard.readText() if (text) onUrlChange(text.trim()) } catch { - /* clipboard blocked — ignore in preview */ + /* clipboard blocked -- ignore in preview */ } } @@ -457,7 +457,7 @@ export function DownloadBar(): React.JSX.Element { // Duplicate guard: if this URL is already in the queue (and the user hasn't // confirmed via "Download anyway"), warn instead of silently enqueuing a copy. - // Canceled/failed items don't count — re-adding those is a legitimate retry. + // Canceled/failed items don't count -- re-adding those is a legitimate retry. if (!confirmDup.current) { const existing = useDownloads .getState() @@ -551,7 +551,11 @@ export function DownloadBar(): React.JSX.Element { input={{ id: 'aerofetch-url', 'aria-label': 'Video or playlist URL' }} value={url} onChange={(_, d) => onUrlChange(d.value)} - onKeyDown={(e) => e.key === 'Enter' && fetchFormats()} + onKeyDown={(e) => { + if (e.key !== 'Enter') return + if (info || probeError || playlist) download() + else void fetchFormats() + }} placeholder="Paste a video or playlist URL, then fetch…" size="large" contentBefore={} @@ -598,7 +602,7 @@ export function DownloadBar(): React.JSX.Element { {dup && (
- Already in your queue: “{dup}”. + Already in your queue: "{dup}". @@ -622,7 +626,7 @@ export function DownloadBar(): React.JSX.Element { {probeError && (
- {probeError} — you can still download with the presets below. + {probeError} -- you can still download with the presets below.
)} @@ -697,8 +701,8 @@ export function DownloadBar(): React.JSX.Element {