Files
AeroFetch/src/main/reveal.ts
T
debont80 661a4e7572 feat(audit): Batch 11 — UX behavior gaps (toast surface, sidebar badge)
A new in-app toast surface is the batch's spine, making the silent-failure and
global-status gaps fixable:

- Toast infra: store/toasts.ts (useToasts + toast(); auto-dismiss) + ui/Toaster.tsx
  (non-portal bottom-center stack, avoids the Fluent-portal GPU flicker). Unit-
  tested (test/toasts.test.ts). Mounted at the app root beside LiveRegion.
- UX6: shared renderer reveal.ts revealFile('open'|'folder') awaits the IPC result
  and toasts the main-process reason instead of silently no-oping on a moved/typed-
  out file. safeShowInFolder upgraded to return an error string like safeOpenPath
  (preload/mock/Api types updated).
- UX9 / UI25: a Fluent CounterBadge on the sidebar Downloads nav item shows the
  active (downloading + queued) count from any tab, via a count-only App selector
  that doesn't re-render on progress ticks.
- UX15: cancelAll store action + a "Cancel all (N)" header button.
- UX24: the disabled "Check watched" button uses disabledFocusable + a Hint that
  explains it needs a watched source.
- UX12: the Terminal gate gains an inline "Enable custom commands" button, so it's
  no longer a dead-end pointing at another screen.
- L144: the DownloadBar duplicate guard now also checks history — a URL downloaded
  earlier warns "Already downloaded: …" (dupInHistory) vs "Already in your queue: …".

Deferred with rationale (CODE-AUDIT.md): UX7/UX8 (Settings IA redesign — visual),
UX18/UX26 (subjective/visual polish), L131 (arguably by-design, needs live taskbar),
L142 (history/templates/sources IPC-return reconciliation — a focused own PR).

Verified: typecheck (node+web) + 279 tests + eslint + production build green;
touched files prettier-clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-01 17:05:26 -04:00

64 lines
1.8 KiB
TypeScript

import { shell } from 'electron'
import { existsSync, statSync } from 'fs'
import { extname, isAbsolute } from 'path'
/**
* Open/reveal helpers used by the shell:* IPC handlers.
*
* The renderer supplies the path (it originates from yt-dlp's after-move print),
* but the IPC boundary must not trust it blindly: a compromised renderer could
* otherwise call openPath() on an arbitrary executable and have the OS run it.
* So openPath is confined to existing files with a known media extension —
* never .exe/.bat/.ps1/etc.
*/
const OPENABLE_EXTENSIONS = new Set([
// video
'.mp4',
'.mkv',
'.webm',
'.mov',
'.avi',
'.flv',
'.ts',
'.m4v',
'.3gp',
'.ogv',
// audio
'.mp3',
'.m4a',
'.opus',
'.ogg',
'.oga',
'.aac',
'.flac',
'.wav',
'.wma',
// subtitle sidecars (plain text — safe to open)
'.vtt',
'.srt'
])
/** Open a downloaded media file with its default app. Returns '' on success,
* or an error string (matching shell.openPath's contract). */
export async function safeOpenPath(p: unknown): Promise<string> {
if (typeof p !== 'string' || !isAbsolute(p)) return 'Invalid path.'
if (!OPENABLE_EXTENSIONS.has(extname(p).toLowerCase())) {
return 'Refusing to open this file type.'
}
try {
if (!statSync(p).isFile()) return 'Not a file.'
} catch {
return 'File not found.'
}
return shell.openPath(p)
}
/** Reveal a path in the OS file manager. Returns '' on success, or an error
* string (mirroring safeOpenPath) so the renderer can surface it (UX6). */
export function safeShowInFolder(p: unknown): string {
if (typeof p !== 'string' || !isAbsolute(p)) return 'Invalid path.'
if (!existsSync(p)) return 'File not found — it may have been moved or deleted.'
shell.showItemInFolder(p)
return ''
}