Files
AeroFetch/src/main/templates.ts
T
debont80 da37690b42 feat: Phase N power-user surface (terminal, palette, per-item, sort, url-templates)
- Built-in yt-dlp terminal: src/main/terminal.ts streams raw yt-dlp runs
  (binary fixed, gated on customCommandEnabled), new TerminalView + sidebar tab
- Command palette (Ctrl/Cmd+K), portal-free CommandPalette.tsx wired in App
- Per-playlist-item editing: per-row video/audio toggle + All video/All audio
  batch; addPlaylist enqueues via addMany with per-item kind
- Weighted format sorting: DownloadOptions.formatSort -> raw -S (overrides codec)
- URL-regex template auto-matching: CommandTemplate.urlPattern + selectExtraArgs
  matchesUrl, threaded through resolveExtraArgs; field in TemplateManager

typecheck + test (192) + electron-vite build all clean. Roadmap: Phase N COMPLETE.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 11:10:22 -04:00

69 lines
2.4 KiB
TypeScript

import { app } from 'electron'
import { join } from 'path'
import { readFileSync, writeFileSync, existsSync } from 'fs'
import type { CommandTemplate } from '@shared/ipc'
import { isTemplateLike } from './validation'
// Plain JSON in userData, same shape as history.ts.
const MAX_TEMPLATES = 100
function templatesFile(): string {
return join(app.getPath('userData'), 'templates.json')
}
// A persisted template entry must at least be an object carrying an id (see
// isTemplateLike in validation.ts); everything else is coerced by sanitize().
// Drop anything that isn't, so a hand-edited templates.json can't inject
// malformed entries. (audit S5)
export function listTemplates(): CommandTemplate[] {
try {
if (!existsSync(templatesFile())) return []
const data = JSON.parse(readFileSync(templatesFile(), 'utf8'))
// Validate shape, then normalise each surviving entry through sanitize() so
// name/args are always well-formed strings regardless of what was on disk.
return Array.isArray(data) ? data.filter(isTemplateLike).map(sanitize) : []
} 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 {
const urlPattern = typeof t.urlPattern === 'string' ? t.urlPattern.trim() : ''
return {
id: String(t.id),
name: (t.name ?? '').trim() || 'Untitled command',
args: (t.args ?? '').trim(),
// Only persist a urlPattern when one was provided, keeping older files clean.
...(urlPattern ? { urlPattern } : {})
}
}
/** 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
}