Fix L49/L53/L59: error string newlines, dialog null-safety, shared theme constant
L49: Remove newline from missing-binary error message in download.ts; inline
error spans render it as literal newline, now one sentence with period.
L53: backup.ts exportBackup/importBackup/showMessageBox no longer use win!
non-null assertions -- each uses a guarded ternary (same pattern index.ts
already used for chooseFolder).
L59: Extract PAGE_BACKGROUND to src/shared/theme.ts; main/index.ts and
renderer/src/theme.ts both import from it -- no more "keep in sync" comment.
typecheck + 242 tests + eslint + prettier green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+3
-3
@@ -475,13 +475,13 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
|
|||||||
other yt-dlp timeout ("Timed out …").
|
other yt-dlp timeout ("Timed out …").
|
||||||
- [ ] **L48 — `cleanError` strips a leading `error:`** for live failures, while raw `ERROR:` text
|
- [ ] **L48 — `cleanError` strips a leading `error:`** for live failures, while raw `ERROR:` text
|
||||||
shows elsewhere (errorlog seed / terminal) — inconsistent error normalization.
|
shows elsewhere (errorlog seed / terminal) — inconsistent error normalization.
|
||||||
- [ ] **L49 — Newlines in user-facing error strings.** download.ts missing-binary messages embed
|
- [x] **L49 — Newlines in user-facing error strings.** download.ts missing-binary messages embed
|
||||||
`\n`, which renders awkwardly in inline/Caption error spans.
|
`\n`, which renders awkwardly in inline/Caption error spans.
|
||||||
- [x] **L50 — "Cookies saved" shown for 0 cookies.** Closing the sign-in window without logging in
|
- [x] **L50 — "Cookies saved" shown for 0 cookies.** Closing the sign-in window without logging in
|
||||||
still writes the file and reports `ok` with `cookieCount: 0`.
|
still writes the file and reports `ok` with `cookieCount: 0`.
|
||||||
- [ ] **L51 — Scheduled daily sync time hardcoded to 09:00** (schedule.ts) — on/off toggle only, no time picker.
|
- [ ] **L51 — Scheduled daily sync time hardcoded to 09:00** (schedule.ts) — on/off toggle only, no time picker.
|
||||||
- [ ] **L52 — Installer handoff is a fixed `setTimeout(app.quit, 1500)`** (updater.ts) — a race on slow machines.
|
- [ ] **L52 — Installer handoff is a fixed `setTimeout(app.quit, 1500)`** (updater.ts) — a race on slow machines.
|
||||||
- [ ] **L53 — `dialog.show*Dialog(win!, …)` non-null assertions** on a possibly-null window
|
- [x] **L53 — `dialog.show*Dialog(win!, …)` non-null assertions** on a possibly-null window
|
||||||
(chooseFolder, backup export/import) — can throw if the sender window is gone.
|
(chooseFolder, backup export/import) — can throw if the sender window is gone.
|
||||||
- [ ] **L54 — DownloadBar preview `<img>` has no `onError` fallback** (inconsistent with `MediaThumb`'s retry).
|
- [ ] **L54 — DownloadBar preview `<img>` has no `onError` fallback** (inconsistent with `MediaThumb`'s retry).
|
||||||
- [x] **L55 — QueueItem meta line can show size twice** — a probed-format `quality` label already
|
- [x] **L55 — QueueItem meta line can show size twice** — a probed-format `quality` label already
|
||||||
@@ -491,7 +491,7 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
|
|||||||
- [x] **L57 — Scheduling a past datetime silently downloads now** (`buildItem` future-only guard),
|
- [x] **L57 — Scheduling a past datetime silently downloads now** (`buildItem` future-only guard),
|
||||||
no feedback that the schedule was ignored.
|
no feedback that the schedule was ignored.
|
||||||
- [x] **L58 — `chooseFolder` passes the macOS-only `createDirectory` property** — dead option on Windows.
|
- [x] **L58 — `chooseFolder` passes the macOS-only `createDirectory` property** — dead option on Windows.
|
||||||
- [ ] **L59 — `THEME_BACKGROUND` (index.ts) duplicates `pageBackground` (theme.ts)** — two
|
- [x] **L59 — `THEME_BACKGROUND` (index.ts) duplicates `pageBackground` (theme.ts)** — two
|
||||||
hand-synced color constants (a "keep in sync" comment guards them).
|
hand-synced color constants (a "keep in sync" comment guards them).
|
||||||
- [x] **L60 — `'external-url'` channel name lacks the `category:` prefix** every other IPC channel uses.
|
- [x] **L60 — `'external-url'` channel name lacks the `category:` prefix** every other IPC channel uses.
|
||||||
- [ ] **L61 — `taskbarProgress` is the lone `ipcMain.on`** (fire-and-forget) amid all-`invoke` channels.
|
- [ ] **L61 — `taskbarProgress` is the lone `ipcMain.on`** (fire-and-forget) amid all-`invoke` channels.
|
||||||
|
|||||||
+13
-8
@@ -12,10 +12,11 @@ interface BackupFile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function exportBackup(win: BrowserWindow | undefined): Promise<BackupExportResult> {
|
export async function exportBackup(win: BrowserWindow | undefined): Promise<BackupExportResult> {
|
||||||
const res = await dialog.showSaveDialog(win!, {
|
const opts = {
|
||||||
defaultPath: 'aerofetch-backup.json',
|
defaultPath: 'aerofetch-backup.json',
|
||||||
filters: [{ name: 'JSON', extensions: ['json'] }]
|
filters: [{ name: 'JSON', extensions: ['json'] }]
|
||||||
})
|
}
|
||||||
|
const res = await (win ? dialog.showSaveDialog(win, opts) : dialog.showSaveDialog(opts))
|
||||||
if (res.canceled || !res.filePath) return { ok: false }
|
if (res.canceled || !res.filePath) return { ok: false }
|
||||||
// Strip credentials (proxy creds, API tokens) so a backup file shared or synced
|
// Strip credentials (proxy creds, API tokens) so a backup file shared or synced
|
||||||
// to the cloud doesn't leak secrets in plaintext (M22). The user re-enters them
|
// to the cloud doesn't leak secrets in plaintext (M22). The user re-enters them
|
||||||
@@ -32,10 +33,11 @@ export async function exportBackup(win: BrowserWindow | undefined): Promise<Back
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function importBackup(win: BrowserWindow | undefined): Promise<BackupImportResult> {
|
export async function importBackup(win: BrowserWindow | undefined): Promise<BackupImportResult> {
|
||||||
const res = await dialog.showOpenDialog(win!, {
|
const openOpts = {
|
||||||
properties: ['openFile'],
|
properties: ['openFile' as const],
|
||||||
filters: [{ name: 'JSON', extensions: ['json'] }]
|
filters: [{ name: 'JSON', extensions: ['json'] }]
|
||||||
})
|
}
|
||||||
|
const res = await (win ? dialog.showOpenDialog(win, openOpts) : dialog.showOpenDialog(openOpts))
|
||||||
if (res.canceled || !res.filePaths[0]) return { ok: false }
|
if (res.canceled || !res.filePaths[0]) return { ok: false }
|
||||||
|
|
||||||
let parsed: unknown
|
let parsed: unknown
|
||||||
@@ -86,8 +88,8 @@ export async function importBackup(win: BrowserWindow | undefined): Promise<Back
|
|||||||
})
|
})
|
||||||
.join('\n')
|
.join('\n')
|
||||||
const more = commandTemplates.length > 10 ? `\n…and ${commandTemplates.length - 10} more.` : ''
|
const more = commandTemplates.length > 10 ? `\n…and ${commandTemplates.length - 10} more.` : ''
|
||||||
const choice = await dialog.showMessageBox(win!, {
|
const msgOpts = {
|
||||||
type: 'warning',
|
type: 'warning' as const,
|
||||||
buttons: ['Enable custom commands', 'Import without enabling', 'Cancel'],
|
buttons: ['Enable custom commands', 'Import without enabling', 'Cancel'],
|
||||||
defaultId: 1,
|
defaultId: 1,
|
||||||
cancelId: 2,
|
cancelId: 2,
|
||||||
@@ -99,7 +101,10 @@ export async function importBackup(win: BrowserWindow | undefined): Promise<Back
|
|||||||
'Custom commands run extra yt-dlp flags on every download. Only enable them if you trust this backup file.\n\n' +
|
'Custom commands run extra yt-dlp flags on every download. Only enable them if you trust this backup file.\n\n' +
|
||||||
preview +
|
preview +
|
||||||
more
|
more
|
||||||
})
|
}
|
||||||
|
const choice = await (win
|
||||||
|
? dialog.showMessageBox(win, msgOpts)
|
||||||
|
: dialog.showMessageBox(msgOpts))
|
||||||
if (choice.response === 2) return { ok: false } // Cancel — change nothing
|
if (choice.response === 2) return { ok: false } // Cancel — change nothing
|
||||||
applyCustomCommands = choice.response === 0
|
applyCustomCommands = choice.response === 0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -239,9 +239,7 @@ export function startDownload(wc: WebContents, opts: StartDownloadOptions): Star
|
|||||||
if (missingBins.length > 0) {
|
if (missingBins.length > 0) {
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
error:
|
error: `${missingBins.join(' and ')} not found in ${getBinDir()}. Add the ffmpeg build's binaries to resources/bin/ (see the README there).`
|
||||||
`${missingBins.join(' and ')} not found in ${getBinDir()}\n` +
|
|
||||||
`Add the ffmpeg build's binaries to resources/bin/ (see the README there).`
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Reject anything that isn't an http(s) URL before it reaches yt-dlp's argv,
|
// Reject anything that isn't an http(s) URL before it reaches yt-dlp's argv,
|
||||||
|
|||||||
+5
-10
@@ -22,6 +22,7 @@ import {
|
|||||||
type SystemThemeInfo,
|
type SystemThemeInfo,
|
||||||
type TaskbarProgress
|
type TaskbarProgress
|
||||||
} from '@shared/ipc'
|
} from '@shared/ipc'
|
||||||
|
import { PAGE_BACKGROUND } from '@shared/theme'
|
||||||
import { getYtdlpVersion, updateYtdlp, runStartupYtdlpAutoUpdate } from './ytdlp'
|
import { getYtdlpVersion, updateYtdlp, runStartupYtdlpAutoUpdate } from './ytdlp'
|
||||||
import { getFfmpegVersions } from './ffmpeg'
|
import { getFfmpegVersions } from './ffmpeg'
|
||||||
import { checkForAppUpdate, downloadAppUpdate, runAppUpdate } from './updater'
|
import { checkForAppUpdate, downloadAppUpdate, runAppUpdate } from './updater'
|
||||||
@@ -109,12 +110,6 @@ if (is.dev && devScript) {
|
|||||||
|
|
||||||
let mainWindow: BrowserWindow | null = null
|
let mainWindow: BrowserWindow | null = null
|
||||||
|
|
||||||
// Page background per theme — keep in sync with `pageBackground` in
|
|
||||||
// src/renderer/src/theme.ts. Set as the window's NATIVE background so the
|
|
||||||
// one-frame compositor repaint (when a tooltip/dropdown overlay first paints on
|
|
||||||
// Windows) shows the app's current color instead of a mismatched white flash.
|
|
||||||
const THEME_BACKGROUND = { light: '#f7f7f8', dark: '#161618' } as const
|
|
||||||
|
|
||||||
// Resolve 'system' against the OS preference so callers always get a concrete color.
|
// Resolve 'system' against the OS preference so callers always get a concrete color.
|
||||||
function resolveBackgroundMode(theme: Settings['theme']): 'light' | 'dark' {
|
function resolveBackgroundMode(theme: Settings['theme']): 'light' | 'dark' {
|
||||||
return theme === 'system' ? (nativeTheme.shouldUseDarkColors ? 'dark' : 'light') : theme
|
return theme === 'system' ? (nativeTheme.shouldUseDarkColors ? 'dark' : 'light') : theme
|
||||||
@@ -177,7 +172,7 @@ function createWindow(): void {
|
|||||||
minWidth: 640,
|
minWidth: 640,
|
||||||
minHeight: 480,
|
minHeight: 480,
|
||||||
show: false,
|
show: false,
|
||||||
backgroundColor: THEME_BACKGROUND[resolveBackgroundMode(getSettings().theme)],
|
backgroundColor: PAGE_BACKGROUND[resolveBackgroundMode(getSettings().theme)],
|
||||||
autoHideMenuBar: true,
|
autoHideMenuBar: true,
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
preload: join(__dirname, '../preload/index.cjs'),
|
preload: join(__dirname, '../preload/index.cjs'),
|
||||||
@@ -342,7 +337,7 @@ function registerIpcHandlers(): void {
|
|||||||
// caption always matches the in-app theme (W3). Use the validated result.
|
// caption always matches the in-app theme (W3). Use the validated result.
|
||||||
if (partial.theme) {
|
if (partial.theme) {
|
||||||
BrowserWindow.fromWebContents(e.sender)?.setBackgroundColor(
|
BrowserWindow.fromWebContents(e.sender)?.setBackgroundColor(
|
||||||
THEME_BACKGROUND[resolveBackgroundMode(result.theme)]
|
PAGE_BACKGROUND[resolveBackgroundMode(result.theme)]
|
||||||
)
|
)
|
||||||
applyNativeTheme(result.theme)
|
applyNativeTheme(result.theme)
|
||||||
}
|
}
|
||||||
@@ -394,7 +389,7 @@ function registerIpcHandlers(): void {
|
|||||||
// background and title bar in sync the same way settingsSet does (W3).
|
// background and title bar in sync the same way settingsSet does (W3).
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
const theme = getSettings().theme
|
const theme = getSettings().theme
|
||||||
win?.setBackgroundColor(THEME_BACKGROUND[resolveBackgroundMode(theme)])
|
win?.setBackgroundColor(PAGE_BACKGROUND[resolveBackgroundMode(theme)])
|
||||||
applyNativeTheme(theme)
|
applyNativeTheme(theme)
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
@@ -461,7 +456,7 @@ function registerIpcHandlers(): void {
|
|||||||
function registerSystemThemeBridge(): void {
|
function registerSystemThemeBridge(): void {
|
||||||
nativeTheme.on('updated', () => {
|
nativeTheme.on('updated', () => {
|
||||||
const info = getSystemThemeInfo()
|
const info = getSystemThemeInfo()
|
||||||
const bg = THEME_BACKGROUND[resolveBackgroundMode(getSettings().theme)]
|
const bg = PAGE_BACKGROUND[resolveBackgroundMode(getSettings().theme)]
|
||||||
for (const win of BrowserWindow.getAllWindows()) {
|
for (const win of BrowserWindow.getAllWindows()) {
|
||||||
win.webContents.send(IpcChannels.systemThemeUpdate, info)
|
win.webContents.send(IpcChannels.systemThemeUpdate, info)
|
||||||
if (getSettings().theme === 'system') win.setBackgroundColor(bg)
|
if (getSettings().theme === 'system') win.setBackgroundColor(bg)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { PAGE_BACKGROUND } from '@shared/theme'
|
||||||
import {
|
import {
|
||||||
createLightTheme,
|
createLightTheme,
|
||||||
createDarkTheme,
|
createDarkTheme,
|
||||||
@@ -168,14 +169,7 @@ export function getTheme(accent: AccentColor, mode: 'light' | 'dark'): Theme {
|
|||||||
return mode === 'dark' ? preset.dark : preset.light
|
return mode === 'dark' ? preset.dark : preset.light
|
||||||
}
|
}
|
||||||
|
|
||||||
// Page background behind the app shell. Light: a clean, barely-cool off-white.
|
export const pageBackground = PAGE_BACKGROUND
|
||||||
// Dark: neutral charcoal. Intentionally accent-INDEPENDENT — the accent shows up
|
|
||||||
// only on buttons/links, so this stays neutral whichever preset is chosen.
|
|
||||||
// Keep light/dark in sync with THEME_BACKGROUND in src/main/index.ts.
|
|
||||||
export const pageBackground = {
|
|
||||||
light: '#f7f7f8',
|
|
||||||
dark: '#161618'
|
|
||||||
}
|
|
||||||
|
|
||||||
// Per-kind thumbnail tints. Video vs audio differ by ramp DEPTH rather than a
|
// Per-kind thumbnail tints. Video vs audio differ by ramp DEPTH rather than a
|
||||||
// competing hue, keeping a monochrome look. Theme-aware (light/dark) but
|
// competing hue, keeping a monochrome look. Theme-aware (light/dark) but
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
/**
|
||||||
|
* Native window background colors per theme mode. Light: barely-cool off-white.
|
||||||
|
* Dark: neutral charcoal. These match the renderer's page background so there's
|
||||||
|
* no flash when a compositor overlay (tooltip, dropdown) first renders on Windows.
|
||||||
|
* Intentionally accent-independent — the accent shows on buttons/links only.
|
||||||
|
*
|
||||||
|
* Shared between main/index.ts (BrowserWindow backgroundColor) and
|
||||||
|
* renderer/src/theme.ts (CSS backgroundColor on the root div) so the two never
|
||||||
|
* drift apart.
|
||||||
|
*/
|
||||||
|
export const PAGE_BACKGROUND = { light: '#f7f7f8', dark: '#161618' } as const
|
||||||
Reference in New Issue
Block a user