229 lines
9.1 KiB
JavaScript
229 lines
9.1 KiB
JavaScript
// 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', () => { /* 后台常驻 */ });
|
||
}
|