import fs from "fs"; import path from "path"; import url from "url"; import port from "./port.js"; import optionService from "./options.js"; import log from "./log.js"; import sqlInit from "./sql_init.js"; import cls from "./cls.js"; import keyboardActionsService from "./keyboard_actions.js"; import remoteMain from "@electron/remote/main/index.js"; import { BrowserWindow, shell, type App, type BrowserWindowConstructorOptions, type WebContents } from "electron"; import { dialog, ipcMain } from "electron"; import { formatDownloadTitle, isDev, isMac, isWindows } from "./utils.js"; import { fileURLToPath } from "url"; import { dirname } from "path"; import { t } from "i18next"; // Prevent the window being garbage collected let mainWindow: BrowserWindow | null; let setupWindow: BrowserWindow | null; async function createExtraWindow(extraWindowHash: string) { const spellcheckEnabled = optionService.getOptionBool("spellCheckEnabled"); const { BrowserWindow } = await import("electron"); const win = new BrowserWindow({ width: 1000, height: 800, title: "TriliumNext Notes", webPreferences: { nodeIntegration: true, contextIsolation: false, spellcheck: spellcheckEnabled }, ...getWindowExtraOpts(), icon: getIcon() }); win.setMenuBarVisibility(false); win.loadURL(`http://127.0.0.1:${port}/?extraWindow=1${extraWindowHash}`); configureWebContents(win.webContents, spellcheckEnabled); } ipcMain.on("create-extra-window", (event, arg) => { createExtraWindow(arg.extraWindowHash); }); interface ExportAsPdfOpts { title: string; } ipcMain.on("export-as-pdf", async (e, opts: ExportAsPdfOpts) => { const browserWindow = BrowserWindow.fromWebContents(e.sender); if (!browserWindow) { return; } const filePath = dialog.showSaveDialogSync(browserWindow, { defaultPath: formatDownloadTitle(opts.title, "file", "application/pdf"), filters: [ { name: t("pdf.export_filter"), extensions: [ "pdf" ] } ] }); if (!filePath) { return; } // TODO: Report if there is an error in generating the PDF. const buffer = await browserWindow.webContents.printToPDF({}); // TODO: Report if there was an error in saving the PDF. fs.writeFileSync(filePath, buffer); shell.openPath(filePath); }); async function createMainWindow(app: App) { if ("setUserTasks" in app) { app.setUserTasks([ { program: process.execPath, arguments: "--new-window", iconPath: process.execPath, iconIndex: 0, title: "Open New Window", description: "Open new window" } ]); } const windowStateKeeper = (await import("electron-window-state")).default; // should not be statically imported const mainWindowState = windowStateKeeper({ // default window width & height, so it's usable on a 1600 * 900 display (including some extra panels etc.) defaultWidth: 1200, defaultHeight: 800 }); const spellcheckEnabled = optionService.getOptionBool("spellCheckEnabled"); const { BrowserWindow } = await import("electron"); // should not be statically imported mainWindow = new BrowserWindow({ x: mainWindowState.x, y: mainWindowState.y, width: mainWindowState.width, height: mainWindowState.height, title: "TriliumNext Notes", webPreferences: { nodeIntegration: true, contextIsolation: false, spellcheck: spellcheckEnabled, webviewTag: true }, icon: getIcon(), ...getWindowExtraOpts() }); mainWindowState.manage(mainWindow); mainWindow.setMenuBarVisibility(false); mainWindow.loadURL(`http://127.0.0.1:${port}`); mainWindow.on("closed", () => (mainWindow = null)); configureWebContents(mainWindow.webContents, spellcheckEnabled); app.on("second-instance", (event, commandLine) => { if (commandLine.includes("--new-window")) { createExtraWindow(""); } else if (mainWindow) { // Someone tried to run a second instance, we should focus our window. // see www.ts "requestSingleInstanceLock" for the rest of this logic with explanation if (mainWindow.isMinimized()) { mainWindow.restore(); } mainWindow.focus(); } }); } function getWindowExtraOpts() { const extraOpts: Partial = {}; if (!optionService.getOptionBool("nativeTitleBarVisible")) { if (isMac) { extraOpts.titleBarStyle = "hiddenInset"; extraOpts.titleBarOverlay = true; } else if (isWindows) { extraOpts.titleBarStyle = "hidden"; extraOpts.titleBarOverlay = true; } else { // Linux or other platforms. extraOpts.frame = false; } } // Window effects (Mica) if (optionService.getOptionBool("backgroundEffects") && isWindows) { extraOpts.backgroundMaterial = "auto"; } return extraOpts; } function configureWebContents(webContents: WebContents, spellcheckEnabled: boolean) { remoteMain.enable(webContents); webContents.setWindowOpenHandler((details) => { async function openExternal() { (await import("electron")).shell.openExternal(details.url); } openExternal(); return { action: "deny" }; }); // prevent drag & drop to navigate away from trilium webContents.on("will-navigate", (ev, targetUrl) => { const parsedUrl = url.parse(targetUrl); // we still need to allow internal redirects from setup and migration pages if (!["localhost", "127.0.0.1"].includes(parsedUrl.hostname || "") || (parsedUrl.path && parsedUrl.path !== "/" && parsedUrl.path !== "/?")) { ev.preventDefault(); } }); if (spellcheckEnabled) { const languageCodes = optionService .getOption("spellCheckLanguageCode") .split(",") .map((code) => code.trim()); webContents.session.setSpellCheckerLanguages(languageCodes); } } function getIcon() { return path.join(dirname(fileURLToPath(import.meta.url)), "../../images/app-icons/png/256x256" + (isDev ? "-dev" : "") + ".png"); } async function createSetupWindow() { const { BrowserWindow } = await import("electron"); // should not be statically imported setupWindow = new BrowserWindow({ width: 800, height: 800, title: "TriliumNext Notes Setup", icon: getIcon(), webPreferences: { // necessary for e.g. utils.isElectron() nodeIntegration: true } }); setupWindow.setMenuBarVisibility(false); setupWindow.loadURL(`http://127.0.0.1:${port}`); setupWindow.on("closed", () => (setupWindow = null)); } function closeSetupWindow() { if (setupWindow) { setupWindow.close(); } } async function registerGlobalShortcuts() { const { globalShortcut } = await import("electron"); await sqlInit.dbReady; const allActions = keyboardActionsService.getKeyboardActions(); for (const action of allActions) { if (!action.effectiveShortcuts) { continue; } for (const shortcut of action.effectiveShortcuts) { if (shortcut.startsWith("global:")) { const translatedShortcut = shortcut.substr(7); const result = globalShortcut.register( translatedShortcut, cls.wrap(() => { if (!mainWindow) { return; } // window may be hidden / not in focus mainWindow.focus(); mainWindow.webContents.send("globalShortcut", action.actionName); }) ); if (result) { log.info(`Registered global shortcut ${translatedShortcut} for action ${action.actionName}`); } else { log.info(`Could not register global shortcut ${translatedShortcut}`); } } } } } function getMainWindow() { return mainWindow; } export default { createMainWindow, createSetupWindow, closeSetupWindow, registerGlobalShortcuts, getMainWindow };