diff --git a/src/main/index.ts b/src/main/index.ts index a28118e..35c1bc3 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -5,6 +5,8 @@ import { cleanupProtocolUrls, detectFileTraits, normalizeMarkdown } from "./file import { close, getIsQuitting, + isWindowClosing, + setIsQuitting, registerGlobalIpcHandlers, registerIpcHandleHandlers, registerIpcOnHandlers, @@ -13,11 +15,18 @@ import createMenu from "./menu"; import { setupUpdateHandlers } from "./update"; import { trackWindow } from "./windowManager"; -let win: BrowserWindow; +let win: BrowserWindow | null = null; let themeEditorWindow: BrowserWindow | null = null; let isRendererReady = false; let pendingStartupFile: string | null = null; +/** 安全获取一个可用的编辑器窗口(优先主窗口,回退到任意存活窗口) */ +function getAvailableWindow(): BrowserWindow | null { + if (win && !win.isDestroyed()) return win; + const allWindows = BrowserWindow.getAllWindows().filter((w) => !w.isDestroyed()); + return allWindows[0] ?? null; +} + async function createWindow() { win = new BrowserWindow({ width: 1200, @@ -40,15 +49,11 @@ async function createWindow() { trackWindow(win, true); globalShortcut.register("CommandOrControl+Shift+I", () => { - if (win) win.webContents.openDevTools(); + const targetWin = getAvailableWindow(); + if (targetWin) targetWin.webContents.openDevTools(); }); - // 注册 IPC 处理程序 (在加载页面前注册,防止竞态条件) - registerIpcOnHandlers(win); - registerIpcHandleHandlers(win); - setupUpdateHandlers(win); - - createMenu(win); + createMenu(); // 处理外部链接跳转(target="_blank" 或 window.open) win.webContents.setWindowOpenHandler(({ url }) => { @@ -81,6 +86,14 @@ async function createWindow() { if (process.env.VITE_DEV_SERVER_URL) { win.webContents.openDevTools(); } + + // macOS: 窗口关闭时如果不是退出流程且不是主动关闭,只隐藏而不关闭 + win.on("close", (event) => { + if (process.platform === "darwin" && !getIsQuitting() && !isWindowClosing(win!.id)) { + event.preventDefault(); + win?.webContents.send("close"); + } + }); } // 创建主题编辑器窗口 @@ -95,7 +108,7 @@ export async function createThemeEditorWindow() { height: 700, minWidth: 800, minHeight: 600, - parent: win, + parent: getAvailableWindow() ?? undefined, modal: false, frame: false, titleBarStyle: "hidden", @@ -168,8 +181,9 @@ function sendFileToRenderer(filePath: string) { // 发送到渲染进程的函数 const sendFile = () => { - if (win && win.webContents) { - win.webContents.send("open-file-at-launch", { + const targetWin = getAvailableWindow(); + if (targetWin) { + targetWin.webContents.send("open-file-at-launch", { filePath, content, fileTraits, @@ -207,7 +221,11 @@ protocol.registerSchemesAsPrivileged([ ]); app.whenReady().then(async () => { + // 注册所有 IPC 处理程序(只注册一次,防止重复注册报错) registerGlobalIpcHandlers(); + registerIpcOnHandlers(); + registerIpcHandleHandlers(); + setupUpdateHandlers(); // 注册自定义协议处理器(仅用于兼容旧版本残留的 milkup:// URL) // 新版本使用 file:// 协议直接加载本地图片 @@ -261,19 +279,7 @@ app.whenReady().then(async () => { await createWindow(); - // createMenu(win) // Moved to createWindow - // registerIpcOnHandlers(win) // Moved to createWindow - // registerIpcHandleHandlers(win) // Moved to createWindow - // setupUpdateHandlers(win) // Moved to createWindow - sendLaunchFileIfExists(); - - win.on("close", (event) => { - if (process.platform === "darwin" && !getIsQuitting()) { - event.preventDefault(); - win.webContents.send("close"); - } - }); }); // 单实例锁 @@ -283,12 +289,13 @@ if (!gotTheLock) { app.quit(); } else { app.on("second-instance", (_event, argv) => { - if (win) { - if (win.isMinimized()) win.restore(); - win.focus(); - // 处理通过命令行传入的文件路径 - sendLaunchFileIfExists(argv); + const targetWin = getAvailableWindow(); + if (targetWin) { + if (targetWin.isMinimized()) targetWin.restore(); + targetWin.focus(); } + // 处理通过命令行传入的文件路径 + sendLaunchFileIfExists(argv); }); } // macOS 专用:Finder 打开文件时触发 @@ -297,11 +304,20 @@ app.on("open-file", (event, filePath) => { event.preventDefault(); sendFileToRenderer(filePath); }); -// 处理应用即将退出事件(包括右键 Dock 图标的退出) +// 处理应用即将退出事件(包括右键 Dock 图标的退出、Cmd+Q) app.on("before-quit", (event) => { - if (process.platform === "darwin" && !getIsQuitting()) { - event.preventDefault(); - close(win); + // 防止重入:close() / close:discard 中的 app.quit() 会再次触发 before-quit + if (getIsQuitting()) return; + + // 标记正在退出,让窗口 close 事件不再拦截 + setIsQuitting(true); + + if (process.platform === "darwin") { + const targetWin = getAvailableWindow(); + if (targetWin) { + event.preventDefault(); + close(targetWin); + } } }); @@ -311,18 +327,22 @@ app.on("window-all-closed", () => { } }); -// macOS 上处理应用激活事件 +// macOS 上处理应用激活事件(点击 Dock 图标) app.on("activate", () => { + // 重置退出标记:用户重新激活应用说明不想退出 + setIsQuitting(false); + if (BrowserWindow.getAllWindows().length === 0) { createWindow(); } else { - // 如果窗口存在但被隐藏,则显示它 - if (win && !win.isVisible()) { - win.show(); - } - // 将窗口置于前台 - if (win) { - win.focus(); + const targetWin = getAvailableWindow(); + if (targetWin) { + // 如果窗口存在但被隐藏,则显示它 + if (!targetWin.isVisible()) { + targetWin.show(); + } + // 将窗口置于前台 + targetWin.focus(); } } }); diff --git a/src/main/ipcBridge.ts b/src/main/ipcBridge.ts index 7616c71..e18ad7f 100644 --- a/src/main/ipcBridge.ts +++ b/src/main/ipcBridge.ts @@ -38,6 +38,13 @@ const windowSaveState = new Map(); /** 正在执行关闭流程的窗口集合 */ const windowClosingSet = new Set(); +/** 应用是否正在退出(macOS Cmd+Q / Dock 右键退出时设为 true) */ +let isQuitting = false; + +export function setIsQuitting(value: boolean): void { + isQuitting = value; +} + /** 窗口关闭后清理状态,防止内存泄漏 */ function cleanupWindowState(windowId: number): void { windowSaveState.delete(windowId); @@ -53,7 +60,7 @@ let directoryWatcher: FSWatcher | null = null; let directoryChangedDebounceTimer: ReturnType | null = null; // 所有 on 类型监听 -export function registerIpcOnHandlers(win: Electron.BrowserWindow) { +export function registerIpcOnHandlers() { ipcMain.on("set-title", (event, filePath: string | null) => { const targetWin = BrowserWindow.fromWebContents(event.sender); if (!targetWin) return; @@ -102,11 +109,24 @@ export function registerIpcOnHandlers(win: Electron.BrowserWindow) { if (!targetWin || targetWin.isDestroyed()) return; const winId = targetWin.id; windowClosingSet.add(winId); - targetWin.close(); + // 使用 destroy() 确保窗口在 macOS 上被彻底销毁(close() 只关闭 NSWindow 但 BrowserWindow 对象仍存活) + targetWin.destroy(); cleanupWindowState(winId); - // 如果是最后一个窗口(非 macOS),退出应用 - const remaining = BrowserWindow.getAllWindows().filter((w) => !w.isDestroyed()); - if (remaining.length === 0 && process.platform !== "darwin") { + + // 查找剩余的编辑器窗口(排除刚关闭的窗口) + const remainingEditorWindows = [...getEditorWindows()].filter( + (w) => w.id !== winId && !w.isDestroyed() + ); + + if (remainingEditorWindows.length > 0) { + // 还有其他编辑器窗口 → 激活其中一个到前台 + const nextWin = remainingEditorWindows[0]; + if (!nextWin.isVisible()) nextWin.show(); + nextWin.focus(); + } else { + // 没有剩余编辑器窗口 → 退出应用(用户主动删除了所有 tab) + // 先标记退出,防止 before-quit 重入拦截 + isQuitting = true; app.quit(); } }); @@ -156,15 +176,18 @@ export function registerIpcOnHandlers(win: Electron.BrowserWindow) { } }); - // 保存自定义主题 + // 保存自定义主题 —— 广播到所有编辑器窗口 ipcMain.on("save-custom-theme", (_event, theme) => { - // 转发到主窗口 - win.webContents.send("custom-theme-saved", theme); + for (const editorWin of getEditorWindows()) { + if (!editorWin.isDestroyed()) { + editorWin.webContents.send("custom-theme-saved", theme); + } + } }); } // 所有 handle 类型监听 —— 使用 event.sender 路由到正确窗口 -export function registerIpcHandleHandlers(win: Electron.BrowserWindow) { +export function registerIpcHandleHandlers() { // 检查文件是否只读 ipcMain.handle("file:isReadOnly", async (_event, filePath: string) => { return isFileReadOnly(filePath); @@ -172,7 +195,8 @@ export function registerIpcHandleHandlers(win: Electron.BrowserWindow) { // 文件打开对话框 ipcMain.handle("dialog:openFile", async (event) => { - const parentWin = BrowserWindow.fromWebContents(event.sender) ?? win; + const parentWin = BrowserWindow.fromWebContents(event.sender); + if (!parentWin) return null; const { canceled, filePaths } = await dialog.showOpenDialog(parentWin, { filters: [{ name: "Markdown", extensions: ["md", "markdown"] }], properties: ["openFile"], @@ -196,7 +220,8 @@ export function registerIpcHandleHandlers(win: Electron.BrowserWindow) { fileTraits, }: { filePath: string | null; content: string; fileTraits?: FileTraits } ) => { - const parentWin = BrowserWindow.fromWebContents(event.sender) ?? win; + const parentWin = BrowserWindow.fromWebContents(event.sender); + if (!parentWin) return null; if (!filePath) { const { canceled, filePath: savePath } = await dialog.showSaveDialog(parentWin, { filters: [{ name: "Markdown", extensions: ["md", "markdown"] }], @@ -212,7 +237,8 @@ export function registerIpcHandleHandlers(win: Electron.BrowserWindow) { ); // 文件另存为对话框 ipcMain.handle("dialog:saveFileAs", async (event, content) => { - const parentWin = BrowserWindow.fromWebContents(event.sender) ?? win; + const parentWin = BrowserWindow.fromWebContents(event.sender); + if (!parentWin) return null; const { canceled, filePath } = await dialog.showSaveDialog(parentWin, { filters: [{ name: "Markdown", extensions: ["md", "markdown"] }], }); @@ -223,14 +249,16 @@ export function registerIpcHandleHandlers(win: Electron.BrowserWindow) { // 同步显示消息框 ipcMain.handle("dialog:OpenDialog", async (event, options: Electron.MessageBoxSyncOptions) => { - const parentWin = BrowserWindow.fromWebContents(event.sender) ?? win; + const parentWin = BrowserWindow.fromWebContents(event.sender); + if (!parentWin) return null; const response = await dialog.showMessageBox(parentWin, options); return response; }); // 显示文件覆盖确认对话框 ipcMain.handle("dialog:showOverwriteConfirm", async (event, fileName: string) => { - const parentWin = BrowserWindow.fromWebContents(event.sender) ?? win; + const parentWin = BrowserWindow.fromWebContents(event.sender); + if (!parentWin) return 0; const result = await dialog.showMessageBox(parentWin, { type: "question", buttons: ["取消", "覆盖", "保存"], @@ -244,7 +272,8 @@ export function registerIpcHandleHandlers(win: Electron.BrowserWindow) { // 显示关闭确认对话框 ipcMain.handle("dialog:showCloseConfirm", async (event, fileName: string) => { - const parentWin = BrowserWindow.fromWebContents(event.sender) ?? win; + const parentWin = BrowserWindow.fromWebContents(event.sender); + if (!parentWin) return 0; const result = await dialog.showMessageBox(parentWin, { type: "question", buttons: ["取消", "不保存", "保存"], @@ -258,7 +287,8 @@ export function registerIpcHandleHandlers(win: Electron.BrowserWindow) { // 显示文件选择对话框 ipcMain.handle("dialog:showOpenDialog", async (event, options: any) => { - const parentWin = BrowserWindow.fromWebContents(event.sender) ?? win; + const parentWin = BrowserWindow.fromWebContents(event.sender); + if (!parentWin) return { canceled: true, filePaths: [] }; const result = await dialog.showOpenDialog(parentWin, options); return result; }); @@ -272,7 +302,8 @@ export function registerIpcHandleHandlers(win: Electron.BrowserWindow) { options?: ExportPDFOptions ): Promise => { const sender = event.sender; - const parentWin = BrowserWindow.fromWebContents(sender) ?? win; + const parentWin = BrowserWindow.fromWebContents(sender); + if (!parentWin) return Promise.reject(new Error("窗口已销毁")); const { pageSize = "A4", scale = 1 } = options || {}; // 保证代码块完整显示 @@ -460,7 +491,8 @@ export function registerIpcHandleHandlers(win: Electron.BrowserWindow) { }); const buffer = await Packer.toBuffer(doc); - const parentWin = BrowserWindow.fromWebContents(event.sender) ?? win; + const parentWin = BrowserWindow.fromWebContents(event.sender); + if (!parentWin) return Promise.reject(new Error("窗口已销毁")); const { canceled, filePath } = await dialog.showSaveDialog(parentWin, { title: "导出为 Word", @@ -857,9 +889,11 @@ export function registerGlobalIpcHandlers() { const notifyChanged = () => { if (directoryChangedDebounceTimer) clearTimeout(directoryChangedDebounceTimer); directoryChangedDebounceTimer = setTimeout(() => { - const mainWindow = BrowserWindow.getAllWindows()[0]; - if (mainWindow && !mainWindow.isDestroyed()) { - mainWindow.webContents.send("workspace:directory-changed"); + // 广播到所有编辑器窗口 + for (const editorWin of getEditorWindows()) { + if (!editorWin.isDestroyed()) { + editorWin.webContents.send("workspace:directory-changed"); + } } }, 300); }; @@ -932,12 +966,15 @@ export function close(win: Electron.BrowserWindow) { if (isSaved) { windowClosingSet.add(win.id); - win.close(); + // 使用 destroy() 确保窗口在 macOS 上被彻底销毁 + win.destroy(); cleanupWindowState(win.id); - // 如果所有窗口都关了(非 macOS),退出应用 + // 如果所有窗口都关了,退出应用 const remaining = BrowserWindow.getAllWindows().filter((w) => !w.isDestroyed()); - if (remaining.length === 0 && process.platform !== "darwin") { - app.quit(); + if (remaining.length === 0) { + if (process.platform !== "darwin" || isQuitting) { + app.quit(); + } } } else { // 有未保存内容,通知渲染进程弹出确认框 @@ -948,10 +985,16 @@ export function close(win: Electron.BrowserWindow) { } export function getIsQuitting() { - // 兼容旧逻辑:当所有窗口都在关闭时视为正在退出 + // 显式退出标记 或 所有窗口都已在关闭流程中 + if (isQuitting) return true; const allWindows = BrowserWindow.getAllWindows().filter((w) => !w.isDestroyed()); return allWindows.length === 0 || allWindows.every((w) => windowClosingSet.has(w.id)); } + +/** 检查指定窗口是否已在关闭流程中(由 close:discard 或 close() 发起) */ +export function isWindowClosing(winId: number): boolean { + return windowClosingSet.has(winId); +} export function isFileReadOnly(filePath: string): boolean { // 先检测是否可写(跨平台) try { diff --git a/src/main/menu.ts b/src/main/menu.ts index e6493ec..9147024 100644 --- a/src/main/menu.ts +++ b/src/main/menu.ts @@ -1,80 +1,91 @@ -import type { BrowserWindow } from 'electron' -import { Menu } from 'electron' -import { close } from './ipcBridge' +import { BrowserWindow, Menu } from "electron"; +import { close } from "./ipcBridge"; -export default function createMenu(win: BrowserWindow) { +/** + * 获取当前聚焦的窗口,用于菜单 click 处理器。 + * 避免在闭包中捕获特定窗口引用,防止窗口销毁后出现 + * "Object has been destroyed" 错误。 + */ +function getFocusedWindow(): BrowserWindow | null { + const win = BrowserWindow.getFocusedWindow(); + if (win && !win.isDestroyed()) return win; + // 如果没有聚焦窗口(例如 macOS 上窗口全部隐藏),回退到第一个可见窗口 + const allWindows = BrowserWindow.getAllWindows(); + return allWindows.find((w) => !w.isDestroyed()) ?? null; +} + +export default function createMenu() { const template: Electron.MenuItemConstructorOptions[] = [ { - label: '文件', + label: "文件", submenu: [ { - label: '打开', - accelerator: 'CmdOrCtrl+O', + label: "打开", + accelerator: "CmdOrCtrl+O", click: () => { - win.webContents.send('menu-open') + getFocusedWindow()?.webContents.send("menu-open"); }, }, { - label: '保存', - accelerator: 'CmdOrCtrl+S', + label: "保存", + accelerator: "CmdOrCtrl+S", click: () => { - win.webContents.send('menu-save') + getFocusedWindow()?.webContents.send("menu-save"); }, }, ], }, { - label: '编辑', + label: "编辑", submenu: [ - { label: '撤销', accelerator: 'CmdOrCtrl+Z', role: 'undo' }, - { label: '重做', accelerator: 'Shift+CmdOrCtrl+Z', role: 'redo' }, - { label: '剪切', accelerator: 'CmdOrCtrl+X', role: 'cut' }, - { label: '复制', accelerator: 'CmdOrCtrl+C', role: 'copy' }, - { label: '粘贴', accelerator: 'CmdOrCtrl+V', role: 'paste' }, - { label: '全选', accelerator: 'CmdOrCtrl+A', role: 'selectAll' }, + { label: "撤销", accelerator: "CmdOrCtrl+Z", role: "undo" }, + { label: "重做", accelerator: "Shift+CmdOrCtrl+Z", role: "redo" }, + { label: "剪切", accelerator: "CmdOrCtrl+X", role: "cut" }, + { label: "复制", accelerator: "CmdOrCtrl+C", role: "copy" }, + { label: "粘贴", accelerator: "CmdOrCtrl+V", role: "paste" }, + { label: "全选", accelerator: "CmdOrCtrl+A", role: "selectAll" }, ], }, { - label: '视图', + label: "视图", submenu: [ - { label: '实际大小', accelerator: 'CmdOrCtrl+0', role: 'resetZoom' }, - { label: '全屏', accelerator: 'F11', role: 'togglefullscreen' }, + { label: "实际大小", accelerator: "CmdOrCtrl+0", role: "resetZoom" }, + { label: "全屏", accelerator: "F11", role: "togglefullscreen" }, { - label: '切换视图', - accelerator: 'CmdOrCtrl+\/', + label: "切换视图", + accelerator: "CmdOrCtrl+/", click: () => { - win.webContents.send('view:toggleView') + getFocusedWindow()?.webContents.send("view:toggleView"); }, }, ], }, { - label: '窗口', - submenu: [ - { label: '最小化', accelerator: 'CmdOrCtrl+M', role: 'minimize' }, - ], + label: "窗口", + submenu: [{ label: "最小化", accelerator: "CmdOrCtrl+M", role: "minimize" }], }, - ] + ]; // 在 macOS 上添加应用菜单 - if (process.platform === 'darwin') { + if (process.platform === "darwin") { template.unshift({ - label: 'milkup', + label: "milkup", submenu: [ - { label: '隐藏 milkup', accelerator: 'Cmd+H', role: 'hide' }, - { label: '隐藏其他', accelerator: 'Cmd+Alt+H', role: 'hideOthers' }, - { type: 'separator' }, + { label: "隐藏 milkup", accelerator: "Cmd+H", role: "hide" }, + { label: "隐藏其他", accelerator: "Cmd+Alt+H", role: "hideOthers" }, + { type: "separator" }, { - label: '退出 milkup', - accelerator: 'Cmd+Q', + label: "退出 milkup", + accelerator: "Cmd+Q", click: () => { - close(win) + const win = getFocusedWindow(); + if (win) close(win); }, }, ], - }) + }); } - const menu = Menu.buildFromTemplate(template) - Menu.setApplicationMenu(menu) + const menu = Menu.buildFromTemplate(template); + Menu.setApplicationMenu(menu); } diff --git a/src/main/update.ts b/src/main/update.ts index 81e9147..1408f2e 100644 --- a/src/main/update.ts +++ b/src/main/update.ts @@ -1,6 +1,7 @@ -import { app, BrowserWindow, ipcMain, shell, net } from "electron"; +import { app, ipcMain, shell, net } from "electron"; import * as fs from "node:fs"; import * as path from "node:path"; +import { getEditorWindows } from "./windowManager"; import { createWriteStream } from "node:fs"; @@ -147,12 +148,25 @@ let currentUpdateInfo: { url: string; filename: string; version: string; size?: let downloadedFilePath: string | null = null; let downloadAbortController: AbortController | null = null; -export function setupUpdateHandlers(win: BrowserWindow) { +/** + * 安全地向所有编辑器窗口广播更新状态。 + * 更新信息与所有窗口相关(用户可能在任意窗口触发检查更新), + * 因此广播到全部存活的编辑器窗口。 + */ +function broadcastToAll(channel: string, ...args: any[]): void { + for (const win of getEditorWindows()) { + if (!win.isDestroyed()) { + win.webContents.send(channel, ...args); + } + } +} + +export function setupUpdateHandlers() { // 1. 检查更新 ipcMain.handle("update:check", async () => { try { console.log("[Main] Starting update check..."); - win.webContents.send("update:status", { status: "checking" }); + broadcastToAll("update:status", { status: "checking" }); const api = "https://api.github.com/repos/auto-plugin/milkup/releases/latest"; console.log("[Main] Fetching from GitHub API:", api); @@ -229,12 +243,12 @@ export function setupUpdateHandlers(win: BrowserWindow) { currentUpdateInfo = updateInfo; // 缓存 update info 供下载使用 - win.webContents.send("update:status", { status: "available", info: updateInfo }); + broadcastToAll("update:status", { status: "available", info: updateInfo }); console.log("[Main] Update available, returning info"); return { updateInfo }; } else { console.warn("[Main] No suitable asset found for platform:", process.platform); - win.webContents.send("update:status", { + broadcastToAll("update:status", { status: "not-available", info: { reason: "no-asset" }, }); @@ -242,12 +256,12 @@ export function setupUpdateHandlers(win: BrowserWindow) { } } else { console.log("[Main] Already on latest version"); - win.webContents.send("update:status", { status: "not-available" }); + broadcastToAll("update:status", { status: "not-available" }); return null; } } catch (error: any) { console.error("[Main] Check update failed:", error); - win.webContents.send("update:status", { status: "error", error: error.message }); + broadcastToAll("update:status", { status: "error", error: error.message }); throw error; } }); @@ -290,8 +304,8 @@ export function setupUpdateHandlers(win: BrowserWindow) { if (expectedSize > 0 && currentSize === expectedSize) { // Exact match, assume already downloaded // Note: Could verify hash if available, but size match is decent optimization - win.webContents.send("update:status", { status: "downloaded", info: currentUpdateInfo }); - win.webContents.send("update:download-progress", { + broadcastToAll("update:status", { status: "downloaded", info: currentUpdateInfo }); + broadcastToAll("update:download-progress", { percent: 100, total: expectedSize, transferred: expectedSize, @@ -375,7 +389,7 @@ export function setupUpdateHandlers(win: BrowserWindow) { // For now just send every chunk or maybe every 1%? // Let's keep it simple as before, but maybe check if % changed significantly if needed. // Existing logic sent every chunk. - win.webContents.send("update:download-progress", { + broadcastToAll("update:download-progress", { percent, total: totalBytes, transferred: downloadedBytes, @@ -393,17 +407,17 @@ export function setupUpdateHandlers(win: BrowserWindow) { downloadAbortController = null; - win.webContents.send("update:status", { status: "downloaded", info: currentUpdateInfo }); + broadcastToAll("update:status", { status: "downloaded", info: currentUpdateInfo }); return downloadedFilePath; } catch (error: any) { if (error.name === "AbortError") { console.log("[Main] Download aborted by user"); - win.webContents.send("update:status", { status: "idle" }); + broadcastToAll("update:status", { status: "idle" }); return; } console.error("[Main] Download error:", error); - win.webContents.send("update:status", { status: "error", error: error.message }); + broadcastToAll("update:status", { status: "error", error: error.message }); downloadAbortController = null; // On error, we generally keep the partial file for resume, UNLESS it's a critical write error? @@ -424,7 +438,7 @@ export function setupUpdateHandlers(win: BrowserWindow) { } else { console.log("[Main] No active download to cancel"); // 如果没有活动的下载,确保状态是 idle - win.webContents.send("update:status", { status: "idle" }); + broadcastToAll("update:status", { status: "idle" }); } });