Compare commits

...

1 Commits

Author SHA1 Message Date
Adorian Doran
35e11807e5 client: add support for persistant system tray icon 2025-11-17 15:36:02 +02:00
10 changed files with 58 additions and 11 deletions

View File

@@ -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) {

View File

@@ -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",

View File

@@ -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": {

View File

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

View File

@@ -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();
}
});

View File

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

View File

@@ -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

View File

@@ -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<number, boolean> = {};; // 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<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 {
createTray
};

View File

@@ -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,

View File

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