Fix L46/L71/L74: unify yt-dlp missing message, editable dir inputs, swatch a11y
L46: Introduced YTDLP_MISSING_MSG constant in binaries.ts. All four callers
(download, indexer, probe, ytdlp) now emit the same user-facing string:
"yt-dlp.exe is missing. Open Settings -> Software update to re-download it."
This removes newline-embedded messages and four divergent phrasings.
L71: Removed readOnly from videoDir/audioDir folder inputs in SettingsView.
Added onChange handler so users can paste or type a path directly without
being forced to use the Browse button. The path still flows through
applySettings() in main, which validates it.
L74: Accent color swatches were triple-labeled (aria-pressed + aria-label +
title). Replaced with a single aria-label that embeds "(selected)" state,
plus title for the hover tooltip. Removed the incorrect aria-pressed
(these are selection buttons, not toggles).
typecheck + 242 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+3
-3
@@ -469,7 +469,7 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
|
|||||||
but there's no ESLint/Prettier config, script, or dependency — no enforced style tooling.
|
but there's no ESLint/Prettier config, script, or dependency — no enforced style tooling.
|
||||||
- [x] **L45 — Three self-descriptions** drift: pkg `description` "A yt-dlp frontend for Windows" vs
|
- [x] **L45 — Three self-descriptions** drift: pkg `description` "A yt-dlp frontend for Windows" vs
|
||||||
builder `copyright` "yt-dlp frontend" vs Sidebar caption "yt-dlp frontend".
|
builder `copyright` "yt-dlp frontend" vs Sidebar caption "yt-dlp frontend".
|
||||||
- [ ] **L46 — yt-dlp-missing error copy differs across 4 modules** (download/ytdlp/probe/indexer):
|
- [x] **L46 — yt-dlp-missing error copy differs across 4 modules** (download/ytdlp/probe/indexer):
|
||||||
"Reinstall AeroFetch, or drop…" vs "Download it into resources/bin/…" vs "Drop it into resources/bin/."
|
"Reinstall AeroFetch, or drop…" vs "Download it into resources/bin/…" vs "Drop it into resources/bin/."
|
||||||
- [x] **L47 — `probeMeta` 30s timeout returns null silently** — no surfaced message, unlike every
|
- [x] **L47 — `probeMeta` 30s timeout returns null silently** — no surfaced message, unlike every
|
||||||
other yt-dlp timeout ("Timed out …").
|
other yt-dlp timeout ("Timed out …").
|
||||||
@@ -510,12 +510,12 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
|
|||||||
(`typeof crypto !== 'undefined'` is always true) — dead defensive branch (see M7). *Fixed with M7: the
|
(`typeof crypto !== 'undefined'` is always true) — dead defensive branch (see M7). *Fixed with M7: the
|
||||||
branch now lives once in `newId` (kept so the fn is total in any host) and is covered by a test that stubs
|
branch now lives once in `newId` (kept so the fn is total in any host) and is covered by a test that stubs
|
||||||
`crypto` to exercise it.*
|
`crypto` to exercise it.*
|
||||||
- [ ] **L71 — Settings folder inputs are `readOnly`** with only a Browse button — no way to paste a
|
- [x] **L71 — Settings folder inputs are `readOnly`** with only a Browse button — no way to paste a
|
||||||
known path.
|
known path.
|
||||||
- [x] **L72 — History re-download drops the thumbnail** (passes only title/channel), so the
|
- [x] **L72 — History re-download drops the thumbnail** (passes only title/channel), so the
|
||||||
re-queued row loses its thumbnail until re-probed.
|
re-queued row loses its thumbnail until re-probed.
|
||||||
- [ ] **L73 — Two verbs for the same probe action** — DownloadBar "Fetch" vs LibraryView "Index".
|
- [ ] **L73 — Two verbs for the same probe action** — DownloadBar "Fetch" vs LibraryView "Index".
|
||||||
- [ ] **L74 — Accent swatches are triple-labeled** (`aria-pressed` + `aria-label` + `title`).
|
- [x] **L74 — Accent swatches are triple-labeled** (`aria-pressed` + `aria-label` + `title`).
|
||||||
- [x] **L75 — Update-token field always visible** in the Software-update card, even when no update
|
- [x] **L75 — Update-token field always visible** in the Software-update card, even when no update
|
||||||
is pending — buries an advanced/rarely-needed input.
|
is pending — buries an advanced/rarely-needed input.
|
||||||
- [x] **L76 — Duplicate-warning may show a placeholder title** — `setDup(existing.title)` can be the
|
- [x] **L76 — Duplicate-warning may show a placeholder title** — `setDup(existing.title)` can be the
|
||||||
|
|||||||
@@ -80,6 +80,10 @@ export function getFfprobePath(): string {
|
|||||||
return join(getBinDir(), 'ffprobe.exe')
|
return join(getBinDir(), 'ffprobe.exe')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** User-facing error shown whenever yt-dlp.exe is not found at startup. */
|
||||||
|
export const YTDLP_MISSING_MSG =
|
||||||
|
'yt-dlp.exe is missing. Open Settings → Software update to re-download it.'
|
||||||
|
|
||||||
/** Optional bundled external downloader; absent unless dropped into resources/bin. */
|
/** Optional bundled external downloader; absent unless dropped into resources/bin. */
|
||||||
export function getAria2cPath(): string {
|
export function getAria2cPath(): string {
|
||||||
return join(getBinDir(), 'aria2c.exe')
|
return join(getBinDir(), 'aria2c.exe')
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import {
|
|||||||
getFfmpegPath,
|
getFfmpegPath,
|
||||||
getFfprobePath,
|
getFfprobePath,
|
||||||
getSystem32Path,
|
getSystem32Path,
|
||||||
getAppIconImage
|
getAppIconImage,
|
||||||
|
YTDLP_MISSING_MSG
|
||||||
} from './binaries'
|
} from './binaries'
|
||||||
import { getSettings, getDownloadArchivePath, getDefaultMediaDir } from './settings'
|
import { getSettings, getDownloadArchivePath, getDefaultMediaDir } from './settings'
|
||||||
import { ensureManagedYtdlp } from './ytdlp'
|
import { ensureManagedYtdlp } from './ytdlp'
|
||||||
@@ -225,7 +226,7 @@ export function startDownload(wc: WebContents, opts: StartDownloadOptions): Star
|
|||||||
if (!existsSync(ytdlp)) {
|
if (!existsSync(ytdlp)) {
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
error: `yt-dlp.exe is missing and couldn't be restored from the bundle.\nReinstall AeroFetch, or drop yt-dlp.exe into resources/bin/ (see the README there).`
|
error: YTDLP_MISSING_MSG
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// ffmpeg is used by nearly every download (merge, audio extract, thumbnail/
|
// ffmpeg is used by nearly every download (merge, audio extract, thumbnail/
|
||||||
|
|||||||
+2
-2
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
import { execFile } from 'child_process'
|
import { execFile } from 'child_process'
|
||||||
import { existsSync } from 'fs'
|
import { existsSync } from 'fs'
|
||||||
import { getYtdlpPath } from './binaries'
|
import { getYtdlpPath, YTDLP_MISSING_MSG } from './binaries'
|
||||||
import { cleanError } from './log'
|
import { cleanError } from './log'
|
||||||
import { assertHttpUrl } from './url'
|
import { assertHttpUrl } from './url'
|
||||||
import {
|
import {
|
||||||
@@ -90,7 +90,7 @@ export async function indexSource(
|
|||||||
return { ok: false, error: (e as Error).message }
|
return { ok: false, error: (e as Error).message }
|
||||||
}
|
}
|
||||||
if (!existsSync(getYtdlpPath())) {
|
if (!existsSync(getYtdlpPath())) {
|
||||||
return { ok: false, error: 'yt-dlp.exe not found. Drop it into resources/bin/.' }
|
return { ok: false, error: YTDLP_MISSING_MSG }
|
||||||
}
|
}
|
||||||
|
|
||||||
const cls = classifySource(url)
|
const cls = classifySource(url)
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
import { execFile } from 'child_process'
|
import { execFile } from 'child_process'
|
||||||
import { existsSync } from 'fs'
|
import { existsSync } from 'fs'
|
||||||
import { getYtdlpPath } from './binaries'
|
import { getYtdlpPath, YTDLP_MISSING_MSG } from './binaries'
|
||||||
import { fmtBytes } from './lib/formatters'
|
import { fmtBytes } from './lib/formatters'
|
||||||
import { cleanError } from './log'
|
import { cleanError } from './log'
|
||||||
import { assertHttpUrl } from './url'
|
import { assertHttpUrl } from './url'
|
||||||
@@ -115,7 +115,7 @@ export function probeMedia(url: string): Promise<ProbeResult> {
|
|||||||
if (!existsSync(ytdlp)) {
|
if (!existsSync(ytdlp)) {
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
ok: false,
|
ok: false,
|
||||||
error: `yt-dlp.exe not found at ${ytdlp}\nDrop it into resources/bin/ (see the README there).`
|
error: YTDLP_MISSING_MSG
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
let target: string
|
let target: string
|
||||||
|
|||||||
+3
-3
@@ -1,7 +1,7 @@
|
|||||||
import { execFile } from 'child_process'
|
import { execFile } from 'child_process'
|
||||||
import { existsSync, mkdirSync, copyFileSync } from 'fs'
|
import { existsSync, mkdirSync, copyFileSync } from 'fs'
|
||||||
import { dirname } from 'path'
|
import { dirname } from 'path'
|
||||||
import { getYtdlpPath, getBundledYtdlpPath } from './binaries'
|
import { getYtdlpPath, getBundledYtdlpPath, YTDLP_MISSING_MSG } from './binaries'
|
||||||
import { getSettings, setSettings } from './settings'
|
import { getSettings, setSettings } from './settings'
|
||||||
import { shouldAutoCheckYtdlp } from './ytdlpPolicy'
|
import { shouldAutoCheckYtdlp } from './ytdlpPolicy'
|
||||||
import {
|
import {
|
||||||
@@ -39,7 +39,7 @@ export function getYtdlpVersion(): Promise<YtdlpVersionResult> {
|
|||||||
if (!existsSync(ytdlpPath)) {
|
if (!existsSync(ytdlpPath)) {
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
ok: false,
|
ok: false,
|
||||||
error: `yt-dlp.exe not found at ${ytdlpPath}\nDownload it into resources/bin/ (see the README there).`
|
error: YTDLP_MISSING_MSG
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,7 +85,7 @@ export function updateYtdlp(channel: YtdlpUpdateChannel): Promise<YtdlpUpdateRes
|
|||||||
if (!existsSync(ytdlpPath)) {
|
if (!existsSync(ytdlpPath)) {
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
ok: false,
|
ok: false,
|
||||||
error: `yt-dlp.exe not found at ${ytdlpPath}\nDownload it into resources/bin/ (see the README there).`
|
error: YTDLP_MISSING_MSG
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -485,11 +485,11 @@ export function SettingsView(): React.JSX.Element {
|
|||||||
>
|
>
|
||||||
<div className={styles.folderRow}>
|
<div className={styles.folderRow}>
|
||||||
<Input
|
<Input
|
||||||
readOnly
|
|
||||||
className={styles.folderInput}
|
className={styles.folderInput}
|
||||||
value={videoDir}
|
value={videoDir}
|
||||||
placeholder="Documents\Video (default)"
|
placeholder="Documents\Video (default)"
|
||||||
contentBefore={<FolderRegular />}
|
contentBefore={<FolderRegular />}
|
||||||
|
onChange={(_, d) => update({ videoDir: d.value })}
|
||||||
/>
|
/>
|
||||||
<Button icon={<FolderRegular />} onClick={() => chooseDir('videoDir')}>
|
<Button icon={<FolderRegular />} onClick={() => chooseDir('videoDir')}>
|
||||||
Browse
|
Browse
|
||||||
@@ -508,11 +508,11 @@ export function SettingsView(): React.JSX.Element {
|
|||||||
>
|
>
|
||||||
<div className={styles.folderRow}>
|
<div className={styles.folderRow}>
|
||||||
<Input
|
<Input
|
||||||
readOnly
|
|
||||||
className={styles.folderInput}
|
className={styles.folderInput}
|
||||||
value={audioDir}
|
value={audioDir}
|
||||||
placeholder="Documents\Audio (default)"
|
placeholder="Documents\Audio (default)"
|
||||||
contentBefore={<FolderRegular />}
|
contentBefore={<FolderRegular />}
|
||||||
|
onChange={(_, d) => update({ audioDir: d.value })}
|
||||||
/>
|
/>
|
||||||
<Button icon={<FolderRegular />} onClick={() => chooseDir('audioDir')}>
|
<Button icon={<FolderRegular />} onClick={() => chooseDir('audioDir')}>
|
||||||
Browse
|
Browse
|
||||||
@@ -625,8 +625,7 @@ export function SettingsView(): React.JSX.Element {
|
|||||||
)}
|
)}
|
||||||
style={{ backgroundColor: opt.swatch }}
|
style={{ backgroundColor: opt.swatch }}
|
||||||
onClick={() => update({ accentColor: opt.value as AccentColor })}
|
onClick={() => update({ accentColor: opt.value as AccentColor })}
|
||||||
aria-pressed={accentColor === opt.value}
|
aria-label={`${opt.label}${accentColor === opt.value ? ' (selected)' : ''}`}
|
||||||
aria-label={opt.label}
|
|
||||||
title={opt.label}
|
title={opt.label}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
Reference in New Issue
Block a user