// SiYuan - Build Your Eternal Digital Garden // Copyright (c) 2020-present, b3log.org // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . const { app, BrowserWindow, shell, Menu, screen, ipcMain, globalShortcut, Tray, } = require("electron"); const path = require("path"); const fs = require("fs"); const net = require("net"); const fetch = require("electron-fetch").default; process.noAsar = true; const appDir = path.dirname(app.getAppPath()); const isDevEnv = process.env.NODE_ENV === "development"; const appVer = app.getVersion(); const confDir = path.join(app.getPath("home"), ".config", "siyuan"); const windowStatePath = path.join(confDir, "windowState.json"); let bootWindow; let firstOpen = false; let workspaces = []; // workspaceDir, id, browserWindow, tray let kernelPort = 6806; let resetWindowStateOnRestart = false; require("@electron/remote/main").initialize(); if (!app.requestSingleInstanceLock()) { app.quit(); return; } try { firstOpen = !fs.existsSync(path.join(confDir, "workspace.json")); if (!fs.existsSync(confDir)) { fs.mkdirSync(confDir, {mode: 0o755, recursive: true}); } } catch (e) { console.error(e); require("electron").dialog.showErrorBox("创建配置目录失败 Failed to create config directory", "思源需要在用户家目录下创建配置文件夹(~/.config/siyuan),请确保该路径具有写入权限。\n\nSiYuan needs to create a configuration folder (~/.config/siyuan) in the user's home directory. Please make sure that the path has write permissions."); app.exit(); } const exitApp = (port, errorWindowId) => { let tray; let mainWindow; // 关闭端口相同的所有非主窗口 BrowserWindow.getAllWindows().forEach((item) => { try { const currentURL = new URL(item.getURL()) if (port.toString() === currentURL.port.toString()) { if (currentURL.href.indexOf("/stage/build/app/?v=") > -1) { mainWindow = item; } else { item.destroy(); } } } catch (e) { // load file is not a url } }); workspaces.find((item, index) => { if (mainWindow.id === item.browserWindow.id) { if (workspaces.length > 1) { item.browserWindow.destroy(); } workspaces.splice(index, 1); tray = item.tray; return true; } }); if (tray && ("win32" === process.platform || "linux" === process.platform)) { tray.destroy(); } if (workspaces.length === 0 && mainWindow) { try { if (resetWindowStateOnRestart) { fs.writeFileSync(windowStatePath, "{}"); } else { const bounds = mainWindow.getBounds(); fs.writeFileSync(windowStatePath, JSON.stringify({ isMaximized: mainWindow.isMaximized(), fullscreen: mainWindow.isFullScreen(), isDevToolsOpened: mainWindow.webContents.isDevToolsOpened(), x: bounds.x, y: bounds.y, width: bounds.width, height: bounds.height, })); } } catch (e) { writeLog(e); } if (errorWindowId) { BrowserWindow.getAllWindows().forEach((item) => { if (errorWindowId !== item.id) { item.destroy(); } }); } else { app.exit(); } globalShortcut.unregisterAll(); writeLog("exited ui"); } } const getServer = (port = kernelPort) => { return "http://127.0.0.1:" + port; }; const sleep = (ms) => { return new Promise(resolve => setTimeout(resolve, ms)); }; const showErrorWindow = (title, content) => { let errorHTMLPath = path.join(appDir, "app", "electron", "error.html"); if (isDevEnv) { errorHTMLPath = path.join(appDir, "electron", "error.html"); } const errWindow = new BrowserWindow({ width: screen.getPrimaryDisplay().size.width / 2, height: screen.getPrimaryDisplay().workAreaSize.height / 2, frame: false, icon: path.join(appDir, "stage", "icon-large.png"), webPreferences: { nodeIntegration: true, webviewTag: true, webSecurity: false, contextIsolation: false, }, }); require("@electron/remote/main").enable(errWindow.webContents); errWindow.loadFile(errorHTMLPath, { query: { home: app.getPath("home"), v: appVer, title: title, content: content, icon: path.join(appDir, "stage", "icon-large.png"), }, }); errWindow.show(); return errWindow.id; }; const writeLog = (out) => { console.log(out); const logFile = path.join(confDir, "app.log"); let log = ""; const maxLogLines = 1024; try { if (fs.existsSync(logFile)) { log = fs.readFileSync(logFile).toString(); let lines = log.split("\n"); if (maxLogLines < lines.length) { log = lines.slice(maxLogLines / 2, maxLogLines).join("\n") + "\n"; } } out = out.toString(); out = new Date().toISOString().replace(/T/, " ").replace(/\..+/, "") + " " + out; log += out + "\n"; fs.writeFileSync(logFile, log); } catch (e) { console.error(e); } }; const boot = () => { // 恢复主窗体状态 let oldWindowState = {}; try { oldWindowState = JSON.parse(fs.readFileSync(windowStatePath, "utf8")); } catch (e) { fs.writeFileSync(windowStatePath, "{}"); } let defaultWidth; let defaultHeight; let workArea; try { defaultWidth = screen.getPrimaryDisplay().size.width; defaultHeight = screen.getPrimaryDisplay().workAreaSize.height; workArea = screen.getPrimaryDisplay().workArea; } catch (e) { console.error(e); } const windowState = Object.assign({}, { isMaximized: true, fullscreen: false, isDevToolsOpened: false, x: 0, y: 0, width: defaultWidth, height: defaultHeight, }, oldWindowState); let x = windowState.x; let y = windowState.y; if (workArea) { // 窗口大小等同于或大于 workArea 时,缩小会隐藏到左下角 if (windowState.width >= workArea.width || windowState.height >= workArea.height) { windowState.width = Math.min(defaultWidth, workArea.width); windowState.height = Math.min(defaultHeight, workArea.height); } if (x > workArea.width) { x = 0; } if (y > workArea.height) { y = 0; } } if (windowState.width < 400) { windowState.width = 400; } if (windowState.height < 300) { windowState.height = 300; } if (x < 0) { x = 0; } if (y < 0) { y = 0; } // 创建主窗体 const currentWindow = new BrowserWindow({ show: false, backgroundColor: "#FFF", // 桌面端主窗体背景色设置为 `#FFF` Fix https://github.com/siyuan-note/siyuan/issues/4544 width: windowState.width, height: windowState.height, minWidth: 493, minHeight: 376, x, y, fullscreenable: true, fullscreen: windowState.fullscreen, trafficLightPosition: {x: 8, y: 8}, webPreferences: { nodeIntegration: true, webviewTag: true, webSecurity: false, contextIsolation: false, autoplayPolicy: "user-gesture-required" // 桌面端禁止自动播放多媒体 https://github.com/siyuan-note/siyuan/issues/7587 }, frame: "darwin" === process.platform, titleBarStyle: "hidden", icon: path.join(appDir, "stage", "icon-large.png"), }); require("@electron/remote/main").enable(currentWindow.webContents); currentWindow.webContents.userAgent = "SiYuan/" + appVer + " https://b3log.org/siyuan Electron"; currentWindow.webContents.session.setSpellCheckerLanguages(["en-US"]); // 发起互联网服务请求时绕过安全策略 https://github.com/siyuan-note/siyuan/issues/5516 currentWindow.webContents.session.webRequest.onBeforeSendHeaders((details, cb) => { if (-1 < details.url.indexOf("bili")) { // B 站不移除 Referer https://github.com/siyuan-note/siyuan/issues/94 cb({requestHeaders: details.requestHeaders}); return; } for (let key in details.requestHeaders) { if ("referer" === key.toLowerCase()) { delete details.requestHeaders[key]; } } cb({requestHeaders: details.requestHeaders}); }); currentWindow.webContents.session.webRequest.onHeadersReceived((details, cb) => { for (let key in details.responseHeaders) { if ("x-frame-options" === key.toLowerCase()) { delete details.responseHeaders[key]; } else if ("content-security-policy" === key.toLowerCase()) { delete details.responseHeaders[key]; } else if ("access-control-allow-origin" === key.toLowerCase()) { delete details.responseHeaders[key]; } } cb({responseHeaders: details.responseHeaders}); }); currentWindow.webContents.on("did-finish-load", () => { let siyuanOpenURL; if ("win32" === process.platform || "linux" === process.platform) { siyuanOpenURL = process.argv.find((arg) => arg.startsWith("siyuan://")); } if (siyuanOpenURL) { if (currentWindow.isMinimized()) { currentWindow.restore(); } if (!currentWindow.isVisible()) { currentWindow.show(); } currentWindow.focus(); setTimeout(() => { // 等待界面js执行完毕 writeLog(siyuanOpenURL); currentWindow.webContents.send("siyuan-openurl", siyuanOpenURL); }, 2000); } }); if (windowState.isDevToolsOpened) { currentWindow.webContents.openDevTools({mode: "bottom"}); } // 主界面事件监听 currentWindow.once("ready-to-show", () => { currentWindow.show(); if (windowState.isMaximized) { currentWindow.maximize(); } else { currentWindow.unmaximize(); } if (bootWindow && !bootWindow.isDestroyed()) { bootWindow.destroy(); } }); // 加载主界面 currentWindow.loadURL(getServer() + "/stage/build/app/index.html?v=" + new Date().getTime()); // 菜单 const productName = "SiYuan"; const template = [{ label: productName, submenu: [{ label: `About ${productName}`, role: "about", }, {type: "separator"}, {role: "services"}, {type: "separator"}, { label: `Hide ${productName}`, role: "hide", }, {role: "hideOthers"}, {role: "unhide"}, {type: "separator"}, { label: `Quit ${productName}`, role: "quit", },], }, { role: "editMenu", submenu: [{role: "cut"}, {role: "copy"}, {role: "paste"}, { role: "pasteAndMatchStyle", accelerator: "CmdOrCtrl+Shift+C" }, {role: "selectAll"},], }, { role: "windowMenu", submenu: [{role: "minimize"}, {role: "zoom"}, {role: "togglefullscreen"}, {type: "separator"}, {role: "toggledevtools"}, {type: "separator"}, {role: "front"},], },]; const menu = Menu.buildFromTemplate(template); Menu.setApplicationMenu(menu); // 当前页面链接使用浏览器打开 currentWindow.webContents.on("will-navigate", (event, url) => { const currentURL = new URL(event.sender.getURL()); if (url.startsWith(getServer(currentURL.port))) { return; } event.preventDefault(); shell.openExternal(url); }); currentWindow.on("close", (event) => { if (currentWindow && !currentWindow.isDestroyed()) { currentWindow.webContents.send("siyuan-save-close", false); } event.preventDefault(); }); workspaces.push({ browserWindow: currentWindow, id: currentWindow.id, }); }; const showWindow = (wnd) => { if (!wnd || wnd.isDestroyed()) { return; } if (wnd.isMinimized()) { wnd.restore(); } if (!wnd.isVisible()) { wnd.show(); } wnd.focus(); }; const initKernel = (workspace, port, lang) => { return new Promise(async (resolve) => { bootWindow = new BrowserWindow({ width: screen.getPrimaryDisplay().size.width / 2, height: screen.getPrimaryDisplay().workAreaSize.height / 2, frame: false, icon: path.join(appDir, "stage", "icon-large.png"), transparent: "linux" !== process.platform, }); const kernelName = "win32" === process.platform ? "SiYuan-Kernel.exe" : "SiYuan-Kernel"; const kernelPath = path.join(appDir, "kernel", kernelName); if (!fs.existsSync(kernelPath)) { showErrorWindow("⚠️ 内核文件丢失 Kernel is missing", "
内核可执行文件丢失,请重新安装思源,并将思源加入杀毒软件信任列表。
The kernel binary is not found, please reinstall SiYuan and add SiYuan into the trust list of your antivirus software.
"); bootWindow.destroy(); resolve(false); return; } if (!isDevEnv || workspaces.length > 0) { if (port && "" !== port) { kernelPort = port; } else { const getAvailablePort = () => { // https://gist.github.com/mikeal/1840641 return new Promise((portResolve, portReject) => { const server = net.createServer(); server.on("error", error => { writeLog(error); kernelPort = ""; portReject(); }); server.listen(0, () => { kernelPort = server.address().port; server.close(() => portResolve(kernelPort)); }); }); }; await getAvailablePort(); } } writeLog("got kernel port [" + kernelPort + "]"); if (!kernelPort) { bootWindow.destroy(); resolve(false); return; } const cmds = ["--port", kernelPort, "--wd", appDir]; if (isDevEnv && workspaces.length === 0) { cmds.push("--mode", "dev"); } if (workspace && "" !== workspace) { cmds.push("--workspace", workspace); } if (port && "" !== port) { cmds.push("--port", port); } if (lang && "" !== lang) { cmds.push("--lang", lang); } let cmd = `ui version [${appVer}], booting kernel [${kernelPath} ${cmds.join(" ")}]`; writeLog(cmd); if (!isDevEnv || workspaces.length > 0) { const cp = require("child_process"); const kernelProcess = cp.spawn(kernelPath, cmds, { detached: false, // 桌面端内核进程不再以游离模式拉起 https://github.com/siyuan-note/siyuan/issues/6336 stdio: "ignore", },); const currentKernelPort = kernelPort; writeLog("booted kernel process [pid=" + kernelProcess.pid + ", port=" + kernelPort + "]"); kernelProcess.on("close", (code) => { writeLog(`kernel [pid=${kernelProcess.pid}, port=${currentKernelPort}] exited with code [${code}]`); if (0 !== code) { let errorWindowId; switch (code) { case 20: errorWindowId = showErrorWindow("⚠️ 数据库被锁定 The database is locked", "
数据库文件正在被其他进程占用,请检查是否同时存在多个内核进程(SiYuan Kernel)服务相同的工作空间。
The database file is being occupied by other processes, please check whether there are multiple kernel processes (SiYuan Kernel) serving the same workspace at the same time.
"); break; case 21: errorWindowId = showErrorWindow("⚠️ 监听端口 " + currentKernelPort + " 失败 Failed to listen to port " + currentKernelPort, "
监听 " + currentKernelPort + " 端口失败,请确保程序拥有网络权限并不受防火墙和杀毒软件阻止。
Failed to listen to port " + currentKernelPort + ", please make sure the program has network permissions and is not blocked by firewalls and antivirus software.
"); break; case 22: errorWindowId = showErrorWindow("⚠️ 创建配置目录失败 Failed to create config directory", "
思源需要在用户家目录下创建配置文件夹(~/.config/siyuan),请确保该路径具有写入权限。
SiYuan needs to create a configuration folder (~/.config/siyuan) in the user\'s home directory. Please make sure that the path has write permissions.
"); break; case 24: // 工作空间已被锁定,尝试切换到第一个打开的工作空间 if (workspaces && 0 < workspaces.length) { showWindow(workspaces[0].browserWindow); } errorWindowId = showErrorWindow("⚠️ 工作空间已被锁定 The workspace is locked", "
该工作空间正在被使用。
The workspace is in use.
"); break; case 25: errorWindowId = showErrorWindow("⚠️ 创建工作空间目录失败 Failed to create workspace directory", "
创建工作空间目录失败。
Failed to create workspace directory.
"); break; case 26: errorWindowId = showErrorWindow("⚠️ 文件系统读写错误 File system access error", "
请检查文件系统权限,并确保没有其他程序正在读写文件;
请勿使用第三方同步盘进行数据同步,否则数据会被损坏(iCloud/OneDrive/Dropbox/Google Drive/坚果云/百度网盘/腾讯微云等)
Please check file system permissions and make sure no other programs are reading or writing to the file;
Do not use a third-party sync disk for data sync, otherwise the data will be damaged (OneDrive/Dropbox/Google Drive/Nutstore/Baidu Netdisk/Tencent Weiyun, etc.)
"); break; case 0: break; default: errorWindowId = showErrorWindow("⚠️ 内核因未知原因退出 The kernel exited for unknown reasons", `
思源内核因未知原因退出 [code=${code}],请尝试重启操作系统后再启动思源。如果该问题依然发生,请检查杀毒软件是否阻止思源内核启动。
SiYuan Kernel exited for unknown reasons [code=${code}], please try to reboot your operating system and then start SiYuan again. If occurs this problem still, please check your anti-virus software whether kill the SiYuan Kernel.
`); break; } exitApp(currentKernelPort, errorWindowId); bootWindow.destroy(); resolve(false); } }); } let gotVersion = false; let apiData; let count = 0; writeLog("checking kernel version"); while (!gotVersion && count < 15) { try { const apiResult = await fetch(getServer() + "/api/system/version"); apiData = await apiResult.json(); gotVersion = true; bootWindow.setResizable(false); bootWindow.loadURL(getServer() + "/appearance/boot/index.html"); bootWindow.show(); } catch (e) { writeLog("get kernel version failed: " + e.message); await sleep(100); } finally { count++; if (14 < count) { writeLog("get kernel ver failed"); showErrorWindow("⚠️ 获取内核服务端口失败 Failed to get kernel serve port", "
获取内核服务端口失败,请确保程序拥有网络权限并不受防火墙和杀毒软件阻止。
Failed to get kernel serve port, please make sure the program has network permissions and is not blocked by firewalls and antivirus software.
"); bootWindow.destroy(); resolve(false); } } } if (!gotVersion) { return; } if (0 === apiData.code) { writeLog("got kernel version [" + apiData.data + "]"); if (!isDevEnv && apiData.data !== appVer) { writeLog(`kernel [${apiData.data}] is running, shutdown it now and then start kernel [${appVer}]`); fetch(getServer() + "/api/system/exit", {method: "POST"}); bootWindow.destroy(); resolve(false); } else { let progressing = false; while (!progressing) { try { const progressResult = await fetch(getServer() + "/api/system/bootProgress"); const progressData = await progressResult.json(); if (progressData.data.progress >= 100) { resolve(true); progressing = true; } else { await sleep(100); } } catch (e) { writeLog("get boot progress failed: " + e.message); fetch(getServer() + "/api/system/exit", {method: "POST"}); bootWindow.destroy(); resolve(false); progressing = true; } } } } else { writeLog(`get kernel version failed: ${apiData.code}, ${apiData.msg}`); resolve(false); } }); }; app.setAsDefaultProtocolClient("siyuan"); app.commandLine.appendSwitch("disable-web-security"); app.commandLine.appendSwitch("auto-detect", "false"); app.commandLine.appendSwitch("no-proxy-server"); app.commandLine.appendSwitch("enable-features", "PlatformHEVCDecoderSupport"); app.setPath("userData", app.getPath("userData") + "-Electron"); // `~/.config` 下 Electron 相关文件夹名称改为 `SiYuan-Electron` https://github.com/siyuan-note/siyuan/issues/3349 app.whenReady().then(() => { const resetTrayMenu = (tray, lang, mainWindow) => { const trayMenuTemplate = [{ label: mainWindow.isVisible() ? lang.hideWindow : lang.showWindow, click: () => { showHideWindow(tray, lang, mainWindow); }, }, { label: lang.officialWebsite, click: () => { shell.openExternal("https://b3log.org/siyuan/"); }, }, { label: lang.openSource, click: () => { shell.openExternal("https://github.com/siyuan-note/siyuan"); }, }, { label: lang.resetWindow, type: "checkbox", click: v => { resetWindowStateOnRestart = v.checked; mainWindow.webContents.send("siyuan-save-close", true); }, }, { label: lang.quit, click: () => { mainWindow.webContents.send("siyuan-save-close", true); }, },]; if ("win32" === process.platform) { // Windows 端支持窗口置顶 https://github.com/siyuan-note/siyuan/issues/6860 trayMenuTemplate.splice(1, 0, { label: mainWindow.isAlwaysOnTop() ? lang.cancelWindowTop : lang.setWindowTop, click: () => { if (!mainWindow.isAlwaysOnTop()) { mainWindow.setAlwaysOnTop(true); } else { mainWindow.setAlwaysOnTop(false); } resetTrayMenu(tray, lang, mainWindow); }, }); } const contextMenu = Menu.buildFromTemplate(trayMenuTemplate); tray.setContextMenu(contextMenu); }; const hideWindow = (wnd) => { // 通过 `Alt+M` 最小化后焦点回到先前的窗口 https://github.com/siyuan-note/siyuan/issues/7275 wnd.minimize(); wnd.hide(); }; const showHideWindow = (tray, lang, mainWindow) => { if (!mainWindow.isVisible()) { if (mainWindow.isMinimized()) { mainWindow.restore(); } mainWindow.show(); } else { hideWindow(mainWindow); } resetTrayMenu(tray, lang, mainWindow); }; ipcMain.on("siyuan-first-quit", () => { app.exit(); }); ipcMain.on("siyuan-show", (event, id) => { showWindow(BrowserWindow.fromId(id)); }); ipcMain.on("siyuan-config-tray", (event, data) => { workspaces.find(item => { if (item.id === data.id) { hideWindow(item.browserWindow); if ("win32" === process.platform || "linux" === process.platform) { resetTrayMenu(item.tray, data.languages, item.browserWindow); } return true; } }); }); ipcMain.on("siyuan-export-pdf", (event, data) => { BrowserWindow.fromId(data.id).webContents.send("siyuan-export-pdf", data); }); ipcMain.on("siyuan-export-close", (event, id) => { BrowserWindow.fromId(id).webContents.send("siyuan-export-close", id); }); ipcMain.on("siyuan-export-prevent", (event, id) => { BrowserWindow.fromId(id).webContents.on("will-navigate", (event, url) => { const currentURL = new URL(event.sender.getURL()); event.preventDefault(); if (url.startsWith(getServer(currentURL.port))) { return; } shell.openExternal(url); }); }); ipcMain.on("siyuan-quit", (event, port) => { exitApp(port); }); ipcMain.on("siyuan-openwindow", (event, data) => { const mainWindow = BrowserWindow.fromId(data.id); const mainBounds = mainWindow.getBounds(); const mainScreen = screen.getDisplayNearestPoint({x: mainBounds.x, y: mainBounds.y}); const win = new BrowserWindow({ show: true, backgroundColor: "#FFF", trafficLightPosition: {x: 8, y: 13}, width: mainScreen.size.width * 0.7, height: mainScreen.size.height * 0.9, minWidth: 493, center: true, minHeight: 376, fullscreenable: true, frame: "darwin" === process.platform, icon: path.join(appDir, "stage", "icon-large.png"), titleBarStyle: "hidden", webPreferences: { contextIsolation: false, nodeIntegration: true, webviewTag: true, webSecurity: false, autoplayPolicy: "user-gesture-required" // 桌面端禁止自动播放多媒体 https://github.com/siyuan-note/siyuan/issues/7587 }, }); win.loadURL(data.url); const targetScreen = screen.getDisplayNearestPoint(screen.getCursorScreenPoint()); if (mainScreen.id !== targetScreen.id) { win.setBounds(targetScreen.workArea); } require("@electron/remote/main").enable(win.webContents); }); ipcMain.on("siyuan-open-workspace", (event, data) => { const foundWorkspace = workspaces.find((item) => { if (item.workspaceDir === data.workspace) { showWindow(item.browserWindow); return true; } }); if (!foundWorkspace) { initKernel(data.workspace, "", data.lang).then((isSucc) => { if (isSucc) { boot(); } }); } }); ipcMain.on("siyuan-init", async (event, data) => { const exitWS = workspaces.find(item => { if (data.id === item.id && item.workspaceDir) { return true; } }); if (exitWS) { return; } let tray; if ("win32" === process.platform || "linux" === process.platform) { // 系统托盘 tray = new Tray(path.join(appDir, "stage", "icon-large.png")); tray.setToolTip(`${path.basename(data.workspaceDir)} - SiYuan v${appVer}`); const mainWindow = BrowserWindow.fromId(data.id); resetTrayMenu(tray, data.languages, mainWindow); tray.on("click", () => { showHideWindow(tray, data.languages, mainWindow); }); } workspaces.find(item => { if (data.id === item.id) { item.workspaceDir = data.workspaceDir; item.tray = tray; return true; } }); await fetch(getServer(data.port) + "/api/system/uiproc?pid=" + process.pid, {method: "POST"}); }); ipcMain.on("siyuan-hotkey", (event, data) => { globalShortcut.unregisterAll(); if (!data.hotkey) { return; } globalShortcut.register(data.hotkey, () => { workspaces.forEach(item => { const mainWindow = item.browserWindow; if (mainWindow.isMinimized()) { mainWindow.restore(); if (!mainWindow.isVisible()) { mainWindow.show(); } } else { if (mainWindow.isVisible()) { if (1 === workspaces.length) { // 改进 `Alt+M` 激活窗口 https://github.com/siyuan-note/siyuan/issues/7258 if (!mainWindow.isFocused()) { mainWindow.show(); } else { hideWindow(mainWindow); } } else { hideWindow(mainWindow); } } else { mainWindow.show(); } } if ("win32" === process.platform || "linux" === process.platform) { resetTrayMenu(item.tray, data.languages, mainWindow); } }); }); }); ipcMain.on("siyuan-send_windows", (event, data) => { BrowserWindow.getAllWindows().forEach(item => { item.webContents.send("siyuan-send_windows", data); }); }); if (firstOpen) { const firstOpenWindow = new BrowserWindow({ width: screen.getPrimaryDisplay().size.width * 0.6, height: screen.getPrimaryDisplay().workAreaSize.height * 0.8, frame: false, icon: path.join(appDir, "stage", "icon-large.png"), transparent: "linux" !== process.platform, webPreferences: { nodeIntegration: true, webviewTag: true, webSecurity: false, contextIsolation: false, }, }); require("@electron/remote/main").enable(firstOpenWindow.webContents); let initHTMLPath = path.join(appDir, "app", "electron", "init.html"); if (isDevEnv) { initHTMLPath = path.join(appDir, "electron", "init.html"); } // 改进桌面端初始化时使用的外观语言 https://github.com/siyuan-note/siyuan/issues/6803 let languages = app.getPreferredSystemLanguages(); let language = languages && 0 < languages.length && "zh-Hans-CN" === languages[0] ? "zh_CN" : "en_US"; firstOpenWindow.loadFile(initHTMLPath, { query: { lang: language, home: app.getPath("home"), v: appVer, icon: path.join(appDir, "stage", "icon-large.png"), }, }); firstOpenWindow.show(); // 初始化启动 ipcMain.on("siyuan-first-init", (event, data) => { initKernel(data.workspace, "", data.lang).then((isSucc) => { if (isSucc) { boot(); } }); firstOpenWindow.destroy(); }); } else { const getArg = (name) => { for (let i = 0; i < process.argv.length; i++) { if (process.argv[i].startsWith(name)) { return process.argv[i].split("=")[1]; } } }; const workspace = getArg("--workspace"); if (workspace) { writeLog("got arg [--workspace=" + workspace + "]"); } const port = getArg("--port"); if (port) { writeLog("got arg [--port=" + port + "]"); } initKernel(workspace, port, "").then((isSucc) => { if (isSucc) { boot(); } }); } }); app.on("open-url", (event, url) => { // for macOS if (url.startsWith("siyuan://")) { workspaces.forEach(item => { if (item.browserWindow && !item.browserWindow.isDestroyed()) { item.browserWindow.webContents.send("siyuan-openurl", url); } }); } }); app.on("second-instance", (event, argv) => { writeLog("second-instance [" + argv + "]"); let workspace = argv.find((arg) => arg.startsWith("--workspace=")); if (workspace) { workspace = workspace.split("=")[1]; writeLog("got second-instance arg [--workspace=" + workspace + "]"); } let port = argv.find((arg) => arg.startsWith("--port=")); if (port) { port = port.split("=")[1]; writeLog("got second-instance arg [--port=" + port + "]"); } else { port = 0; } const foundWorkspace = workspaces.find(item => { if (item.browserWindow && !item.browserWindow.isDestroyed()) { if (workspace && workspace === item.workspaceDir) { showWindow(item.browserWindow); return true; } } }); if (foundWorkspace) { return; } if (workspace) { initKernel(workspace, port, "").then((isSucc) => { if (isSucc) { boot(); } }); return; } const siyuanURL = argv.find((arg) => arg.startsWith("siyuan://")); workspaces.forEach(item => { if (item.browserWindow && !item.browserWindow.isDestroyed() && siyuanURL) { item.browserWindow.webContents.send("siyuan-openurl", siyuanURL); } }); if (!siyuanURL && 0 < workspaces.length) { showWindow(workspaces[0].browserWindow); } }); app.on("activate", () => { if (workspaces.length > 0) { const mainWindow = workspaces[0].browserWindow; if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.show(); } } if (BrowserWindow.getAllWindows().length === 0) { boot(); } }); // 在编辑器内打开链接的处理,比如 iframe 上的打开链接。 app.on("web-contents-created", (webContentsCreatedEvent, contents) => { contents.setWindowOpenHandler((details) => { shell.openExternal(details.url); return {action: "deny"}; }); }); app.on("before-quit", (event) => { workspaces.forEach(item => { if (item.browserWindow && !item.browserWindow.isDestroyed()) { event.preventDefault(); item.browserWindow.webContents.send("siyuan-save-close", true); } }); }); const {powerMonitor} = require("electron"); powerMonitor.on("suspend", () => { writeLog("system suspend"); }); powerMonitor.on("resume", async () => { // 桌面端系统休眠唤醒后判断网络连通性后再执行数据同步 https://github.com/siyuan-note/siyuan/issues/6687 writeLog("system resume"); const isOnline = async () => { try { const result = await fetch("https://www.baidu.com", {timeout: 1000}); return 200 === result.status; } catch (e) { try { const result = await fetch("https://icanhazip.com", {timeout: 1000}); return 200 === result.status; } catch (e) { return false; } } }; let online = false; for (let i = 0; i < 7; i++) { if (await isOnline()) { online = true; break; } writeLog("network is offline"); await sleep(1000); } if (!online) { writeLog("network is offline, do not sync after system resume"); return; } workspaces.forEach(item => { const currentURL = new URL(item.browserWindow.getURL()); const server = getServer(currentURL.port); writeLog("sync after system resume [" + server + "/api/sync/performSync" + "]"); fetch(server + "/api/sync/performSync", {method: "POST"}); }); }); powerMonitor.on("shutdown", () => { writeLog("system shutdown"); workspaces.forEach(item => { const currentURL = new URL(item.browserWindow.getURL()); fetch(getServer(currentURL.port) + "/api/system/exit", {method: "POST"}); }); });