import { app, BrowserWindow, dialog, ipcMain, session, shell, } from 'electron' import type { DownloadItem, Event as ElectronEvent, IpcMainInvokeEvent, WebContents, } from 'electron' import { existsSync } from 'node:fs' import path from 'node:path' import type { ProcessOptions } from '../../src/shared/types' import { IMAGE_INPUT_EXTENSIONS, ImageProcessor } from './image-processor' import { LicenseManager } from './license-manager' app.setName('正版情趣王') app.enableSandbox() const singleInstance = app.requestSingleInstanceLock() if (!singleInstance) { app.quit() } let mainWindow: BrowserWindow | null = null const imageProcessor = new ImageProcessor() const licenseManager = new LicenseManager() const UPDATE_INSTALLER_EXTENSIONS = new Set(['.exe', '.msi']) function createWindow(): void { const preload = path.join(__dirname, '../preload/index.js') const windowOptions: Electron.BrowserWindowConstructorOptions = { width: 1360, height: 860, minWidth: 1120, minHeight: 720, show: false, backgroundColor: '#F4F7FB', title: '正版情趣王', titleBarStyle: 'hidden', webPreferences: { preload, contextIsolation: true, nodeIntegration: false, sandbox: true, webSecurity: true, }, } if (process.platform !== 'darwin') { windowOptions.titleBarOverlay = { color: '#F7F9FC', symbolColor: '#596273', height: 48, } } mainWindow = new BrowserWindow(windowOptions) mainWindow.webContents.setWindowOpenHandler(() => ({ action: 'deny' })) mainWindow.webContents.on('will-navigate', (event, targetUrl) => { if (targetUrl !== mainWindow?.webContents.getURL()) { event.preventDefault() } }) mainWindow.once('ready-to-show', () => mainWindow?.show()) const devServerUrl = process.env.VITE_DEV_SERVER_URL if (devServerUrl) { void mainWindow.loadURL(devServerUrl) } else { void mainWindow.loadFile(path.join(__dirname, '../../../dist/index.html')) } } function registerIpcHandlers(): void { ipcMain.handle('app:info', (event) => { assertTrustedSender(event) return licenseManager.getAppInfo() }) ipcMain.handle('license:state', (event) => { assertTrustedSender(event) return licenseManager.getState() }) ipcMain.handle('license:activate', (event, activationCode: unknown) => { assertTrustedSender(event) if (typeof activationCode !== 'string' || activationCode.length > 64) { throw new Error('激活码格式不正确') } return licenseManager.activate(activationCode) }) ipcMain.handle('license:validate', (event) => { assertTrustedSender(event) return licenseManager.validate() }) ipcMain.handle('license:clear', (event) => { assertTrustedSender(event) return licenseManager.clear() }) ipcMain.handle('license:check-version', (event) => { assertTrustedSender(event) return licenseManager.checkVersion() }) ipcMain.handle('license:download-update', async ( event, downloadUrl: unknown, latestVersion: unknown, ) => { assertTrustedSender(event) const url = validateUpdateUrl(downloadUrl) const savePath = await downloadUpdateInstaller(event.sender, url, latestVersion) const choice = await dialog.showMessageBox(mainWindow!, { type: 'info', title: '更新已下载', message: '新版安装包下载完成', detail: '安装包已保存到:\n' + savePath + '\n\n点击“立即安装”继续升级,安装程序可能会提示关闭当前软件。', buttons: ['立即安装', '打开文件夹', '稍后安装'], defaultId: 0, cancelId: 2, noLink: true, }) if (choice.response === 0) { const openError = await shell.openPath(savePath) if (openError) { throw new Error('无法启动安装程序:' + openError) } } else if (choice.response === 1) { shell.showItemInFolder(savePath) } }) ipcMain.handle('images:select-files', async (event) => { assertTrustedSender(event) const result = await dialog.showOpenDialog(mainWindow!, { title: '选择图片', properties: ['openFile', 'multiSelections'], filters: [{ name: '支持的图片', extensions: IMAGE_INPUT_EXTENSIONS }], }) return result.canceled ? [] : result.filePaths }) ipcMain.handle('images:select-folder', async (event) => { assertTrustedSender(event) const result = await dialog.showOpenDialog(mainWindow!, { title: '选择图片文件夹', properties: ['openDirectory'], }) return result.canceled ? [] : result.filePaths }) ipcMain.handle('images:select-output', async (event) => { assertTrustedSender(event) const result = await dialog.showOpenDialog(mainWindow!, { title: '选择输出目录', properties: ['openDirectory', 'createDirectory'], }) return result.canceled ? null : result.filePaths[0] }) ipcMain.handle('images:inspect', (event, paths: unknown) => { assertTrustedSender(event) return imageProcessor.inspect(validatePaths(paths)) }) ipcMain.handle('images:process', (event, options: ProcessOptions) => { assertTrustedSender(event) const validated: ProcessOptions = { ...options, paths: validatePaths(options?.paths), } return imageProcessor.process(validated, (progress) => { if (!event.sender.isDestroyed()) { event.sender.send('images:progress', progress) } }) }) ipcMain.handle('images:cancel', (event) => { assertTrustedSender(event) imageProcessor.cancel() }) ipcMain.handle('images:reveal', (event, filePath: unknown) => { assertTrustedSender(event) if (typeof filePath !== 'string' || !path.isAbsolute(filePath)) { throw new Error('文件路径无效') } shell.showItemInFolder(filePath) }) } function validateUpdateUrl(value: unknown): URL { if (typeof value !== 'string') { throw new Error('下载地址无效') } const url = new URL(value) if (url.protocol !== 'https:') { throw new Error('安装包必须通过 HTTPS 下载') } const extension = path.extname(decodeUrlPathname(url)).toLowerCase() if (!UPDATE_INSTALLER_EXTENSIONS.has(extension)) { throw new Error('下载地址不是受支持的 Windows 安装包') } return url } function decodeUrlPathname(url: URL): string { try { return decodeURIComponent(url.pathname) } catch { throw new Error('安装包下载地址格式不正确') } } function installerFilename(url: URL, latestVersion: unknown): string { const extension = path.extname(decodeUrlPathname(url)).toLowerCase() const version = typeof latestVersion === 'string' ? latestVersion.trim().replace(/^v/i, '').replace(/[^0-9A-Za-z._-]/g, '').slice(0, 32) : '' return version ? '正版情趣王-Setup-' + version + extension : '正版情趣王-Setup' + extension } function availableDownloadPath(filename: string): string { const downloadsDirectory = app.getPath('downloads') const extension = path.extname(filename) const basename = path.basename(filename, extension) let candidate = path.join(downloadsDirectory, filename) for (let index = 2; existsSync(candidate); index += 1) { candidate = path.join(downloadsDirectory, basename + '-' + index + extension) } return candidate } function downloadUpdateInstaller( sender: WebContents, url: URL, latestVersion: unknown, ): Promise { const savePath = availableDownloadPath(installerFilename(url, latestVersion)) const browserSession = sender.session return new Promise((resolve, reject) => { let downloadItem: DownloadItem | null = null let settled = false const cleanup = (): void => { clearTimeout(timeout) browserSession.removeListener('will-download', handleDownload) } const finish = (error?: Error): void => { if (settled) return settled = true cleanup() if (error) reject(error) else resolve(savePath) } const handleDownload = ( _event: ElectronEvent, item: DownloadItem, source: WebContents, ): void => { if (source !== sender) return downloadItem = item browserSession.removeListener('will-download', handleDownload) item.setSavePath(savePath) item.once('done', (_doneEvent, state) => { if (state === 'completed') { finish() } else { finish(new Error(state === 'cancelled' ? '安装包下载已取消' : '安装包下载中断,请重试')) } }) } const timeout = setTimeout(() => { downloadItem?.cancel() finish(new Error('安装包下载超时,请检查网络后重试')) }, 30 * 60 * 1000) browserSession.on('will-download', handleDownload) try { // 由 Electron 直接下载,避免依赖 Windows 的 HTTPS 默认应用关联。 sender.downloadURL(url.toString()) } catch (error) { finish(error instanceof Error ? error : new Error('无法开始下载安装包')) } }) } function assertTrustedSender(event: IpcMainInvokeEvent): void { const senderUrl = event.senderFrame?.url || '' if (senderUrl.startsWith('file://')) { return } if (!app.isPackaged && senderUrl.startsWith('http://127.0.0.1:5173')) { return } throw new Error('拒绝来自非可信页面的请求') } function validatePaths(value: unknown): string[] { if (!Array.isArray(value) || value.length > 10_000) { throw new Error('文件列表无效') } return value.map((item) => { if (typeof item !== 'string' || !path.isAbsolute(item) || item.length > 4096) { throw new Error('文件路径无效') } return item }) } if (singleInstance) { app.whenReady().then(() => { app.setAppUserModelId('com.jxxyk.genuineqingquwang') session.defaultSession.setPermissionRequestHandler((_webContents, _permission, callback) => { callback(false) }) registerIpcHandlers() createWindow() }) app.on('second-instance', () => { if (mainWindow) { if (mainWindow.isMinimized()) mainWindow.restore() mainWindow.focus() } }) app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow() }) app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit() }) }