7e3e5af52d
Critical: - C1: single source of truth for IPC mock + Settings defaults. DEFAULT_SETTINGS in @shared/ipc; main DEFAULTS, renderer FALLBACK, and preview MOCK_SETTINGS all derive from it. Whole browser mock collapsed into one typed mockApi.ts; main.tsx shrinks to window.api = mockApi. PREVIEW check single-sourced in isPreview.ts. - C2: break the downloads<->sources circular import via a typed event bus (store/coordinator.ts). App eagerly imports sources for side-effects so startup load + scheduled --sync + downloadCompleted subscription still run. Reliability: - L140/L148: cancel/pause release the active slot synchronously; identity-guarded releaseActive; per-spawn cookie jar so a same-id retry/resume is never rejected by a stale entry nor run over the concurrency cap. - L141: renderer history capped at 500 + url de-dup to match main. - R6: writeJsonAtomic reports/logs write failures and keeps data dirty for retry instead of an empty catch. - R7: encryptSecret warns before the plaintext fallback. typecheck + 243 tests + eslint green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
71 lines
2.6 KiB
TypeScript
71 lines
2.6 KiB
TypeScript
import { create } from 'zustand'
|
|
import { DEFAULT_SETTINGS, type Settings } from '@shared/ipc'
|
|
import { isPreview as PREVIEW } from '../isPreview'
|
|
import { logError } from '../reportError'
|
|
|
|
// The pre-load fallback is the canonical DEFAULT_SETTINGS (single-sourced from the
|
|
// shared contract, C1) — including theme:'system' (SR1) so there's no white-on-
|
|
// first-paint flash for a dark-mode user before the real settings arrive. In the
|
|
// browser preview a couple of fields differ: placeholder folder paths, and
|
|
// onboarding pre-dismissed so design work isn't blocked behind the welcome screen
|
|
// (a real first launch keeps DEFAULT_SETTINGS' false from main).
|
|
const FALLBACK: Settings = PREVIEW
|
|
? {
|
|
...DEFAULT_SETTINGS,
|
|
videoDir: 'C:\\Users\\you\\Documents\\Video',
|
|
audioDir: 'C:\\Users\\you\\Documents\\Audio',
|
|
hasCompletedOnboarding: true
|
|
}
|
|
: DEFAULT_SETTINGS
|
|
|
|
interface SettingsState extends Settings {
|
|
/** true once persisted settings have loaded (always true in preview) */
|
|
loaded: boolean
|
|
update: (partial: Partial<Settings>) => void
|
|
/** open the OS folder picker and store the result as the video or audio folder */
|
|
chooseDir: (target: 'videoDir' | 'audioDir') => void
|
|
/** clear a per-kind folder override, restoring its Documents\… default */
|
|
clearDir: (target: 'videoDir' | 'audioDir') => void
|
|
}
|
|
|
|
export const useSettings = create<SettingsState>((set, get) => ({
|
|
...FALLBACK,
|
|
loaded: PREVIEW,
|
|
|
|
update: (partial) => {
|
|
set(partial) // optimistic local update
|
|
if (PREVIEW) return
|
|
// M34: reconcile with main's authoritative, validated result — for exactly the
|
|
// keys we changed — so a value main clamps, sanitizes, or rejects (e.g. an
|
|
// unsafe filenameTemplate, an out-of-range maxConcurrent, a malformed
|
|
// rateLimit) stops showing as accepted in the UI until restart.
|
|
window.api
|
|
.setSettings(partial)
|
|
.then((saved) => {
|
|
const reconciled = Object.fromEntries(
|
|
(Object.keys(partial) as (keyof Settings)[]).map((k) => [k, saved[k]])
|
|
) as Partial<Settings>
|
|
set(reconciled)
|
|
})
|
|
.catch(logError('setSettings'))
|
|
},
|
|
|
|
chooseDir: (target) => {
|
|
if (PREVIEW) return
|
|
// Seed the OS picker with the folder this target currently points at (W5).
|
|
window.api
|
|
.chooseFolder(get()[target])
|
|
.then((dir) => {
|
|
if (dir) get().update({ [target]: dir })
|
|
})
|
|
.catch(logError('chooseFolder'))
|
|
},
|
|
|
|
clearDir: (target) => get().update({ [target]: '' })
|
|
}))
|
|
|
|
// Load persisted settings on startup.
|
|
if (!PREVIEW) {
|
|
window.api.getSettings().then((s) => useSettings.setState({ ...s, loaded: true }))
|
|
}
|