Files
tkNewPageElectron/main.js
2025-10-10 19:41:11 +08:00

229 lines
9.1 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// main.js
const { app, Tray, Menu, dialog, BrowserWindow, ipcMain, nativeTheme } = require('electron');
const { autoUpdater } = require('electron-updater');
const path = require('path');
const fs = require('fs');
const { spawn, spawnSync, exec } = require('child_process');
const log = require('electron-log');
Object.assign(console, log.functions);
const ALLOW_UNSIGNED = process.env.YOLO_ALLOW_UNSIGNED === '1';
let installTriggered = false; // NEW: 防止重复触发安装
// ===== 全局状态 =====
let tray = null;
let managedChild = null;
let quitting = false;
let updateWin = null;
let pythonStarted = false; // NEW: 已启动 tkpage
let updaterKicked = false; // NEW: 是否已触发检查
// ===== tkpage 路径 =====
function resolveExePath() {
const devExe = path.resolve(__dirname, 'resources', 'python', 'tkpage.exe');
const prodExe = path.join(process.resourcesPath, 'python', 'tkpage.exe');
return app.isPackaged ? prodExe : devExe;
}
// ===== 启动 tkpage只会启动一次 =====
function launchPythonIfNeeded() {
if (pythonStarted) return;
const exe = resolveExePath();
const cwd = path.dirname(exe);
console.info('[tkpage exe]', exe);
if (!fs.existsSync(exe)) {
dialog.showErrorBox('启动失败', `未找到可执行文件:\n${exe}`);
return;
}
pythonStarted = true;
managedChild = spawn(exe, [], {
cwd,
windowsHide: false,
detached: false,
stdio: 'ignore',
});
managedChild.on('exit', (code, signal) => {
console.info(`[tkpage] exit code=${code} signal=${signal}`);
if (!quitting) { quitting = true; app.quit(); }
});
managedChild.on('close', (code, signal) => {
console.info(`[tkpage] close code=${code} signal=${signal}`);
if (!quitting) { quitting = true; app.quit(); }
});
}
// ===== 结束 tkpage =====
function killPythonGUI(killSelf = true) {
if (managedChild && managedChild.pid) {
try { process.kill(managedChild.pid); } catch (e) {
console.warn('[kill] managed kill error:', e?.message || e);
} finally { managedChild = null; }
}
try { spawnSync('taskkill', ['/f', '/t', '/im', 'tkpage.exe'], { windowsHide: true }); } catch { }
if (killSelf) {
try { exec('taskkill /IM YOLO助手.exe /F'); } catch { }
}
}
// ===== 托盘 =====
function createTray() {
const iconPath = path.join(__dirname, 'assets', 'icon.ico');
tray = new Tray(iconPath);
const menu = Menu.buildFromTemplate([
{ label: '检查更新', click: () => startUpdateCheck(/*force*/true) },
{ type: 'separator' },
{ label: '显示更新窗口', click: () => { if (updateWin) { updateWin.show(); updateWin.focus(); } else { createUpdateWindow(); } } },
{ type: 'separator' },
{ label: '重启 Python GUI', click: () => { killPythonGUI(); pythonStarted = false; launchPythonIfNeeded(); } },
{ type: 'separator' },
{ label: '退出', click: () => { quitting = true; killPythonGUI(); app.quit(); } },
]);
tray.setToolTip('YOLO');
tray.setContextMenu(menu);
}
// ===== 更新窗口 =====
function createUpdateWindow() {
if (updateWin && !updateWin.isDestroyed()) { updateWin.show(); updateWin.focus(); return; }
updateWin = new BrowserWindow({
width: 640, height: 420, resizable: false, fullscreenable: false, maximizable: false,
autoHideMenuBar: true, show: true, center: true,
backgroundColor: nativeTheme.shouldUseDarkColors ? '#0b1220' : '#ffffff',
icon: path.join(__dirname, 'assets', 'icon.ico'),
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true, nodeIntegration: false, sandbox: true,
}
});
updateWin.on('close', (e) => { e.preventDefault(); updateWin.hide(); });
updateWin.loadFile(path.join(__dirname, 'index.html')).catch(err => console.error('[updateWin] load error', err));
}
// ===== Updater 事件桥接到渲染进程,并控制 tkpage 启动时机 =====
function wireUpdaterIpc() {
if (ALLOW_UNSIGNED) {
console.warn('[updater] verifyUpdateCodeSignature disabled (YOLO_ALLOW_UNSIGNED=1)');
autoUpdater.verifyUpdateCodeSignature = async () => null;
}
autoUpdater.autoDownload = true;
autoUpdater.autoInstallOnAppQuit = false;
const send = (ch, data) => { if (updateWin && !updateWin.isDestroyed()) updateWin.webContents.send(ch, data); };
autoUpdater.on('checking-for-update', () => {
console.info('[updater] Checking');
tray?.setToolTip('YOLO - 正在检查更新…');
send('update:status', { status: 'checking' });
});
autoUpdater.on('update-available', (info) => {
console.info('[updater] Available', info?.version);
tray?.setToolTip(`YOLO - 检测到新版本 v${info?.version},开始下载…`);
send('update:status', { status: 'available', version: info?.version, notes: info?.releaseNotes || '' });
// 此时不启动 tkpage等待下载完成
});
autoUpdater.on('update-not-available', (info) => {
console.info('[updater] None');
tray?.setToolTip('YOLO - 已是最新版本');
send('update:status', { status: 'none', currentVersion: app.getVersion(), info });
// ✅ 没有更新,立刻启动 tkpage
launchPythonIfNeeded();
// 可隐藏更新窗口(让应用留在托盘+tkpage前台
setTimeout(() => updateWin?.hide(), 1200);
});
autoUpdater.on('download-progress', (p) => {
const percent = Math.max(0, Math.min(100, p?.percent || 0));
const transferred = Math.floor((p?.transferred || 0) / 1024 / 1024);
const total = Math.floor((p?.total || 0) / 1024 / 1024);
const speed = Math.floor((p?.bytesPerSecond || 0) / 1024 / 1024);
tray?.setToolTip(`YOLO - 正在下载更新:${percent.toFixed(1)}%`);
send('update:progress', { percent, transferred, total, speed });
});
autoUpdater.on('update-downloaded', (info) => {
console.info('[updater] Downloaded', info?.version);
tray?.setToolTip('YOLO - 更新已下载,等待安装…');
// 仅通知渲染进程显示“立即安装”按钮;不自动安装
send('update:status', { status: 'downloaded', version: info?.version });
// 可选择把更新窗口确保前置
if (updateWin && !updateWin.isDestroyed()) { updateWin.show(); updateWin.focus(); }
});
autoUpdater.on('error', (err) => {
const msg = err?.message || String(err);
console.error('[updater] error', msg);
tray?.setToolTip('YOLO - 更新失败');
dialog.showErrorBox('自动更新失败', msg);
send('update:status', { status: 'error', message: msg });
// ⚠️ 出错:不阻断主流程,直接启动 tkpage
launchPythonIfNeeded();
});
// 渲染侧手动触发
ipcMain.on('updater:check', () => startUpdateCheck(/*force*/true));
ipcMain.on('updater:install-now', () => {
if (installTriggered) return;
installTriggered = true;
console.info('[updater] quitAndInstall requested by renderer');
tray?.setToolTip('YOLO - 正在安装更新…');
quitting = true;
try { killPythonGUI(false); } catch (e) {
console.warn('[updater] killPythonGUI error:', e?.message || e);
}
setTimeout(() => {
try {
autoUpdater.quitAndInstall(false, true);
} catch (e) {
installTriggered = false;
console.error('[updater] quitAndInstall error:', e?.message || e);
dialog.showErrorBox('安装失败', String(e?.message || e));
}
}, 300);
});
}
// ===== 触发更新检查(仅触发一次,托盘点“检查更新”可强制再触发) =====
function startUpdateCheck(force = false) {
if (updaterKicked && !force) return;
updaterKicked = true;
// 显示更新窗口(可选:你也可以只在有更新时显示)
if (!updateWin || updateWin.isDestroyed()) createUpdateWindow();
else { updateWin.show(); updateWin.focus(); }
autoUpdater.checkForUpdates().catch(() => { /* 错误会在 on('error') 里处理 */ });
}
// ===== 单例 & 生命周期 =====
process.on('unhandledRejection', (r) => console.error('[unhandledRejection]', r));
process.on('uncaughtException', (e) => console.error('[uncaughtException]', e));
const gotLock = app.requestSingleInstanceLock();
if (!gotLock) {
app.quit();
} else {
app.on('second-instance', () => { if (updateWin) { updateWin.show(); updateWin.focus(); } });
app.whenReady().then(() => {
if (process.platform === 'win32') {
app.setAppUserModelId('com.yolozs.newPage');
}
createTray();
createUpdateWindow();
wireUpdaterIpc();
// ✅ 启动时:只检查更新,不启动 tkpage。无更新 → 启动;有更新 → 下载并重启安装。
startUpdateCheck();
});
app.on('before-quit', () => { quitting = true; killPythonGUI(true); });
app.on('window-all-closed', () => { /* 后台常驻 */ });
}