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>
103 lines
3.3 KiB
TypeScript
103 lines
3.3 KiB
TypeScript
import { create } from 'zustand'
|
|
import { HISTORY_MAX_ENTRIES, type HistoryEntry } from '@shared/ipc'
|
|
import { isPreview as PREVIEW } from '../isPreview'
|
|
import { logError } from '../reportError'
|
|
|
|
// Mock history so the History tab has content during UI design.
|
|
const seed: HistoryEntry[] = PREVIEW
|
|
? [
|
|
{
|
|
id: 'h1',
|
|
title: 'How yt-dlp Works Under the Hood',
|
|
channel: 'Open Source Weekly',
|
|
url: 'https://youtube.com/watch?v=zzz9999',
|
|
kind: 'video',
|
|
quality: '720p · mp4 · 184 MB',
|
|
sizeLabel: '184 MB',
|
|
filePath: 'C:\\Users\\you\\Downloads\\How yt-dlp Works Under the Hood.mp4',
|
|
completedAt: Date.now() - 1000 * 60 * 42
|
|
},
|
|
{
|
|
id: 'h2',
|
|
title: 'Deep Focus — Ambient Mix',
|
|
channel: 'ChillStudio',
|
|
url: 'https://youtube.com/watch?v=aaa1111',
|
|
kind: 'audio',
|
|
quality: 'Best',
|
|
sizeLabel: '142 MB',
|
|
filePath: 'C:\\Users\\you\\Downloads\\Deep Focus — Ambient Mix.mp3',
|
|
completedAt: Date.now() - 1000 * 60 * 60 * 26
|
|
},
|
|
{
|
|
id: 'h3',
|
|
title: 'Conference Keynote 2026 (Full)',
|
|
channel: 'TechConf',
|
|
url: 'https://youtube.com/watch?v=bbb2222',
|
|
kind: 'video',
|
|
quality: '1080p · mp4 · 1.2 GB',
|
|
sizeLabel: '1.2 GB',
|
|
filePath: 'C:\\Users\\you\\Downloads\\Conference Keynote 2026 (Full).mp4',
|
|
completedAt: Date.now() - 1000 * 60 * 60 * 24 * 3
|
|
}
|
|
]
|
|
: []
|
|
|
|
// Cap the optimistic in-memory list at the same shared limit main persists to
|
|
// (HISTORY_MAX_ENTRIES) so a long session can't grow it past what a reload would
|
|
// show (L141). De-duping by url as well as id keeps it consistent with main's add
|
|
// (M35), so the optimistic list matches the reload.
|
|
interface HistoryState {
|
|
entries: HistoryEntry[]
|
|
add: (entry: HistoryEntry) => void
|
|
remove: (id: string) => void
|
|
removeMany: (ids: string[]) => void
|
|
clear: () => void
|
|
openFile: (id: string) => void
|
|
showInFolder: (id: string) => void
|
|
}
|
|
|
|
export const useHistory = create<HistoryState>((set, get) => ({
|
|
entries: seed,
|
|
|
|
add: (entry) => {
|
|
set((s) => ({
|
|
entries: [entry, ...s.entries.filter((e) => e.id !== entry.id && e.url !== entry.url)].slice(
|
|
0,
|
|
HISTORY_MAX_ENTRIES
|
|
)
|
|
}))
|
|
if (!PREVIEW) window.api.addHistory(entry).catch(logError('addHistory'))
|
|
},
|
|
|
|
remove: (id) => {
|
|
set((s) => ({ entries: s.entries.filter((e) => e.id !== id) }))
|
|
if (!PREVIEW) window.api.removeHistory(id).catch(logError('removeHistory'))
|
|
},
|
|
|
|
removeMany: (ids) => {
|
|
const remove = new Set(ids)
|
|
set((s) => ({ entries: s.entries.filter((e) => !remove.has(e.id)) }))
|
|
if (!PREVIEW) window.api.removeManyHistory(ids).catch(logError('removeManyHistory'))
|
|
},
|
|
|
|
clear: () => {
|
|
set({ entries: [] })
|
|
if (!PREVIEW) window.api.clearHistory().catch(logError('clearHistory'))
|
|
},
|
|
|
|
openFile: (id) => {
|
|
const e = get().entries.find((x) => x.id === id)
|
|
if (!PREVIEW && e?.filePath) window.api.openPath(e.filePath)
|
|
},
|
|
|
|
showInFolder: (id) => {
|
|
const e = get().entries.find((x) => x.id === id)
|
|
if (!PREVIEW && e?.filePath) window.api.showInFolder(e.filePath)
|
|
}
|
|
}))
|
|
|
|
// Load persisted history on startup.
|
|
if (!PREVIEW) {
|
|
window.api.listHistory().then((entries) => useHistory.setState({ entries }))
|
|
}
|