Complete Phase C (custom commands & yt-dlp updater) and Phase D (library, UX & notifications)

Phase C had been implemented in the working tree but never committed:
named custom-command templates (CRUD + per-download override), command
preview, and the in-app yt-dlp self-updater.

Phase D adds: history search/kind-filter/multi-select/bulk-delete/
re-download; native OS notifications on completion/failure; a private/
incognito download mode that skips history; settings+template backup/
restore via JSON; and a persistent error log surfaced as a Diagnostics
report in Settings.

See ROADMAP.md for the full per-item breakdown.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 06:19:45 -04:00
parent 67133f13b5
commit a822a2bb52
24 changed files with 1678 additions and 129 deletions
+57
View File
@@ -0,0 +1,57 @@
import { dialog, type BrowserWindow } from 'electron'
import { readFileSync, writeFileSync } from 'fs'
import { getSettings, setSettings } from './settings'
import { listTemplates, replaceTemplates } from './templates'
import type {
BackupExportResult,
BackupImportResult,
Settings,
CommandTemplate
} from '@shared/ipc'
interface BackupFile {
version: 1
settings: Settings
templates: CommandTemplate[]
}
export async function exportBackup(win: BrowserWindow | undefined): Promise<BackupExportResult> {
const res = await dialog.showSaveDialog(win!, {
defaultPath: 'aerofetch-backup.json',
filters: [{ name: 'JSON', extensions: ['json'] }]
})
if (res.canceled || !res.filePath) return { ok: false }
const payload: BackupFile = { version: 1, settings: getSettings(), templates: listTemplates() }
try {
writeFileSync(res.filePath, JSON.stringify(payload, null, 2))
return { ok: true, path: res.filePath }
} catch (e) {
return { ok: false, error: (e as Error).message }
}
}
export async function importBackup(win: BrowserWindow | undefined): Promise<BackupImportResult> {
const res = await dialog.showOpenDialog(win!, {
properties: ['openFile'],
filters: [{ name: 'JSON', extensions: ['json'] }]
})
if (res.canceled || !res.filePaths[0]) return { ok: false }
let parsed: unknown
try {
parsed = JSON.parse(readFileSync(res.filePaths[0], 'utf8'))
} catch (e) {
return { ok: false, error: `Could not read backup file: ${(e as Error).message}` }
}
if (!parsed || typeof parsed !== 'object') {
return { ok: false, error: 'Not a valid AeroFetch backup file.' }
}
// setSettings/replaceTemplates validate and sanitize every field, so a
// hand-edited or partial backup file degrades gracefully rather than
// corrupting the store.
const file = parsed as Partial<BackupFile>
if (file.settings && typeof file.settings === 'object') setSettings(file.settings)
if (Array.isArray(file.templates)) replaceTemplates(file.templates)
return { ok: true }
}