Files
AeroFetch/src/renderer/src/store/settings.ts
T
debont80 7e3e5af52d Fix C1/C2 + reliability cluster (L140/L141/L148, R6/R7)
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>
2026-06-30 20:26:18 -04:00

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 }))
}