client: add support for persistant system tray icon

This commit is contained in:
Adorian Doran 2025-11-17 15:36:02 +02:00
parent bbcc670655
commit 35e11807e5
10 changed files with 58 additions and 11 deletions

View File

@ -62,6 +62,8 @@ function initOnElectron() {
if (options.get("nativeTitleBarVisible") !== "true") { if (options.get("nativeTitleBarVisible") !== "true") {
initTitleBarButtons(style, currentWindow); initTitleBarButtons(style, currentWindow);
} }
electron.ipcRenderer.send("ipcReady");
} }
function initTitleBarButtons(style: CSSStyleDeclaration, currentWindow: Electron.BrowserWindow) { function initTitleBarButtons(style: CSSStyleDeclaration, currentWindow: Electron.BrowserWindow) {

View File

@ -1377,7 +1377,8 @@
}, },
"tray": { "tray": {
"title": "System 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": { "heading_style": {
"title": "Heading Style", "title": "Heading Style",

View File

@ -1291,6 +1291,7 @@
}, },
"tray": { "tray": {
"enable_tray": "Activează system tray-ul (este necesară repornirea aplicației pentru a avea efect)", "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" "title": "Tray-ul de sistem"
}, },
"update_available": { "update_available": {

View File

@ -87,6 +87,8 @@ function SearchEngineSettings() {
function TrayOptionsSettings() { function TrayOptionsSettings() {
const [ disableTray, setDisableTray ] = useTriliumOptionBool("disableTray"); const [ disableTray, setDisableTray ] = useTriliumOptionBool("disableTray");
const [ persistantTray, setPersistantTray ] = useTriliumOptionBool("persistantTray");
return ( return (
<OptionsSection title={t("tray.title")}> <OptionsSection title={t("tray.title")}>
@ -96,6 +98,12 @@ function TrayOptionsSettings() {
currentValue={!disableTray} currentValue={!disableTray}
onChange={trayEnabled => setDisableTray(!trayEnabled)} onChange={trayEnabled => setDisableTray(!trayEnabled)}
/> />
<FormCheckbox
name="persistant-tray"
label={t("tray.persistant_tray")}
currentValue={persistantTray}
onChange={enabled => setPersistantTray(enabled)}
/>
</OptionsSection> </OptionsSection>
) )
} }

View File

@ -54,7 +54,8 @@ async function main() {
// for applications and their menu bar to stay active until the user quits // for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q. // explicitly with Cmd + Q.
app.on("window-all-closed", () => { app.on("window-all-closed", () => {
if (process.platform !== "darwin") { const persistantTrayEnabled = options.getOptionBool("persistantTray");
if (!persistantTrayEnabled && process.platform !== "darwin") {
app.quit(); app.quit();
} }
}); });

View File

@ -75,9 +75,9 @@ const ALLOWED_OPTIONS = new Set<OptionNames>([
"highlightsList", "highlightsList",
"checkForUpdates", "checkForUpdates",
"disableTray", "disableTray",
"persistantTray",
"eraseUnusedAttachmentsAfterSeconds", "eraseUnusedAttachmentsAfterSeconds",
"eraseUnusedAttachmentsAfterTimeScale", "eraseUnusedAttachmentsAfterTimeScale",
"disableTray",
"customSearchEngineName", "customSearchEngineName",
"customSearchEngineUrl", "customSearchEngineUrl",
"promotedAttributesOpenInRibbon", "promotedAttributesOpenInRibbon",

View File

@ -124,6 +124,7 @@ const defaultOptions: DefaultOption[] = [
{ name: "highlightsList", value: '["underline","color","bgColor"]', isSynced: true }, { name: "highlightsList", value: '["underline","color","bgColor"]', isSynced: true },
{ name: "checkForUpdates", value: "true", isSynced: true }, { name: "checkForUpdates", value: "true", isSynced: true },
{ name: "disableTray", value: "false", isSynced: false }, { name: "disableTray", value: "false", isSynced: false },
{ name: "persistantTray", value: "false", isSynced: false },
{ name: "eraseUnusedAttachmentsAfterSeconds", value: "2592000", isSynced: true }, // default 30 days { name: "eraseUnusedAttachmentsAfterSeconds", value: "2592000", isSynced: true }, // default 30 days
{ name: "eraseUnusedAttachmentsAfterTimeScale", value: "86400", isSynced: true }, // default 86400 seconds = Day { name: "eraseUnusedAttachmentsAfterTimeScale", value: "86400", isSynced: true }, // default 86400 seconds = Day
{ name: "logRetentionDays", value: "90", isSynced: false }, // default 90 days { name: "logRetentionDays", value: "90", isSynced: false }, // default 90 days

View File

@ -1,4 +1,4 @@
import electron from "electron"; import electron, { app } from "electron";
import type { BrowserWindow, Tray } from "electron"; import type { BrowserWindow, Tray } from "electron";
import { default as i18next, t } from "i18next"; import { default as i18next, t } from "i18next";
import path from "path"; import path from "path";
@ -17,6 +17,7 @@ import windowService from "./window.js";
let tray: Tray; let tray: Tray;
// `mainWindow.isVisible` doesn't work with `mainWindow.show` and `mainWindow.hide` - it returns `false` when the window // `mainWindow.isVisible` doesn't work with `mainWindow.show` and `mainWindow.hide` - it returns `false` when the window
// is minimized // is minimized
const windowVisibilityMap: Record<number, boolean> = {};; // Dictionary for storing window ID and its visibility status const windowVisibilityMap: Record<number, boolean> = {};; // Dictionary for storing window ID and its visibility status
function getTrayIconPath() { function getTrayIconPath() {
@ -107,7 +108,6 @@ function updateTrayMenu() {
if (!tray) { if (!tray) {
return; return;
} }
const lastFocusedWindow = windowService.getLastFocusedWindow();
const allWindows = windowService.getAllWindows(); const allWindows = windowService.getAllWindows();
updateWindowVisibilityMap(allWindows); updateWindowVisibilityMap(allWindows);
@ -119,19 +119,22 @@ function updateTrayMenu() {
} }
function openNewWindow() { function openNewWindow() {
const lastFocusedWindow = windowService.getLastFocusedWindow();
if (lastFocusedWindow){ if (lastFocusedWindow){
lastFocusedWindow.webContents.send("globalShortcut", "openNewWindow"); lastFocusedWindow.webContents.send("globalShortcut", "openNewWindow");
} }
} }
function triggerKeyboardAction(actionName: KeyboardActionNames) { async function triggerKeyboardAction(actionName: KeyboardActionNames) {
if (lastFocusedWindow){ const lastFocusedWindow = await getCurrentWindow();
if (lastFocusedWindow) {
lastFocusedWindow.webContents.send("globalShortcut", actionName); lastFocusedWindow.webContents.send("globalShortcut", actionName);
ensureVisible(lastFocusedWindow); ensureVisible(lastFocusedWindow);
} }
} }
function openInSameTab(note: BNote | BRecentNote) { async function openInSameTab(note: BNote | BRecentNote) {
const lastFocusedWindow = await getCurrentWindow();
if (lastFocusedWindow){ if (lastFocusedWindow){
lastFocusedWindow.webContents.send("openInSameTab", note.noteId); lastFocusedWindow.webContents.send("openInSameTab", note.noteId);
ensureVisible(lastFocusedWindow); ensureVisible(lastFocusedWindow);
@ -310,6 +313,16 @@ function createTray() {
i18next.on("languageChanged", updateTrayMenu); i18next.on("languageChanged", updateTrayMenu);
} }
async function getCurrentWindow(): Promise<BrowserWindow | null> {
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 { export default {
createTray createTray
}; };

View File

@ -8,7 +8,7 @@ import sqlInit from "./sql_init.js";
import cls from "./cls.js"; import cls from "./cls.js";
import keyboardActionsService from "./keyboard_actions.js"; import keyboardActionsService from "./keyboard_actions.js";
import electron from "electron"; 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 { formatDownloadTitle, isDev, isMac, isWindows } from "./utils.js";
import { t } from "i18next"; import { t } from "i18next";
import { RESOURCE_DIR } from "./resource_dir.js"; 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. let allWindows: BrowserWindow[] = []; // // Used to store all windows, sorted by the order of focus.
function trackWindowFocus(win: BrowserWindow) { 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. // 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. // Therefore, we need to push the window into the allWindows array every time it gets focused.
win.on("focus", () => { win.on("focus", () => {
@ -212,11 +213,14 @@ async function createMainWindow(app: App) {
mainWindowState.manage(mainWindow); mainWindowState.manage(mainWindow);
mainWindow.setMenuBarVisibility(false); mainWindow.setMenuBarVisibility(false);
mainWindow.loadURL(`http://127.0.0.1:${port}`);
mainWindow.on("closed", () => (mainWindow = null)); 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); trackWindowFocus(mainWindow);
await waitForIpc(mainWindow);
} }
function getWindowExtraOpts() { function getWindowExtraOpts() {
@ -385,6 +389,21 @@ function getAllWindows() {
return allWindows; 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 { export default {
createMainWindow, createMainWindow,
createExtraWindow, createExtraWindow,

View File

@ -121,6 +121,7 @@ export interface OptionDefinitions extends KeyboardShortcutsOptions<KeyboardActi
downloadImagesAutomatically: boolean; downloadImagesAutomatically: boolean;
checkForUpdates: boolean; checkForUpdates: boolean;
disableTray: boolean; disableTray: boolean;
persistantTray: boolean;
promotedAttributesOpenInRibbon: boolean; promotedAttributesOpenInRibbon: boolean;
editedNotesOpenInRibbon: boolean; editedNotesOpenInRibbon: boolean;
codeBlockWordWrap: boolean; codeBlockWordWrap: boolean;