Initial commit: AeroFetch — yt-dlp/YouTube downloader for Windows
Electron + React (Fluent UI) desktop frontend for yt-dlp: - Download queue with live progress, concurrency cap, cancel/retry - Format/quality picker via yt-dlp probe; audio extraction to MP3 - Settings + history persistence (electron-store / JSON) - Clipboard link auto-detect; persisted light/dark theme - NSIS + portable packaging (binaries bundled at build time, not in git) UI: "Studio" sidebar layout with a toffee-brown theme (light + neutral dark). Dropdowns use a native <select> and tooltips a CSS-only Hint, to avoid a dev-machine GPU overlay flicker/blank issue. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
import { app, shell, BrowserWindow, ipcMain, dialog, clipboard } from 'electron'
|
||||
import { join } from 'path'
|
||||
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
|
||||
import {
|
||||
IpcChannels,
|
||||
type StartDownloadOptions,
|
||||
type Settings,
|
||||
type HistoryEntry
|
||||
} from '@shared/ipc'
|
||||
import { getYtdlpVersion } from './ytdlp'
|
||||
import { probeMedia } from './probe'
|
||||
import { startDownload, cancelDownload } from './download'
|
||||
import { getSettings, setSettings } from './settings'
|
||||
import { listHistory, addHistory, removeHistory, clearHistory } from './history'
|
||||
import { setupPortableData } from './portable'
|
||||
|
||||
// Force software compositing (GPU acceleration off). On this dev machine's old GeForce
|
||||
// GT 625 — and plausibly on the old / locked-down public PCs this app targets —
|
||||
// Chromium's GPU compositor renders blank: under hardware acceleration the WHOLE window
|
||||
// paints white (popup overlays alone were blank earlier). Updating the NVIDIA driver to
|
||||
// the last Fermi release did not fix it. Software compositing renders correctly and
|
||||
// costs no meaningful performance for this lightweight UI, so it is the safe default for
|
||||
// this app's target hardware. The only downside is a minor overlay-repaint flicker that
|
||||
// appears on the most broken GPUs. The window backgroundColor is theme-matched (see
|
||||
// createWindow); native window-occlusion tracking is disabled (a separate documented
|
||||
// Windows blank/flicker cause). Must run before the app is ready.
|
||||
app.disableHardwareAcceleration()
|
||||
app.commandLine.appendSwitch('disable-features', 'CalculateNativeWinOcclusion')
|
||||
|
||||
// Redirect user data next to the exe when possible (portable, no-admin). Must run
|
||||
// before the app is ready and before any user path is read.
|
||||
setupPortableData()
|
||||
|
||||
// Page background per theme — keep in sync with `pageBackground` in
|
||||
// src/renderer/src/theme.ts. Set as the window's NATIVE background so the
|
||||
// one-frame compositor repaint (when a tooltip/dropdown overlay first paints on
|
||||
// Windows) shows the app's current color instead of a mismatched white flash.
|
||||
const THEME_BACKGROUND = { light: '#f6f1ef', dark: '#161618' } as const
|
||||
|
||||
function createWindow(): void {
|
||||
const mainWindow = new BrowserWindow({
|
||||
width: 920,
|
||||
height: 700,
|
||||
show: false,
|
||||
backgroundColor: THEME_BACKGROUND[getSettings().theme],
|
||||
autoHideMenuBar: true,
|
||||
webPreferences: {
|
||||
preload: join(__dirname, '../preload/index.mjs'),
|
||||
sandbox: false,
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false
|
||||
}
|
||||
})
|
||||
|
||||
mainWindow.on('ready-to-show', () => {
|
||||
mainWindow.show()
|
||||
})
|
||||
|
||||
// Open external links in the OS browser, never in-app.
|
||||
mainWindow.webContents.setWindowOpenHandler((details) => {
|
||||
shell.openExternal(details.url)
|
||||
return { action: 'deny' }
|
||||
})
|
||||
|
||||
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
|
||||
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
|
||||
} else {
|
||||
mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
|
||||
}
|
||||
}
|
||||
|
||||
function registerIpcHandlers(): void {
|
||||
ipcMain.handle(IpcChannels.ytdlpVersion, () => getYtdlpVersion())
|
||||
|
||||
ipcMain.handle(IpcChannels.probe, (_e, url: string) => probeMedia(url))
|
||||
|
||||
ipcMain.handle(IpcChannels.downloadStart, (e, opts: StartDownloadOptions) =>
|
||||
startDownload(e.sender, opts)
|
||||
)
|
||||
ipcMain.handle(IpcChannels.downloadCancel, (_e, id: string) => cancelDownload(id))
|
||||
|
||||
ipcMain.handle(IpcChannels.defaultFolder, () => app.getPath('downloads'))
|
||||
|
||||
ipcMain.handle(IpcChannels.chooseFolder, async (e) => {
|
||||
const win = BrowserWindow.fromWebContents(e.sender) ?? undefined
|
||||
const res = await dialog.showOpenDialog(win!, {
|
||||
properties: ['openDirectory', 'createDirectory']
|
||||
})
|
||||
return res.canceled || !res.filePaths[0] ? null : res.filePaths[0]
|
||||
})
|
||||
|
||||
ipcMain.handle(IpcChannels.openPath, (_e, p: string) => shell.openPath(p))
|
||||
ipcMain.handle(IpcChannels.showInFolder, (_e, p: string) => shell.showItemInFolder(p))
|
||||
|
||||
ipcMain.handle(IpcChannels.clipboardRead, () => clipboard.readText())
|
||||
|
||||
ipcMain.handle(IpcChannels.settingsGet, () => getSettings())
|
||||
ipcMain.handle(IpcChannels.settingsSet, (e, partial: Partial<Settings>) => {
|
||||
const result = setSettings(partial)
|
||||
// Keep the window's native background in sync with the theme so a compositor
|
||||
// repaint never flashes a mismatched color behind an overlay.
|
||||
if (partial.theme) {
|
||||
BrowserWindow.fromWebContents(e.sender)?.setBackgroundColor(THEME_BACKGROUND[partial.theme])
|
||||
}
|
||||
return result
|
||||
})
|
||||
|
||||
ipcMain.handle(IpcChannels.historyList, () => listHistory())
|
||||
ipcMain.handle(IpcChannels.historyAdd, (_e, entry: HistoryEntry) => addHistory(entry))
|
||||
ipcMain.handle(IpcChannels.historyRemove, (_e, id: string) => removeHistory(id))
|
||||
ipcMain.handle(IpcChannels.historyClear, () => clearHistory())
|
||||
}
|
||||
|
||||
app.whenReady().then(() => {
|
||||
electronApp.setAppUserModelId('com.aerofetch.app')
|
||||
|
||||
app.on('browser-window-created', (_, window) => {
|
||||
optimizer.watchWindowShortcuts(window)
|
||||
})
|
||||
|
||||
registerIpcHandlers()
|
||||
createWindow()
|
||||
|
||||
app.on('activate', () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) createWindow()
|
||||
})
|
||||
})
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit()
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user