From a4fd0feb6b1559f03cdc63ac18cecbb0e8ccbb13 Mon Sep 17 00:00:00 2001 From: debont80 Date: Tue, 30 Jun 2026 14:32:50 -0400 Subject: [PATCH] Fix L29/L52: QUALITY_OPTIONS moved to UI module; installer spawn via child_process L29: QUALITY_OPTIONS was a presentational constant living in store/downloads.ts, coupling views to the store. Moved to src/renderer/src/qualityOptions.ts, which assembles it directly from @shared/ipc. store/downloads.ts no longer imports VIDEO/AUDIO_QUALITY_OPTIONS. Both consumers (DownloadBar, SettingsView) updated to import from the new module. L52: Replaced the fixed `setTimeout(app.quit, 1500)` installer handoff with a detached spawn. The installer is spawned with stdio:'ignore' and unref'd so it outlives AeroFetch even on a slow machine. app.quit() fires on setImmediate so the IPC response returns to the renderer before the process exits. typecheck + 242 tests green. Co-Authored-By: Claude Opus 4.8 --- CODE-AUDIT.md | 4 ++-- src/main/updater.ts | 17 +++++++++-------- src/renderer/src/components/DownloadBar.tsx | 3 ++- src/renderer/src/components/SettingsView.tsx | 3 ++- src/renderer/src/qualityOptions.ts | 6 ++++++ src/renderer/src/store/downloads.ts | 9 --------- 6 files changed, 21 insertions(+), 21 deletions(-) create mode 100644 src/renderer/src/qualityOptions.ts diff --git a/CODE-AUDIT.md b/CODE-AUDIT.md index 1a8d966..eab18b7 100644 --- a/CODE-AUDIT.md +++ b/CODE-AUDIT.md @@ -436,7 +436,7 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n runs its primary action). Inconsistent. - [x] **L28 — Default kind/quality applied once.** DownloadBar seeds from settings via a one-shot ref; changing the default in Settings while on the Downloads tab isn't reflected until reload. -- [ ] **L29 — UI constant in the store.** `QUALITY_OPTIONS` lives in `store/downloads.ts` yet is a +- [x] **L29 — UI constant in the store.** `QUALITY_OPTIONS` lives in `store/downloads.ts` yet is a presentational constant imported by views — couples UI to the store module. - [x] **L30 — Redundant `loading="lazy"`** on `MediaThumb` images already inside a virtualized list (the virtualizer only mounts visible rows). @@ -480,7 +480,7 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n - [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`. - [ ] **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. +- [x] **L52 — Installer handoff is a fixed `setTimeout(app.quit, 1500)`** (updater.ts) — a race on slow machines. - [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. - [x] **L54 — DownloadBar preview `` has no `onError` fallback** (inconsistent with `MediaThumb`'s retry). diff --git a/src/main/updater.ts b/src/main/updater.ts index 6c068aa..79d1bb2 100644 --- a/src/main/updater.ts +++ b/src/main/updater.ts @@ -1,4 +1,5 @@ -import { app, net, shell, type WebContents } from 'electron' +import { app, net, type WebContents } from 'electron' +import { spawn } from 'child_process' import { createWriteStream, type WriteStream } from 'fs' import { stat, unlink } from 'fs/promises' import { join, normalize, dirname } from 'path' @@ -413,9 +414,6 @@ export async function downloadAppUpdate(url: string, wc: WebContents): Promise { try { @@ -428,10 +426,13 @@ export async function runAppUpdate(filePath: string): Promise<{ ok: boolean; err return { ok: false, error: 'Refused to run an unexpected file.' } } await stat(target) // throws if the file is missing - const err = await shell.openPath(target) // hand the installer to the OS - if (err) return { ok: false, error: err } - // Give the installer a beat to spawn, then quit so it can overwrite our files. - setTimeout(() => app.quit(), INSTALLER_HANDOFF_MS) + // Spawn the installer detached + unref'd so it outlives our process even on a + // slow machine. shell.openPath goes through ShellExecute but gives us no child + // PID to wait on; spawning directly lets us call unref() before we quit. + const child = spawn(target, [], { detached: true, stdio: 'ignore', shell: false }) + child.unref() + // Let the IPC response reach the renderer before we exit. + setImmediate(() => app.quit()) return { ok: true } } catch (e) { return { ok: false, error: e instanceof Error ? e.message : String(e) } diff --git a/src/renderer/src/components/DownloadBar.tsx b/src/renderer/src/components/DownloadBar.tsx index 4fb1374..6f8e259 100644 --- a/src/renderer/src/components/DownloadBar.tsx +++ b/src/renderer/src/components/DownloadBar.tsx @@ -28,7 +28,8 @@ import { WarningRegular } from '@fluentui/react-icons' import type { MediaInfo, FormatOption, PlaylistInfo } from '@shared/ipc' -import { useDownloads, QUALITY_OPTIONS, type MediaKind } from '../store/downloads' +import { useDownloads, type MediaKind } from '../store/downloads' +import { QUALITY_OPTIONS } from '../qualityOptions' import { sameVideo } from '../store/queueStats' import { useSettings } from '../store/settings' import { Select } from './Select' diff --git a/src/renderer/src/components/SettingsView.tsx b/src/renderer/src/components/SettingsView.tsx index 8a793cd..45ecac2 100644 --- a/src/renderer/src/components/SettingsView.tsx +++ b/src/renderer/src/components/SettingsView.tsx @@ -55,7 +55,8 @@ import { } from '@shared/ipc' import { useSettings } from '../store/settings' import { useSystemTheme } from '../store/systemTheme' -import { QUALITY_OPTIONS, useDownloads } from '../store/downloads' +import { useDownloads } from '../store/downloads' +import { QUALITY_OPTIONS } from '../qualityOptions' import { useTemplates } from '../store/templates' import { useErrorLog } from '../store/errorlog' import { Select } from './Select' diff --git a/src/renderer/src/qualityOptions.ts b/src/renderer/src/qualityOptions.ts new file mode 100644 index 0000000..c4cdd85 --- /dev/null +++ b/src/renderer/src/qualityOptions.ts @@ -0,0 +1,6 @@ +import { VIDEO_QUALITY_OPTIONS, AUDIO_QUALITY_OPTIONS } from '@shared/ipc' + +export const QUALITY_OPTIONS = { + video: VIDEO_QUALITY_OPTIONS, + audio: AUDIO_QUALITY_OPTIONS +} as const diff --git a/src/renderer/src/store/downloads.ts b/src/renderer/src/store/downloads.ts index 4c34412..0085744 100644 --- a/src/renderer/src/store/downloads.ts +++ b/src/renderer/src/store/downloads.ts @@ -1,7 +1,5 @@ import { create } from 'zustand' import { - VIDEO_QUALITY_OPTIONS, - AUDIO_QUALITY_OPTIONS, type DownloadEvent, type DownloadMeta, type DownloadOptions, @@ -71,13 +69,6 @@ export interface DownloadItem { mediaItemId?: string } -// `satisfies` (not an annotation) so the tuple element types survive: under -// noUncheckedIndexedAccess a `readonly string[]` index yields `string | undefined`, -// but the const tuples keep `QUALITY_OPTIONS[kind][0]` known-defined (L168). -export const QUALITY_OPTIONS = { - video: VIDEO_QUALITY_OPTIONS, - audio: AUDIO_QUALITY_OPTIONS -} satisfies Record // Placeholder channel shown while metadata is still being fetched. Tracked as a // constant so the meta/error/done handlers can recognise and clear it instead of