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
+58
View File
@@ -0,0 +1,58 @@
import { app } from 'electron'
import { join } from 'path'
import { readFileSync, writeFileSync, existsSync } from 'fs'
import type { CommandTemplate } from '@shared/ipc'
// Plain JSON in userData, same shape as history.ts.
const MAX_TEMPLATES = 100
function templatesFile(): string {
return join(app.getPath('userData'), 'templates.json')
}
export function listTemplates(): CommandTemplate[] {
try {
if (!existsSync(templatesFile())) return []
const data = JSON.parse(readFileSync(templatesFile(), 'utf8'))
return Array.isArray(data) ? (data as CommandTemplate[]) : []
} catch {
return []
}
}
function save(templates: CommandTemplate[]): void {
try {
writeFileSync(templatesFile(), JSON.stringify(templates.slice(0, MAX_TEMPLATES), null, 2))
} catch {
/* best-effort; a read-only data dir just means no persisted templates */
}
}
function sanitize(t: CommandTemplate): CommandTemplate {
return {
id: String(t.id),
name: (t.name ?? '').trim() || 'Untitled command',
args: (t.args ?? '').trim()
}
}
/** Add a new template, or update an existing one (matched by id). */
export function saveTemplate(template: CommandTemplate): CommandTemplate[] {
const clean = sanitize(template)
const templates = [clean, ...listTemplates().filter((t) => t.id !== clean.id)]
save(templates)
return templates
}
export function removeTemplate(id: string): CommandTemplate[] {
const templates = listTemplates().filter((t) => t.id !== id)
save(templates)
return templates
}
/** Replace the entire template list (backup restore) rather than merge-by-id. */
export function replaceTemplates(templates: CommandTemplate[]): CommandTemplate[] {
const clean = templates.map(sanitize)
save(clean)
return clean
}