feat(audit): Batch 23 — settings key renames with migration (CC1) + config.ts (CC11)

CC1: customCommandEnabled→enableCustomCommands, downloadArchive→
useDownloadArchive, clipboardWatch→watchClipboard, sidebarCollapsed→
isSidebarCollapsed, hardwareAcceleration→useHardwareAcceleration,
videoDir/audioDir→videoFolder/audioFolder (+ chooseDir/clearDir store
actions → chooseFolder/clearFolder). Rename map in settingsMigration.ts
(pure, unit-tested incl. pre-rename backup fixture), applied as an
idempotent on-disk shim at store open and on backup import so old
settings.json and old backups both keep working.

CC11: update host/owner/repo + release API moved to src/main/config.ts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-02 12:34:05 -04:00
parent 545cfeed5e
commit 134c6a167c
25 changed files with 288 additions and 135 deletions
+29 -11
View File
@@ -1,6 +1,7 @@
import { app, safeStorage } from 'electron'
import Store from 'electron-store'
import { isSafeFilenameTemplate, isSafeOutputDir } from './validation'
import { RENAMED_SETTINGS_KEYS } from './settingsMigration'
import { logger } from './logger'
import {
AUDIO_FORMATS,
@@ -91,11 +92,28 @@ function sanitizeOptions(input: unknown): DownloadOptions {
}
}
// Constructed lazily — electron-store needs app paths, which exist only after
// the app is ready (all callers run post-ready).
// Constructed lazily — electron-store needs app paths. (`userData` resolves
// pre-ready too, which the W15 hardware-acceleration gate in index.ts relies on.)
let store: Store<Settings> | null = null
function getStore(): Store<Settings> {
if (!store) store = new Store<Settings>({ name: 'settings', defaults: DEFAULTS })
if (!store) {
store = new Store<Settings>({ name: 'settings', defaults: DEFAULTS })
// CC1 key-rename migration: move any legacy key still on disk to its new
// name, once. `has()` reports defaults as present, so probe the raw store
// object instead; idempotent because the old key is deleted afterwards.
const raw = store.store as unknown as Record<string, unknown>
const legacy = RENAMED_SETTINGS_KEYS.filter(([oldKey]) => oldKey in raw)
if (legacy.length > 0) {
const untyped = store as unknown as {
set(k: string, v: unknown): void
delete(k: string): void
}
for (const [oldKey, newKey] of legacy) {
untyped.set(newKey, raw[oldKey])
untyped.delete(oldKey)
}
}
}
return store
}
@@ -185,7 +203,7 @@ export function getSettings(): Settings {
// `set`, so only write when something actually changed — otherwise this churns
// the settings file on every read. (audit P1)
const cur = s.store
// videoDir/audioDir are intentionally left blank by default — an empty value
// videoFolder/audioFolder are intentionally left blank by default — an empty value
// routes that kind into Documents\Video / Documents\Audio (see buildCommand).
// Only an explicit user choice (Settings → folders) overrides that.
// Migrate settings files that predate downloadOptions (or hold a partial one),
@@ -278,18 +296,18 @@ function applySettings(s: Store<Settings>, partial: Partial<Settings>): void {
// well-formed 24h HH:MM is ever stored (L51).
if (isValidSyncTime(value)) s.set('syncTime', value)
break
case 'clipboardWatch':
case 'watchClipboard':
case 'useAria2c':
case 'restrictFilenames':
case 'downloadArchive':
case 'useDownloadArchive':
case 'autoUpdateYtdlp':
case 'customCommandEnabled':
case 'enableCustomCommands':
case 'notifyOnComplete':
case 'autoDownloadNew':
case 'hasCompletedOnboarding':
case 'minimizeToTray':
case 'sidebarCollapsed':
case 'hardwareAcceleration':
case 'isSidebarCollapsed':
case 'useHardwareAcceleration':
if (typeof value === 'boolean') s.set(key, value)
break
case 'launchAtStartup':
@@ -325,8 +343,8 @@ function applySettings(s: Store<Settings>, partial: Partial<Settings>): void {
// group (it merges field changes locally before calling setSettings).
s.set('downloadOptions', sanitizeOptions(value))
break
case 'videoDir':
case 'audioDir':
case 'videoFolder':
case 'audioFolder':
// An empty string is allowed — it clears the override and restores the
// Documents\Video / Documents\Audio default for that kind.
if (typeof value === 'string' && (value.trim() === '' || isSafeOutputDir(value.trim()))) {