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 <noreply@anthropic.com>
This commit is contained in:
+2
-2
@@ -436,7 +436,7 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
|
|||||||
runs its primary action). Inconsistent.
|
runs its primary action). Inconsistent.
|
||||||
- [x] **L28 — Default kind/quality applied once.** DownloadBar seeds from settings via a one-shot
|
- [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.
|
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.
|
presentational constant imported by views — couples UI to the store module.
|
||||||
- [x] **L30 — Redundant `loading="lazy"`** on `MediaThumb` images already inside a virtualized
|
- [x] **L30 — Redundant `loading="lazy"`** on `MediaThumb` images already inside a virtualized
|
||||||
list (the virtualizer only mounts visible rows).
|
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
|
- [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.
|
- [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
|
- [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.
|
||||||
- [x] **L54 — DownloadBar preview `<img>` has no `onError` fallback** (inconsistent with `MediaThumb`'s retry).
|
- [x] **L54 — DownloadBar preview `<img>` has no `onError` fallback** (inconsistent with `MediaThumb`'s retry).
|
||||||
|
|||||||
+9
-8
@@ -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 { createWriteStream, type WriteStream } from 'fs'
|
||||||
import { stat, unlink } from 'fs/promises'
|
import { stat, unlink } from 'fs/promises'
|
||||||
import { join, normalize, dirname } from 'path'
|
import { join, normalize, dirname } from 'path'
|
||||||
@@ -413,9 +414,6 @@ export async function downloadAppUpdate(url: string, wc: WebContents): Promise<A
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Let the launched installer spawn before we quit to release our files (ms). */
|
|
||||||
const INSTALLER_HANDOFF_MS = 1500
|
|
||||||
|
|
||||||
/** Launch a freshly-downloaded installer, then quit so it can replace the app. */
|
/** Launch a freshly-downloaded installer, then quit so it can replace the app. */
|
||||||
export async function runAppUpdate(filePath: string): Promise<{ ok: boolean; error?: string }> {
|
export async function runAppUpdate(filePath: string): Promise<{ ok: boolean; error?: string }> {
|
||||||
try {
|
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.' }
|
return { ok: false, error: 'Refused to run an unexpected file.' }
|
||||||
}
|
}
|
||||||
await stat(target) // throws if the file is missing
|
await stat(target) // throws if the file is missing
|
||||||
const err = await shell.openPath(target) // hand the installer to the OS
|
// Spawn the installer detached + unref'd so it outlives our process even on a
|
||||||
if (err) return { ok: false, error: err }
|
// slow machine. shell.openPath goes through ShellExecute but gives us no child
|
||||||
// Give the installer a beat to spawn, then quit so it can overwrite our files.
|
// PID to wait on; spawning directly lets us call unref() before we quit.
|
||||||
setTimeout(() => app.quit(), INSTALLER_HANDOFF_MS)
|
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 }
|
return { ok: true }
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return { ok: false, error: e instanceof Error ? e.message : String(e) }
|
return { ok: false, error: e instanceof Error ? e.message : String(e) }
|
||||||
|
|||||||
@@ -28,7 +28,8 @@ import {
|
|||||||
WarningRegular
|
WarningRegular
|
||||||
} from '@fluentui/react-icons'
|
} from '@fluentui/react-icons'
|
||||||
import type { MediaInfo, FormatOption, PlaylistInfo } from '@shared/ipc'
|
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 { sameVideo } from '../store/queueStats'
|
||||||
import { useSettings } from '../store/settings'
|
import { useSettings } from '../store/settings'
|
||||||
import { Select } from './Select'
|
import { Select } from './Select'
|
||||||
|
|||||||
@@ -55,7 +55,8 @@ import {
|
|||||||
} from '@shared/ipc'
|
} from '@shared/ipc'
|
||||||
import { useSettings } from '../store/settings'
|
import { useSettings } from '../store/settings'
|
||||||
import { useSystemTheme } from '../store/systemTheme'
|
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 { useTemplates } from '../store/templates'
|
||||||
import { useErrorLog } from '../store/errorlog'
|
import { useErrorLog } from '../store/errorlog'
|
||||||
import { Select } from './Select'
|
import { Select } from './Select'
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
import { create } from 'zustand'
|
import { create } from 'zustand'
|
||||||
import {
|
import {
|
||||||
VIDEO_QUALITY_OPTIONS,
|
|
||||||
AUDIO_QUALITY_OPTIONS,
|
|
||||||
type DownloadEvent,
|
type DownloadEvent,
|
||||||
type DownloadMeta,
|
type DownloadMeta,
|
||||||
type DownloadOptions,
|
type DownloadOptions,
|
||||||
@@ -71,13 +69,6 @@ export interface DownloadItem {
|
|||||||
mediaItemId?: string
|
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<MediaKind, readonly string[]>
|
|
||||||
|
|
||||||
// Placeholder channel shown while metadata is still being fetched. Tracked as a
|
// 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
|
// constant so the meta/error/done handlers can recognise and clear it instead of
|
||||||
|
|||||||
Reference in New Issue
Block a user