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