/** * Electron app 模块:开机启动、跳转列表、关于面板、日志路径、角标、优雅重启、指标等。 * 文档:https://www.electronjs.org/docs/latest/api/app */ import { app, BrowserWindow, ipcMain, shell } from 'electron' import fs from 'fs' import path from 'path' const VALID_NAV = new Set(['desk', 'record', 'network', 'proxy', 'system', 'wechat']) export function parseNavFromArgv(argv: string[]): string | null { for (const a of argv) { const m = /^--nav=(.+)$/.exec(a) if (!m) continue const id = m[1].trim() if (VALID_NAV.has(id)) return id } return null } /** 须在 app.ready 之前调用(Windows Toast / 任务栏分组) */ export function setAppUserModelIdEarly(): void { if (process.platform !== 'win32') return try { app.setAppUserModelId('com.douyin.recorder.console') } catch { /* ignore */ } } function readPackageJsonField(field: 'description' | 'version'): string { try { const candidates = [ path.join(app.getAppPath(), 'package.json'), path.join(process.cwd(), 'package.json'), ] for (const p of candidates) { if (fs.existsSync(p)) { const j = JSON.parse(fs.readFileSync(p, 'utf-8')) as Record const v = j[field] if (typeof v === 'string') return v } } } catch { /* ignore */ } return '' } /** ready 后立即调用 */ export function applyAppHostBootstrap(getBackendRoot: () => string): void { try { const logDir = path.join(app.getPath('userData'), 'logs') app.setAppLogsPath(logDir) } catch { /* ignore */ } const ver = app.getVersion() const desc = readPackageJsonField('description') || '直播录制与转播控制台' app.setAboutPanelOptions({ applicationName: app.getName(), applicationVersion: ver, copyright: `Copyright © ${new Date().getFullYear()}`, credits: desc, }) if (process.env.WEB2_SECURE_DNS === '1') { try { app.configureHostResolver({ secureDnsMode: 'automatic' }) } catch { /* ignore */ } } const proxy = process.env.WEB2_ELECTRON_GLOBAL_PROXY?.trim() if (proxy) { try { void app.setProxy({ proxyRules: proxy }) } catch { /* ignore */ } } if (process.platform === 'win32') { try { app.setUserTasks([ { program: process.execPath, arguments: '--nav=desk', title: '工作台', description: '打开工作台仪表盘', iconPath: process.execPath, iconIndex: 0, }, { program: process.execPath, arguments: '--nav=record', title: '录制', description: '打开录制与转播控制', iconPath: process.execPath, iconIndex: 0, }, { program: process.execPath, arguments: '--nav=network', title: '网络', description: '网络与远程绑定', iconPath: process.execPath, iconIndex: 0, }, ]) } catch { /* ignore */ } } } export function registerAppHostIpcHandlers(getBackendRoot: () => string): void { ipcMain.removeHandler('app:get-login-item') ipcMain.handle('app:get-login-item', () => { try { return app.getLoginItemSettings() } catch { return { openAtLogin: false } } }) ipcMain.removeHandler('app:set-login-item') ipcMain.handle('app:set-login-item', (_evt, openAtLogin: unknown) => { if (typeof openAtLogin !== 'boolean') return { ok: false as const, error: '参数无效' } try { app.setLoginItemSettings({ openAtLogin, path: process.execPath, args: [], }) return { ok: true as const } } catch (e) { return { ok: false as const, error: e instanceof Error ? e.message : String(e) } } }) ipcMain.removeHandler('app:relaunch') ipcMain.handle('app:relaunch', () => { app.relaunch() app.exit(0) }) ipcMain.removeHandler('app:get-metrics') ipcMain.handle('app:get-metrics', () => { try { return app.getAppMetrics() } catch { return [] } }) ipcMain.removeHandler('app:show-about') ipcMain.handle('app:show-about', () => { try { app.showAboutPanel() } catch { /* ignore */ } }) ipcMain.removeHandler('app:set-badge') ipcMain.handle('app:set-badge', (_evt, count: unknown) => { const n = typeof count === 'number' && Number.isFinite(count) ? Math.max(0, Math.floor(count)) : 0 try { if (process.platform === 'darwin' || process.platform === 'linux') { app.setBadgeCount(n) } } catch { /* Windows 无 dock 角标,忽略 */ } }) ipcMain.removeHandler('app:get-paths') ipcMain.handle('app:get-paths', () => { const br = getBackendRoot() return { userData: app.getPath('userData'), logs: app.getPath('logs'), temp: app.getPath('temp'), backendRoot: br, backendConfig: path.join(br, 'config'), } }) ipcMain.removeHandler('app:add-recent-config') ipcMain.handle('app:add-recent-config', () => { const cfg = path.join(getBackendRoot(), 'config') try { if (fs.existsSync(cfg)) { app.addRecentDocument(cfg) } } catch { /* ignore */ } }) ipcMain.removeHandler('app:open-logs-folder') ipcMain.handle('app:open-logs-folder', async () => { const p = app.getPath('logs') try { await fs.promises.mkdir(p, { recursive: true }) } catch { /* ignore */ } const err = await shell.openPath(p) if (err) throw new Error(err) }) } export function sendNavToWindow(win: BrowserWindow, nav: string): void { if (!VALID_NAV.has(nav)) return try { win.webContents.send('app:nav', nav) } catch { /* ignore */ } }