diff --git a/apps/client/src/desktop.ts b/apps/client/src/desktop.ts index cca6c8c0f..dc8390715 100644 --- a/apps/client/src/desktop.ts +++ b/apps/client/src/desktop.ts @@ -62,6 +62,8 @@ function initOnElectron() { if (options.get("nativeTitleBarVisible") !== "true") { initTitleBarButtons(style, currentWindow); } + + electron.ipcRenderer.send("ipcReady"); } function initTitleBarButtons(style: CSSStyleDeclaration, currentWindow: Electron.BrowserWindow) { diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index 54025d690..44c85a24e 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -1377,7 +1377,8 @@ }, "tray": { "title": "System Tray", - "enable_tray": "Enable tray (Trilium needs to be restarted for this change to take effect)" + "enable_tray": "Enable tray (Trilium needs to be restarted for this change to take effect)", + "persistant_tray": "Always show the tray icon, even if no windows are currently open" }, "heading_style": { "title": "Heading Style", diff --git a/apps/client/src/translations/ro/translation.json b/apps/client/src/translations/ro/translation.json index 0f443bc53..bd93b4c92 100644 --- a/apps/client/src/translations/ro/translation.json +++ b/apps/client/src/translations/ro/translation.json @@ -1291,6 +1291,7 @@ }, "tray": { "enable_tray": "Activează system tray-ul (este necesară repornirea aplicației pentru a avea efect)", + "persistant-tray": "Afișează întotdeauna iconița, chiar dacă nu este deschisă nicio fereastră.", "title": "Tray-ul de sistem" }, "update_available": { diff --git a/apps/client/src/widgets/type_widgets/options/other.tsx b/apps/client/src/widgets/type_widgets/options/other.tsx index 6ac92b420..725ee1b45 100644 --- a/apps/client/src/widgets/type_widgets/options/other.tsx +++ b/apps/client/src/widgets/type_widgets/options/other.tsx @@ -87,6 +87,8 @@ function SearchEngineSettings() { function TrayOptionsSettings() { const [ disableTray, setDisableTray ] = useTriliumOptionBool("disableTray"); + const [ persistantTray, setPersistantTray ] = useTriliumOptionBool("persistantTray"); + return ( @@ -96,6 +98,12 @@ function TrayOptionsSettings() { currentValue={!disableTray} onChange={trayEnabled => setDisableTray(!trayEnabled)} /> + setPersistantTray(enabled)} + /> ) } diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 3617c4c9f..09789bfd7 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -54,7 +54,8 @@ async function main() { // for applications and their menu bar to stay active until the user quits // explicitly with Cmd + Q. app.on("window-all-closed", () => { - if (process.platform !== "darwin") { + const persistantTrayEnabled = options.getOptionBool("persistantTray"); + if (!persistantTrayEnabled && process.platform !== "darwin") { app.quit(); } }); diff --git a/apps/server/src/routes/api/options.ts b/apps/server/src/routes/api/options.ts index 05f659db8..1181f783e 100644 --- a/apps/server/src/routes/api/options.ts +++ b/apps/server/src/routes/api/options.ts @@ -75,9 +75,9 @@ const ALLOWED_OPTIONS = new Set([ "highlightsList", "checkForUpdates", "disableTray", + "persistantTray", "eraseUnusedAttachmentsAfterSeconds", "eraseUnusedAttachmentsAfterTimeScale", - "disableTray", "customSearchEngineName", "customSearchEngineUrl", "promotedAttributesOpenInRibbon", diff --git a/apps/server/src/services/options_init.ts b/apps/server/src/services/options_init.ts index e170b347c..d2e736549 100644 --- a/apps/server/src/services/options_init.ts +++ b/apps/server/src/services/options_init.ts @@ -124,6 +124,7 @@ const defaultOptions: DefaultOption[] = [ { name: "highlightsList", value: '["underline","color","bgColor"]', isSynced: true }, { name: "checkForUpdates", value: "true", isSynced: true }, { name: "disableTray", value: "false", isSynced: false }, + { name: "persistantTray", value: "false", isSynced: false }, { name: "eraseUnusedAttachmentsAfterSeconds", value: "2592000", isSynced: true }, // default 30 days { name: "eraseUnusedAttachmentsAfterTimeScale", value: "86400", isSynced: true }, // default 86400 seconds = Day { name: "logRetentionDays", value: "90", isSynced: false }, // default 90 days diff --git a/apps/server/src/services/tray.ts b/apps/server/src/services/tray.ts index 504e81b21..f5f3335ae 100644 --- a/apps/server/src/services/tray.ts +++ b/apps/server/src/services/tray.ts @@ -1,4 +1,4 @@ -import electron from "electron"; +import electron, { app } from "electron"; import type { BrowserWindow, Tray } from "electron"; import { default as i18next, t } from "i18next"; import path from "path"; @@ -17,6 +17,7 @@ import windowService from "./window.js"; let tray: Tray; // `mainWindow.isVisible` doesn't work with `mainWindow.show` and `mainWindow.hide` - it returns `false` when the window // is minimized + const windowVisibilityMap: Record = {};; // Dictionary for storing window ID and its visibility status function getTrayIconPath() { @@ -107,7 +108,6 @@ function updateTrayMenu() { if (!tray) { return; } - const lastFocusedWindow = windowService.getLastFocusedWindow(); const allWindows = windowService.getAllWindows(); updateWindowVisibilityMap(allWindows); @@ -119,19 +119,22 @@ function updateTrayMenu() { } function openNewWindow() { + const lastFocusedWindow = windowService.getLastFocusedWindow(); if (lastFocusedWindow){ lastFocusedWindow.webContents.send("globalShortcut", "openNewWindow"); } } - function triggerKeyboardAction(actionName: KeyboardActionNames) { - if (lastFocusedWindow){ + async function triggerKeyboardAction(actionName: KeyboardActionNames) { + const lastFocusedWindow = await getCurrentWindow(); + if (lastFocusedWindow) { lastFocusedWindow.webContents.send("globalShortcut", actionName); ensureVisible(lastFocusedWindow); } } - function openInSameTab(note: BNote | BRecentNote) { + async function openInSameTab(note: BNote | BRecentNote) { + const lastFocusedWindow = await getCurrentWindow(); if (lastFocusedWindow){ lastFocusedWindow.webContents.send("openInSameTab", note.noteId); ensureVisible(lastFocusedWindow); @@ -310,6 +313,16 @@ function createTray() { i18next.on("languageChanged", updateTrayMenu); } +async function getCurrentWindow(): Promise { + if (!windowService.getMainWindow()) { + // If no windows are open, create a new main window + await windowService.createMainWindow(app); + return windowService.getMainWindow(); + } else { + return windowService.getLastFocusedWindow(); + } +} + export default { createTray }; diff --git a/apps/server/src/services/window.ts b/apps/server/src/services/window.ts index c2fab2ac3..f635515ba 100644 --- a/apps/server/src/services/window.ts +++ b/apps/server/src/services/window.ts @@ -8,7 +8,7 @@ import sqlInit from "./sql_init.js"; import cls from "./cls.js"; import keyboardActionsService from "./keyboard_actions.js"; import electron from "electron"; -import type { App, BrowserWindowConstructorOptions, BrowserWindow, WebContents, IpcMainEvent } from "electron"; +import { App, BrowserWindowConstructorOptions, BrowserWindow, WebContents, ipcMain, IpcMainEvent } from "electron"; import { formatDownloadTitle, isDev, isMac, isWindows } from "./utils.js"; import { t } from "i18next"; import { RESOURCE_DIR } from "./resource_dir.js"; @@ -21,6 +21,7 @@ let setupWindow: BrowserWindow | null; let allWindows: BrowserWindow[] = []; // // Used to store all windows, sorted by the order of focus. function trackWindowFocus(win: BrowserWindow) { + // We need to get the last focused window from allWindows. If the last window is closed, we return the previous window. // Therefore, we need to push the window into the allWindows array every time it gets focused. win.on("focus", () => { @@ -212,11 +213,14 @@ async function createMainWindow(app: App) { mainWindowState.manage(mainWindow); mainWindow.setMenuBarVisibility(false); - mainWindow.loadURL(`http://127.0.0.1:${port}`); mainWindow.on("closed", () => (mainWindow = null)); - configureWebContents(mainWindow.webContents, spellcheckEnabled); + await mainWindow.loadURL(`http://127.0.0.1:${port}`); + await configureWebContents(mainWindow.webContents, spellcheckEnabled); trackWindowFocus(mainWindow); + + await waitForIpc(mainWindow); + } function getWindowExtraOpts() { @@ -385,6 +389,21 @@ function getAllWindows() { return allWindows; } +function waitForIpc(win: BrowserWindow) { + return new Promise((resolve, reject) => { + const handler = (ev: IpcMainEvent, ) => { + const senderWindow = BrowserWindow.fromWebContents(ev.sender); + + if (senderWindow === win) { + ipcMain.off("ipcReady", handler); + resolve(); + } + }; + + ipcMain.on("ipcReady", handler); + }); +} + export default { createMainWindow, createExtraWindow, diff --git a/packages/commons/src/lib/options_interface.ts b/packages/commons/src/lib/options_interface.ts index 0e044d37c..62dd2a74b 100644 --- a/packages/commons/src/lib/options_interface.ts +++ b/packages/commons/src/lib/options_interface.ts @@ -121,6 +121,7 @@ export interface OptionDefinitions extends KeyboardShortcutsOptions