main.js 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053
  1. // SiYuan - Build Your Eternal Digital Garden
  2. // Copyright (c) 2020-present, b3log.org
  3. //
  4. // This program is free software: you can redistribute it and/or modify
  5. // it under the terms of the GNU Affero General Public License as published by
  6. // the Free Software Foundation, either version 3 of the License, or
  7. // (at your option) any later version.
  8. //
  9. // This program is distributed in the hope that it will be useful,
  10. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. // GNU Affero General Public License for more details.
  13. //
  14. // You should have received a copy of the GNU Affero General Public License
  15. // along with this program. If not, see <https://www.gnu.org/licenses/>.
  16. const {
  17. app,
  18. BrowserWindow,
  19. shell,
  20. Menu,
  21. screen,
  22. ipcMain,
  23. globalShortcut,
  24. Tray,
  25. } = require("electron");
  26. const path = require("path");
  27. const fs = require("fs");
  28. const net = require("net");
  29. const fetch = require("electron-fetch").default;
  30. process.noAsar = true;
  31. const appDir = path.dirname(app.getAppPath());
  32. const isDevEnv = process.env.NODE_ENV === "development";
  33. const appVer = app.getVersion();
  34. const confDir = path.join(app.getPath("home"), ".config", "siyuan");
  35. const windowStatePath = path.join(confDir, "windowState.json");
  36. let bootWindow;
  37. let firstOpen = false;
  38. let workspaces = []; // workspaceDir, id, browserWindow, tray
  39. let kernelPort = 6806;
  40. require("@electron/remote/main").initialize();
  41. if (!app.requestSingleInstanceLock()) {
  42. app.quit();
  43. return;
  44. }
  45. try {
  46. firstOpen = !fs.existsSync(path.join(confDir, "workspace.json"));
  47. if (!fs.existsSync(confDir)) {
  48. fs.mkdirSync(confDir, {mode: 0o755, recursive: true});
  49. }
  50. } catch (e) {
  51. console.error(e);
  52. require("electron").dialog.showErrorBox("创建配置目录失败 Failed to create config directory",
  53. "思源需要在用户家目录下创建配置文件夹(~/.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.");
  54. app.exit();
  55. }
  56. const getServer = (port = kernelPort) => {
  57. return "http://127.0.0.1:" + port;
  58. };
  59. const sleep = (ms) => {
  60. return new Promise(resolve => setTimeout(resolve, ms));
  61. };
  62. const showErrorWindow = (title, content) => {
  63. let errorHTMLPath = path.join(appDir, "app", "electron", "error.html");
  64. if (isDevEnv) {
  65. errorHTMLPath = path.join(appDir, "electron", "error.html");
  66. }
  67. const errWindow = new BrowserWindow({
  68. width: screen.getPrimaryDisplay().size.width / 2,
  69. height: screen.getPrimaryDisplay().workAreaSize.height / 2,
  70. frame: false,
  71. icon: path.join(appDir, "stage", "icon-large.png"),
  72. webPreferences: {
  73. nodeIntegration: true,
  74. webviewTag: true,
  75. webSecurity: false,
  76. contextIsolation: false,
  77. },
  78. });
  79. require("@electron/remote/main").enable(errWindow.webContents);
  80. errWindow.loadFile(errorHTMLPath, {
  81. query: {
  82. home: app.getPath("home"),
  83. v: appVer,
  84. title: title,
  85. content: content,
  86. icon: path.join(appDir, "stage", "icon-large.png"),
  87. },
  88. });
  89. errWindow.show();
  90. };
  91. const writeLog = (out) => {
  92. console.log(out);
  93. const logFile = path.join(confDir, "app.log");
  94. let log = "";
  95. const maxLogLines = 1024;
  96. try {
  97. if (fs.existsSync(logFile)) {
  98. log = fs.readFileSync(logFile).toString();
  99. let lines = log.split("\n");
  100. if (maxLogLines < lines.length) {
  101. log = lines.slice(maxLogLines / 2, maxLogLines).join("\n") + "\n";
  102. }
  103. }
  104. out = out.toString();
  105. out = new Date().toISOString().replace(/T/, " ").replace(/\..+/, "") + " " +
  106. out;
  107. log += out + "\n";
  108. fs.writeFileSync(logFile, log);
  109. } catch (e) {
  110. console.error(e);
  111. }
  112. };
  113. const boot = () => {
  114. // 恢复主窗体状态
  115. let oldWindowState = {};
  116. try {
  117. oldWindowState = JSON.parse(fs.readFileSync(windowStatePath, "utf8"));
  118. } catch (e) {
  119. fs.writeFileSync(windowStatePath, "{}");
  120. }
  121. let defaultWidth;
  122. let defaultHeight;
  123. let workArea;
  124. try {
  125. defaultWidth = screen.getPrimaryDisplay().size.width;
  126. defaultHeight = screen.getPrimaryDisplay().workAreaSize.height;
  127. workArea = screen.getPrimaryDisplay().workArea;
  128. } catch (e) {
  129. console.error(e);
  130. }
  131. const windowState = Object.assign({}, {
  132. isMaximized: true,
  133. fullscreen: false,
  134. isDevToolsOpened: false,
  135. x: 0, y: 0,
  136. width: defaultWidth,
  137. height: defaultHeight,
  138. }, oldWindowState);
  139. let x = windowState.x;
  140. let y = windowState.y;
  141. if (workArea) {
  142. // 窗口大小等同于或大于 workArea 时,缩小会隐藏到左下角
  143. if (windowState.width >= workArea.width || windowState.height >=
  144. workArea.height) {
  145. windowState.width = Math.min(defaultWidth, workArea.width);
  146. windowState.height = Math.min(defaultHeight, workArea.height);
  147. }
  148. if (x > workArea.width) {
  149. x = 0;
  150. }
  151. if (y > workArea.height) {
  152. y = 0;
  153. }
  154. }
  155. if (windowState.width < 400) {
  156. windowState.width = 400;
  157. }
  158. if (windowState.height < 300) {
  159. windowState.height = 300;
  160. }
  161. if (x < 0) {
  162. x = 0;
  163. }
  164. if (y < 0) {
  165. y = 0;
  166. }
  167. // 创建主窗体
  168. const currentWindow = new BrowserWindow({
  169. show: false,
  170. backgroundColor: "#FFF", // 桌面端主窗体背景色设置为 `#FFF` Fix https://github.com/siyuan-note/siyuan/issues/4544
  171. width: windowState.width,
  172. height: windowState.height,
  173. minWidth: 493,
  174. minHeight: 376,
  175. x,
  176. y,
  177. fullscreenable: true,
  178. fullscreen: windowState.fullscreen,
  179. trafficLightPosition: {x: 8, y: 8},
  180. webPreferences: {
  181. nodeIntegration: true,
  182. webviewTag: true,
  183. webSecurity: false,
  184. contextIsolation: false,
  185. },
  186. frame: "darwin" === process.platform,
  187. titleBarStyle: "hidden",
  188. icon: path.join(appDir, "stage", "icon-large.png"),
  189. });
  190. require("@electron/remote/main").enable(currentWindow.webContents);
  191. currentWindow.webContents.userAgent = "SiYuan/" + appVer +
  192. " https://b3log.org/siyuan Electron";
  193. currentWindow.webContents.session.setSpellCheckerLanguages(["en-US"]);
  194. // 发起互联网服务请求时绕过安全策略 https://github.com/siyuan-note/siyuan/issues/5516
  195. currentWindow.webContents.session.webRequest.onBeforeSendHeaders(
  196. (details, cb) => {
  197. if (-1 < details.url.indexOf("bili")) {
  198. // B 站不移除 Referer https://github.com/siyuan-note/siyuan/issues/94
  199. cb({requestHeaders: details.requestHeaders});
  200. return;
  201. }
  202. for (let key in details.requestHeaders) {
  203. if ("referer" === key.toLowerCase()) {
  204. delete details.requestHeaders[key];
  205. }
  206. }
  207. cb({requestHeaders: details.requestHeaders});
  208. });
  209. currentWindow.webContents.session.webRequest.onHeadersReceived(
  210. (details, cb) => {
  211. for (let key in details.responseHeaders) {
  212. if ("x-frame-options" === key.toLowerCase()) {
  213. delete details.responseHeaders[key];
  214. } else if ("content-security-policy" === key.toLowerCase()) {
  215. delete details.responseHeaders[key];
  216. } else if ("access-control-allow-origin" === key.toLowerCase()) {
  217. delete details.responseHeaders[key];
  218. }
  219. }
  220. cb({responseHeaders: details.responseHeaders});
  221. });
  222. currentWindow.webContents.on("did-finish-load", () => {
  223. let siyuanOpenURL;
  224. if ("win32" === process.platform || "linux" === process.platform) {
  225. siyuanOpenURL = process.argv.find((arg) => arg.startsWith("siyuan://"));
  226. }
  227. if (siyuanOpenURL) {
  228. if (currentWindow.isMinimized()) {
  229. currentWindow.restore();
  230. }
  231. if (!currentWindow.isVisible()) {
  232. currentWindow.show();
  233. }
  234. currentWindow.focus();
  235. setTimeout(() => { // 等待界面js执行完毕
  236. writeLog(siyuanOpenURL);
  237. currentWindow.webContents.send("siyuan-openurl", siyuanOpenURL);
  238. }, 2000);
  239. }
  240. });
  241. if (windowState.isDevToolsOpened) {
  242. currentWindow.webContents.openDevTools({mode: "bottom"});
  243. }
  244. // 主界面事件监听
  245. currentWindow.once("ready-to-show", () => {
  246. currentWindow.show();
  247. if (windowState.isMaximized) {
  248. currentWindow.maximize();
  249. } else {
  250. currentWindow.unmaximize();
  251. }
  252. if (bootWindow && !bootWindow.isDestroyed()) {
  253. bootWindow.destroy();
  254. }
  255. });
  256. // 加载主界面
  257. currentWindow.loadURL(getServer() + "/stage/build/app/index.html?v=" +
  258. new Date().getTime());
  259. // 菜单
  260. const productName = "SiYuan";
  261. const template = [
  262. {
  263. label: productName,
  264. submenu: [
  265. {
  266. label: `About ${productName}`,
  267. role: "about",
  268. },
  269. {type: "separator"},
  270. {role: "services"},
  271. {type: "separator"},
  272. {
  273. label: `Hide ${productName}`,
  274. role: "hide",
  275. },
  276. {role: "hideOthers"},
  277. {role: "unhide"},
  278. {type: "separator"},
  279. {
  280. label: `Quit ${productName}`,
  281. role: "quit",
  282. },
  283. ],
  284. },
  285. {
  286. role: "editMenu",
  287. submenu: [
  288. {role: "cut"},
  289. {role: "copy"},
  290. {role: "paste"},
  291. {role: "pasteAndMatchStyle", accelerator: "CmdOrCtrl+Shift+C"},
  292. {role: "selectAll"},
  293. ],
  294. },
  295. {
  296. role: "windowMenu",
  297. submenu: [
  298. {role: "minimize"},
  299. {role: "zoom"},
  300. {role: "togglefullscreen"},
  301. {type: "separator"},
  302. {role: "toggledevtools"},
  303. {type: "separator"},
  304. {role: "front"},
  305. ],
  306. },
  307. ];
  308. const menu = Menu.buildFromTemplate(template);
  309. Menu.setApplicationMenu(menu);
  310. // 当前页面链接使用浏览器打开
  311. currentWindow.webContents.on("will-navigate", (event, url) => {
  312. const currentURL = new URL(event.sender.getURL());
  313. if (url.startsWith(getServer(currentURL.port))) {
  314. return;
  315. }
  316. event.preventDefault();
  317. shell.openExternal(url);
  318. });
  319. currentWindow.on("close", (event) => {
  320. if (currentWindow && !currentWindow.isDestroyed()) {
  321. currentWindow.webContents.send("siyuan-save-close", false);
  322. }
  323. event.preventDefault();
  324. });
  325. workspaces.push({
  326. browserWindow: currentWindow,
  327. id: currentWindow.id,
  328. });
  329. };
  330. const showWindow = (wnd) => {
  331. if (!wnd || wnd.isDestroyed()) {
  332. return;
  333. }
  334. if (wnd.isMinimized()) {
  335. wnd.restore();
  336. }
  337. if (!wnd.isVisible()) {
  338. wnd.show();
  339. }
  340. wnd.focus();
  341. };
  342. const initKernel = (workspace, port, lang) => {
  343. return new Promise(async (resolve) => {
  344. bootWindow = new BrowserWindow({
  345. width: screen.getPrimaryDisplay().size.width / 2,
  346. height: screen.getPrimaryDisplay().workAreaSize.height / 2,
  347. frame: false,
  348. icon: path.join(appDir, "stage", "icon-large.png"),
  349. transparent: "linux" !== process.platform,
  350. });
  351. const kernelName = "win32" === process.platform
  352. ? "SiYuan-Kernel.exe"
  353. : "SiYuan-Kernel";
  354. const kernelPath = path.join(appDir, "kernel", kernelName);
  355. if (!fs.existsSync(kernelPath)) {
  356. showErrorWindow("⚠️ 内核文件丢失 Kernel is missing",
  357. "<div>内核可执行文件丢失,请重新安装思源,并将思源加入杀毒软件信任列表。</div><div>The kernel binary is not found, please reinstall SiYuan and add SiYuan into the trust list of your antivirus software.</div>");
  358. bootWindow.destroy();
  359. resolve(false);
  360. return;
  361. }
  362. if (!isDevEnv || workspaces.length > 0) {
  363. if (port && "" !== port) {
  364. kernelPort = port;
  365. } else {
  366. const getAvailablePort = () => {
  367. // https://gist.github.com/mikeal/1840641
  368. return new Promise((portResolve, portReject) => {
  369. const server = net.createServer();
  370. server.on("error", error => {
  371. writeLog(error);
  372. kernelPort = "";
  373. portReject();
  374. });
  375. server.listen(0, () => {
  376. kernelPort = server.address().port;
  377. server.close(() => portResolve(kernelPort));
  378. });
  379. });
  380. };
  381. await getAvailablePort();
  382. }
  383. }
  384. writeLog("got kernel port [" + kernelPort + "]");
  385. if (!kernelPort) {
  386. bootWindow.destroy();
  387. resolve(false);
  388. return;
  389. }
  390. const cmds = ["--port", kernelPort, "--wd", appDir];
  391. if (isDevEnv && workspaces.length === 0) {
  392. cmds.push("--mode", "dev");
  393. }
  394. if (workspace && "" !== workspace) {
  395. cmds.push("--workspace", workspace);
  396. }
  397. if (port && "" !== port) {
  398. cmds.push("--port", port);
  399. }
  400. if (lang && "" !== lang) {
  401. cmds.push("--lang", lang);
  402. }
  403. let cmd = `ui version [${appVer}], booting kernel [${kernelPath} ${cmds.join(
  404. " ")}]`;
  405. writeLog(cmd);
  406. let kernelProcessPid = "";
  407. if (!isDevEnv || workspaces.length > 0) {
  408. const cp = require("child_process");
  409. const kernelProcess = cp.spawn(kernelPath,
  410. cmds, {
  411. detached: false, // 桌面端内核进程不再以游离模式拉起 https://github.com/siyuan-note/siyuan/issues/6336
  412. stdio: "ignore",
  413. },
  414. );
  415. kernelProcessPid = kernelProcess.pid;
  416. writeLog("booted kernel process [pid=" + kernelProcessPid + ", port=" +
  417. kernelPort + "]");
  418. kernelProcess.on("close", (code) => {
  419. writeLog(`kernel [pid=${kernelProcessPid}] exited with code [${code}]`);
  420. if (0 !== code) {
  421. switch (code) {
  422. case 20:
  423. showErrorWindow("⚠️ 数据库被锁定 The database is locked",
  424. "<div>数据库文件正在被其他进程占用,请检查是否同时存在多个内核进程(SiYuan Kernel)服务相同的工作空间。</div><div>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.</div>");
  425. break;
  426. case 21:
  427. showErrorWindow("⚠️ 监听端口 " + kernelPort +
  428. " 失败 Failed to listen to port " + kernelPort,
  429. "<div>监听 " + kernelPort +
  430. " 端口失败,请确保程序拥有网络权限并不受防火墙和杀毒软件阻止。</div><div>Failed to listen to port " +
  431. kernelPort +
  432. ", please make sure the program has network permissions and is not blocked by firewalls and antivirus software.</div>");
  433. break;
  434. case 22:
  435. showErrorWindow(
  436. "⚠️ 创建配置目录失败 Failed to create config directory",
  437. "<div>思源需要在用户家目录下创建配置文件夹(~/.config/siyuan),请确保该路径具有写入权限。</div><div>SiYuan needs to create a configuration folder (~/.config/siyuan) in the user\'s home directory. Please make sure that the path has write permissions.</div>");
  438. break;
  439. case 23:
  440. showErrorWindow(
  441. "⚠️ 无法读写块树文件 Failed to access blocktree file",
  442. "<div>块树文件正在被其他程序锁定或者已经损坏,请删除 工作空间/temp/ 文件夹后重启</div><div>The block tree file is being locked by another program or is corrupted, please delete the workspace/temp/ folder and restart.</div>");
  443. break;
  444. case 24: // 工作空间已被锁定,尝试切换到第一个打开的工作空间
  445. if (workspaces && 0 < workspaces.length) {
  446. showWindow(workspaces[0].browserWindow);
  447. }
  448. showErrorWindow(
  449. "⚠️ 工作空间已被锁定 The workspace is locked",
  450. "<div>该工作空间正在被使用。</div><div>The workspace is in use.</div>");
  451. break;
  452. case 25:
  453. showErrorWindow(
  454. "⚠️ 创建工作空间目录失败 Failed to create workspace directory",
  455. "<div>创建工作空间目录失败。</div><div>Failed to create workspace directory.</div>");
  456. break;
  457. case 0:
  458. case 1: // Fatal error
  459. break;
  460. default:
  461. showErrorWindow(
  462. "⚠️ 内核因未知原因退出 The kernel exited for unknown reasons",
  463. `<div>思源内核因未知原因退出 [code=${code}],请尝试重启操作系统后再启动思源。如果该问题依然发生,请检查杀毒软件是否阻止思源内核启动。</div>
  464. <div>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.</div>`);
  465. break;
  466. }
  467. bootWindow.destroy();
  468. resolve(false);
  469. }
  470. });
  471. }
  472. let gotVersion = false;
  473. let apiData;
  474. let count = 0;
  475. writeLog("checking kernel version");
  476. while (!gotVersion && count < 15) {
  477. try {
  478. const apiResult = await fetch(getServer() + "/api/system/version");
  479. apiData = await apiResult.json();
  480. gotVersion = true;
  481. bootWindow.setResizable(false);
  482. bootWindow.loadURL(getServer() + "/appearance/boot/index.html");
  483. bootWindow.show();
  484. } catch (e) {
  485. writeLog("get kernel version failed: " + e.message);
  486. await sleep(100);
  487. } finally {
  488. count++;
  489. if (14 < count) {
  490. writeLog("get kernel ver failed");
  491. showErrorWindow(
  492. "⚠️ 获取内核服务端口失败 Failed to get kernel serve port",
  493. "<div>获取内核服务端口失败,请确保程序拥有网络权限并不受防火墙和杀毒软件阻止。</div><div>Failed to get kernel serve port, please make sure the program has network permissions and is not blocked by firewalls and antivirus software.</div>");
  494. bootWindow.destroy();
  495. resolve(false);
  496. }
  497. }
  498. }
  499. if (!gotVersion) {
  500. return;
  501. }
  502. if (0 === apiData.code) {
  503. writeLog("got kernel version [" + apiData.data + "]");
  504. if (!isDevEnv && apiData.data !== appVer) {
  505. writeLog(
  506. `kernel [${apiData.data}] is running, shutdown it now and then start kernel [${appVer}]`);
  507. fetch(getServer() + "/api/system/exit", {method: "POST"});
  508. bootWindow.destroy();
  509. resolve(false);
  510. } else {
  511. let progressing = false;
  512. while (!progressing) {
  513. try {
  514. const progressResult = await fetch(
  515. getServer() + "/api/system/bootProgress");
  516. const progressData = await progressResult.json();
  517. if (progressData.data.progress >= 100) {
  518. resolve(true);
  519. progressing = true;
  520. } else {
  521. await sleep(100);
  522. }
  523. } catch (e) {
  524. writeLog("get boot progress failed: " + e.message);
  525. fetch(getServer() + "/api/system/exit", {method: "POST"});
  526. bootWindow.destroy();
  527. resolve(false);
  528. progressing = true;
  529. }
  530. }
  531. }
  532. } else {
  533. writeLog(`get kernel version failed: ${apiData.code}, ${apiData.msg}`);
  534. resolve(false);
  535. }
  536. });
  537. };
  538. app.setAsDefaultProtocolClient("siyuan");
  539. app.commandLine.appendSwitch("disable-web-security");
  540. app.commandLine.appendSwitch("auto-detect", "false");
  541. app.commandLine.appendSwitch("no-proxy-server");
  542. app.commandLine.appendSwitch("enable-features", "PlatformHEVCDecoderSupport");
  543. app.setPath("userData", app.getPath("userData") + "-Electron"); // `~/.config` 下 Electron 相关文件夹名称改为 `SiYuan-Electron` https://github.com/siyuan-note/siyuan/issues/3349
  544. app.whenReady().then(() => {
  545. let resetWindowStateOnRestart = false;
  546. const resetTrayMenu = (tray, lang, mainWindow) => {
  547. const trayMenuTemplate = [
  548. {
  549. label: mainWindow.isVisible()
  550. ? lang.hideWindow
  551. : lang.showWindow,
  552. click: () => {
  553. showHideWindow(tray, lang, mainWindow);
  554. },
  555. },
  556. {
  557. label: lang.officialWebsite,
  558. click: () => {
  559. shell.openExternal("https://b3log.org/siyuan/");
  560. },
  561. },
  562. {
  563. label: lang.openSource,
  564. click: () => {
  565. shell.openExternal("https://github.com/siyuan-note/siyuan");
  566. },
  567. },
  568. {
  569. label: lang.resetWindow,
  570. type: "checkbox",
  571. click: v => {
  572. resetWindowStateOnRestart = v.checked;
  573. mainWindow.webContents.send("siyuan-save-close", true);
  574. },
  575. },
  576. {
  577. label: lang.quit,
  578. click: () => {
  579. mainWindow.webContents.send("siyuan-save-close", true);
  580. },
  581. },
  582. ];
  583. if ("win32" === process.platform) {
  584. // Windows 端支持窗口置顶 https://github.com/siyuan-note/siyuan/issues/6860
  585. trayMenuTemplate.splice(1, 0, {
  586. label: mainWindow.isAlwaysOnTop()
  587. ? lang.cancelWindowTop
  588. : lang.setWindowTop,
  589. click: () => {
  590. if (!mainWindow.isAlwaysOnTop()) {
  591. mainWindow.setAlwaysOnTop(true);
  592. } else {
  593. mainWindow.setAlwaysOnTop(false);
  594. }
  595. resetTrayMenu(tray, lang, mainWindow);
  596. },
  597. });
  598. }
  599. const contextMenu = Menu.buildFromTemplate(trayMenuTemplate);
  600. tray.setContextMenu(contextMenu);
  601. };
  602. const hideWindow = (wnd) => {
  603. // 通过 `Alt+M` 最小化后焦点回到先前的窗口 https://github.com/siyuan-note/siyuan/issues/7275
  604. wnd.minimize();
  605. wnd.hide();
  606. };
  607. const showHideWindow = (tray, lang, mainWindow) => {
  608. if (!mainWindow.isVisible()) {
  609. if (mainWindow.isMinimized()) {
  610. mainWindow.restore();
  611. }
  612. mainWindow.show();
  613. } else {
  614. hideWindow(mainWindow);
  615. }
  616. resetTrayMenu(tray, lang, mainWindow);
  617. };
  618. ipcMain.on("siyuan-first-quit", () => {
  619. app.exit();
  620. });
  621. ipcMain.on("siyuan-show", (event, id) => {
  622. showWindow(BrowserWindow.fromId(id));
  623. });
  624. ipcMain.on("siyuan-config-tray", (event, data) => {
  625. workspaces.find(item => {
  626. if (item.id === data.id) {
  627. hideWindow(item.browserWindow);
  628. if ("win32" === process.platform || "linux" === process.platform) {
  629. resetTrayMenu(item.tray, data.languages, item.browserWindow);
  630. }
  631. return true;
  632. }
  633. });
  634. });
  635. ipcMain.on("siyuan-closetab", (event, data) => {
  636. BrowserWindow.getAllWindows().forEach(item => {
  637. item.webContents.send("siyuan-closetab", data);
  638. });
  639. });
  640. ipcMain.on("siyuan-export-pdf", (event, data) => {
  641. BrowserWindow.fromId(data.id).webContents.send("siyuan-export-pdf", data);
  642. });
  643. ipcMain.on("siyuan-export-close", (event, id) => {
  644. BrowserWindow.fromId(id).webContents.send("siyuan-export-close", id);
  645. });
  646. ipcMain.on("siyuan-export-prevent", (event, id) => {
  647. BrowserWindow.fromId(id).webContents.on("will-navigate", (event, url) => {
  648. const currentURL = new URL(event.sender.getURL());
  649. if (url.startsWith(getServer(currentURL.port))) {
  650. return;
  651. }
  652. event.preventDefault();
  653. shell.openExternal(url);
  654. });
  655. });
  656. ipcMain.on("siyuan-quit", (event, id) => {
  657. const mainWindow = BrowserWindow.fromId(id);
  658. let tray;
  659. workspaces.find((item, index) => {
  660. if (item.id === id) {
  661. if (workspaces.length > 1) {
  662. mainWindow.destroy();
  663. }
  664. tray = item.tray;
  665. workspaces.splice(index, 1);
  666. return true;
  667. }
  668. });
  669. if (tray && "win32" === process.platform) {
  670. tray.destroy();
  671. }
  672. if (workspaces.length === 0) {
  673. try {
  674. if (resetWindowStateOnRestart) {
  675. fs.writeFileSync(windowStatePath, "{}");
  676. } else {
  677. const bounds = mainWindow.getBounds();
  678. fs.writeFileSync(windowStatePath, JSON.stringify({
  679. isMaximized: mainWindow.isMaximized(),
  680. fullscreen: mainWindow.isFullScreen(),
  681. isDevToolsOpened: mainWindow.webContents.isDevToolsOpened(),
  682. x: bounds.x,
  683. y: bounds.y,
  684. width: bounds.width,
  685. height: bounds.height,
  686. }));
  687. }
  688. } catch (e) {
  689. writeLog(e);
  690. }
  691. app.exit();
  692. globalShortcut.unregisterAll();
  693. writeLog("exited ui");
  694. }
  695. });
  696. ipcMain.on("siyuan-openwindow", (event, data) => {
  697. const win = new BrowserWindow({
  698. show: true,
  699. backgroundColor: "#FFF",
  700. trafficLightPosition: {x: 8, y: 13},
  701. width: screen.getPrimaryDisplay().size.width * 0.7,
  702. height: screen.getPrimaryDisplay().size.height * 0.9,
  703. minWidth: 493,
  704. minHeight: 376,
  705. fullscreenable: true,
  706. frame: "darwin" === process.platform,
  707. icon: path.join(appDir, "stage", "icon-large.png"),
  708. titleBarStyle: "hidden",
  709. webPreferences: {
  710. contextIsolation: false,
  711. nodeIntegration: true,
  712. webviewTag: true,
  713. webSecurity: false,
  714. },
  715. });
  716. win.loadURL(data);
  717. require("@electron/remote/main").enable(win.webContents);
  718. });
  719. ipcMain.on("siyuan-open-workspace", (event, data) => {
  720. const foundWorkspace = workspaces.find((item) => {
  721. if (item.workspaceDir === data.workspace) {
  722. showWindow(item.browserWindow);
  723. return true;
  724. }
  725. });
  726. if (!foundWorkspace) {
  727. initKernel(data.workspace, "", data.lang).then((isSucc) => {
  728. if (isSucc) {
  729. boot();
  730. }
  731. });
  732. }
  733. });
  734. ipcMain.on("siyuan-init", async (event, data) => {
  735. const exitWS = workspaces.find(item => {
  736. if (data.id === item.id && item.workspaceDir) {
  737. return true;
  738. }
  739. });
  740. if (exitWS) {
  741. return;
  742. }
  743. let tray;
  744. if ("win32" === process.platform || "linux" === process.platform) {
  745. // 系统托盘
  746. tray = new Tray(path.join(appDir, "stage", "icon-large.png"));
  747. tray.setToolTip(`${path.basename(data.workspaceDir)} - SiYuan v${appVer}`);
  748. const mainWindow = BrowserWindow.fromId(data.id);
  749. resetTrayMenu(tray, data.languages, mainWindow);
  750. tray.on("click", () => {
  751. showHideWindow(tray, data.languages, mainWindow);
  752. });
  753. }
  754. workspaces.find(item => {
  755. if (data.id === item.id) {
  756. item.workspaceDir = data.workspaceDir;
  757. item.tray = tray;
  758. return true;
  759. }
  760. });
  761. await fetch(getServer(data.port) + "/api/system/uiproc?pid=" + process.pid,
  762. {method: "POST"});
  763. });
  764. ipcMain.on("siyuan-hotkey", (event, data) => {
  765. globalShortcut.unregisterAll();
  766. if (!data.hotkey) {
  767. return;
  768. }
  769. globalShortcut.register(data.hotkey, () => {
  770. workspaces.forEach(item => {
  771. const mainWindow = item.browserWindow;
  772. if (mainWindow.isMinimized()) {
  773. mainWindow.restore();
  774. if (!mainWindow.isVisible()) {
  775. mainWindow.show();
  776. }
  777. } else {
  778. if (mainWindow.isVisible()) {
  779. if (1 === workspaces.length) { // 改进 `Alt+M` 激活窗口 https://github.com/siyuan-note/siyuan/issues/7258
  780. if (!mainWindow.isFocused()) {
  781. mainWindow.show();
  782. } else {
  783. hideWindow(mainWindow);
  784. }
  785. } else {
  786. hideWindow(mainWindow);
  787. }
  788. } else {
  789. mainWindow.show();
  790. }
  791. }
  792. if ("win32" === process.platform || "linux" === process.platform) {
  793. resetTrayMenu(item.tray, data.languages, mainWindow);
  794. }
  795. });
  796. });
  797. });
  798. ipcMain.on("siyuan-lock-screen", () => {
  799. BrowserWindow.getAllWindows().forEach(item => {
  800. item.webContents.send("siyuan-lock-screen");
  801. });
  802. });
  803. if (firstOpen) {
  804. const firstOpenWindow = new BrowserWindow({
  805. width: screen.getPrimaryDisplay().size.width * 0.6,
  806. height: screen.getPrimaryDisplay().workAreaSize.height * 0.8,
  807. frame: false,
  808. icon: path.join(appDir, "stage", "icon-large.png"),
  809. transparent: "linux" !== process.platform,
  810. webPreferences: {
  811. nodeIntegration: true,
  812. webviewTag: true,
  813. webSecurity: false,
  814. contextIsolation: false,
  815. },
  816. });
  817. require("@electron/remote/main").enable(firstOpenWindow.webContents);
  818. let initHTMLPath = path.join(appDir, "app", "electron", "init.html");
  819. if (isDevEnv) {
  820. initHTMLPath = path.join(appDir, "electron", "init.html");
  821. }
  822. // 改进桌面端初始化时使用的外观语言 https://github.com/siyuan-note/siyuan/issues/6803
  823. let languages = app.getPreferredSystemLanguages();
  824. let language = languages && 0 < languages.length && "zh-Hans-CN" ===
  825. languages[0] ? "zh_CN" : "en_US";
  826. firstOpenWindow.loadFile(
  827. initHTMLPath, {
  828. query: {
  829. lang: language,
  830. home: app.getPath("home"),
  831. v: appVer,
  832. icon: path.join(appDir, "stage", "icon-large.png"),
  833. },
  834. });
  835. firstOpenWindow.show();
  836. // 初始化启动
  837. ipcMain.on("siyuan-first-init", (event, data) => {
  838. initKernel(data.workspace, "", data.lang).then((isSucc) => {
  839. if (isSucc) {
  840. boot();
  841. }
  842. });
  843. firstOpenWindow.destroy();
  844. });
  845. } else {
  846. const getArg = (name) => {
  847. for (let i = 0; i < process.argv.length; i++) {
  848. if (process.argv[i].startsWith(name)) {
  849. return process.argv[i].split("=")[1];
  850. }
  851. }
  852. };
  853. const workspace = getArg("--workspace");
  854. if (workspace) {
  855. writeLog("got arg [--workspace=" + workspace + "]");
  856. }
  857. const port = getArg("--port");
  858. if (port) {
  859. writeLog("got arg [--port=" + port + "]");
  860. }
  861. initKernel(workspace, port, "").then((isSucc) => {
  862. if (isSucc) {
  863. boot();
  864. }
  865. });
  866. }
  867. });
  868. app.on("open-url", (event, url) => { // for macOS
  869. if (url.startsWith("siyuan://")) {
  870. workspaces.forEach(item => {
  871. if (item.browserWindow && !item.browserWindow.isDestroyed()) {
  872. item.browserWindow.webContents.send("siyuan-openurl", url);
  873. }
  874. });
  875. }
  876. });
  877. app.on("second-instance", (event, argv) => {
  878. writeLog("second-instance [" + argv + "]");
  879. let workspace = argv.find((arg) => arg.startsWith("--workspace="));
  880. if (workspace) {
  881. workspace = workspace.split("=")[1];
  882. writeLog("got second-instance arg [--workspace=" + workspace + "]");
  883. }
  884. let port = argv.find((arg) => arg.startsWith("--port="));
  885. if (port) {
  886. port = port.split("=")[1];
  887. writeLog("got second-instance arg [--port=" + port + "]");
  888. } else {
  889. port = 0;
  890. }
  891. const foundWorkspace = workspaces.find(item => {
  892. if (item.browserWindow && !item.browserWindow.isDestroyed()) {
  893. if (workspace && workspace === item.workspaceDir) {
  894. showWindow(item.browserWindow);
  895. return true;
  896. }
  897. }
  898. });
  899. if (foundWorkspace) {
  900. return;
  901. }
  902. if (workspace) {
  903. initKernel(workspace, port, "").then((isSucc) => {
  904. if (isSucc) {
  905. boot();
  906. }
  907. });
  908. return;
  909. }
  910. const siyuanURL = argv.find((arg) => arg.startsWith("siyuan://"));
  911. workspaces.forEach(item => {
  912. if (item.browserWindow && !item.browserWindow.isDestroyed() && siyuanURL) {
  913. item.browserWindow.webContents.send("siyuan-openurl", siyuanURL);
  914. }
  915. });
  916. if (!siyuanURL && 0 < workspaces.length) {
  917. showWindow(workspaces[0].browserWindow);
  918. }
  919. });
  920. app.on("activate", () => {
  921. if (workspaces.length > 0) {
  922. const mainWindow = workspaces[0].browserWindow;
  923. if (mainWindow && !mainWindow.isDestroyed()) {
  924. mainWindow.show();
  925. }
  926. }
  927. if (BrowserWindow.getAllWindows().length === 0) {
  928. boot();
  929. }
  930. });
  931. // 在编辑器内打开链接的处理,比如 iframe 上的打开链接。
  932. app.on("web-contents-created", (webContentsCreatedEvent, contents) => {
  933. contents.setWindowOpenHandler((details) => {
  934. shell.openExternal(details.url);
  935. return {action: "deny"};
  936. });
  937. });
  938. app.on("before-quit", (event) => {
  939. workspaces.forEach(item => {
  940. if (item.browserWindow && !item.browserWindow.isDestroyed()) {
  941. event.preventDefault();
  942. item.browserWindow.webContents.send("siyuan-save-close", true);
  943. }
  944. });
  945. });
  946. const {powerMonitor} = require("electron");
  947. powerMonitor.on("suspend", () => {
  948. writeLog("system suspend");
  949. });
  950. powerMonitor.on("resume", async () => {
  951. // 桌面端系统休眠唤醒后判断网络连通性后再执行数据同步 https://github.com/siyuan-note/siyuan/issues/6687
  952. writeLog("system resume");
  953. const isOnline = async () => {
  954. try {
  955. const result = await fetch("https://www.baidu.com", {timeout: 1000});
  956. return 200 === result.status;
  957. } catch (e) {
  958. try {
  959. const result = await fetch("https://icanhazip.com", {timeout: 1000});
  960. return 200 === result.status;
  961. } catch (e) {
  962. return false;
  963. }
  964. }
  965. };
  966. let online = false;
  967. for (let i = 0; i < 7; i++) {
  968. if (await isOnline()) {
  969. online = true;
  970. break;
  971. }
  972. writeLog("network is offline");
  973. await sleep(1000);
  974. }
  975. if (!online) {
  976. writeLog("network is offline, do not sync after system resume");
  977. return;
  978. }
  979. workspaces.forEach(item => {
  980. const currentURL = new URL(item.browserWindow.getURL());
  981. const server = getServer(currentURL.port);
  982. writeLog(
  983. "sync after system resume [" + server + "/api/sync/performSync" + "]");
  984. fetch(server + "/api/sync/performSync", {method: "POST"});
  985. });
  986. });
  987. powerMonitor.on("shutdown", () => {
  988. writeLog("system shutdown");
  989. workspaces.forEach(item => {
  990. const currentURL = new URL(item.browserWindow.getURL());
  991. fetch(getServer(currentURL.port) + "/api/system/exit", {method: "POST"});
  992. });
  993. });