mirror of
https://github.com/zadam/trilium.git
synced 2025-10-20 07:08:55 +02:00
305 lines
9.5 KiB
TypeScript
305 lines
9.5 KiB
TypeScript
import { Menu, Tray, BrowserWindow } from "electron";
|
|
import path from "path";
|
|
import windowService from "./window.js";
|
|
import optionService from "./options.js";
|
|
import { fileURLToPath } from "url";
|
|
import type { KeyboardActionNames } from "./keyboard_actions_interface.js";
|
|
import date_notes from "./date_notes.js";
|
|
import type BNote from "../becca/entities/bnote.js";
|
|
import becca from "../becca/becca.js";
|
|
import becca_service from "../becca/becca_service.js";
|
|
import type BRecentNote from "../becca/entities/brecent_note.js";
|
|
import { ipcMain, nativeTheme } from "electron/main";
|
|
import { default as i18next, t } from "i18next";
|
|
import { isDev, isMac } from "./utils.js";
|
|
import cls from "./cls.js";
|
|
|
|
let tray: Tray;
|
|
// `mainWindow.isVisible` doesn't work with `mainWindow.show` and `mainWindow.hide` - it returns `false` when the window
|
|
// is minimized
|
|
let windowVisibilityMap: Record<number, boolean> = {};; // Dictionary for storing window ID and its visibility status
|
|
|
|
function getTrayIconPath() {
|
|
let name: string;
|
|
if (isMac) {
|
|
name = "icon-blackTemplate";
|
|
} else if (isDev) {
|
|
name = "icon-purple";
|
|
} else {
|
|
name = "icon-color";
|
|
}
|
|
|
|
return path.join(path.dirname(fileURLToPath(import.meta.url)), "../..", "images", "app-icons", "tray", `${name}.png`);
|
|
}
|
|
|
|
function getIconPath(name: string) {
|
|
const suffix = !isMac && nativeTheme.shouldUseDarkColors ? "-inverted" : "";
|
|
return path.join(path.dirname(fileURLToPath(import.meta.url)), "../..", "images", "app-icons", "tray", `${name}Template${suffix}.png`);
|
|
}
|
|
|
|
function registerVisibilityListener(window: BrowserWindow) {
|
|
if (!window) {
|
|
return;
|
|
}
|
|
|
|
// They need to be registered before the tray updater is registered
|
|
window.on("show", () => {
|
|
windowVisibilityMap[window.id] = true;
|
|
updateTrayMenu();
|
|
});
|
|
window.on("hide", () => {
|
|
windowVisibilityMap[window.id] = false;
|
|
updateTrayMenu();
|
|
});
|
|
|
|
window.on("minimize", updateTrayMenu);
|
|
window.on("maximize", updateTrayMenu);
|
|
}
|
|
|
|
function getWindowTitle(window: BrowserWindow | null) {
|
|
if (!window) {
|
|
return;
|
|
}
|
|
const title = window.getTitle();
|
|
const titleWithoutAppName = title.replace(/\s-\s[^-]+$/, ''); // Remove the name of the app
|
|
|
|
// Limit title maximum length to 17
|
|
if (titleWithoutAppName.length > 20) {
|
|
return titleWithoutAppName.substring(0, 17) + '...';
|
|
}
|
|
|
|
return titleWithoutAppName;
|
|
}
|
|
|
|
function updateWindowVisibilityMap(allWindows: BrowserWindow[]) {
|
|
const currentWindowIds: number[] = allWindows.map(window => window.id);
|
|
|
|
// Deleting closed windows from windowVisibilityMap
|
|
for (const [id, visibility] of Object.entries(windowVisibilityMap)) {
|
|
const windowId = Number(id);
|
|
if (!currentWindowIds.includes(windowId)) {
|
|
delete windowVisibilityMap[windowId];
|
|
}
|
|
}
|
|
|
|
// Iterate through allWindows to make sure the ID of each window exists in windowVisibilityMap
|
|
allWindows.forEach(window => {
|
|
const windowId = window.id;
|
|
if (!(windowId in windowVisibilityMap)) {
|
|
// If it does not exist, it is the newly created window
|
|
windowVisibilityMap[windowId] = true;
|
|
registerVisibilityListener(window);
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
function updateTrayMenu() {
|
|
const lastFocusedWindow = windowService.getLastFocusedWindow();
|
|
const allWindows = windowService.getAllWindows();
|
|
updateWindowVisibilityMap(allWindows);
|
|
|
|
function ensureVisible(win: BrowserWindow) {
|
|
if (win) {
|
|
win.show();
|
|
win.focus();
|
|
}
|
|
}
|
|
|
|
function openNewWindow() {
|
|
if (lastFocusedWindow){
|
|
lastFocusedWindow.webContents.send("globalShortcut", "openNewWindow");
|
|
}
|
|
}
|
|
|
|
function triggerKeyboardAction(actionName: KeyboardActionNames) {
|
|
if (lastFocusedWindow){
|
|
lastFocusedWindow.webContents.send("globalShortcut", actionName);
|
|
ensureVisible(lastFocusedWindow);
|
|
}
|
|
}
|
|
|
|
function openInSameTab(note: BNote | BRecentNote) {
|
|
if (lastFocusedWindow){
|
|
lastFocusedWindow.webContents.send("openInSameTab", note.noteId);
|
|
ensureVisible(lastFocusedWindow);
|
|
}
|
|
}
|
|
|
|
function buildBookmarksMenu() {
|
|
const parentNote = becca.getNoteOrThrow("_lbBookmarks");
|
|
const menuItems: Electron.MenuItemConstructorOptions[] = [];
|
|
|
|
for (const bookmarkNote of parentNote?.children) {
|
|
if (bookmarkNote.isLabelTruthy("bookmarkFolder")) {
|
|
menuItems.push({
|
|
label: bookmarkNote.title,
|
|
type: "submenu",
|
|
submenu: bookmarkNote.children.map((subitem) => {
|
|
return {
|
|
label: subitem.title,
|
|
type: "normal",
|
|
click: () => openInSameTab(subitem)
|
|
};
|
|
})
|
|
});
|
|
} else {
|
|
menuItems.push({
|
|
label: bookmarkNote.title,
|
|
type: "normal",
|
|
click: () => openInSameTab(bookmarkNote)
|
|
});
|
|
}
|
|
}
|
|
|
|
return menuItems;
|
|
}
|
|
|
|
function buildRecentNotesMenu() {
|
|
const recentNotes = becca.getRecentNotesFromQuery(`
|
|
SELECT recent_notes.*
|
|
FROM recent_notes
|
|
JOIN notes USING(noteId)
|
|
WHERE notes.isDeleted = 0
|
|
ORDER BY utcDateCreated DESC
|
|
LIMIT 10
|
|
`);
|
|
const menuItems: Electron.MenuItemConstructorOptions[] = [];
|
|
const formatter = new Intl.DateTimeFormat(undefined, {
|
|
dateStyle: "short",
|
|
timeStyle: "short"
|
|
});
|
|
|
|
for (const recentNote of recentNotes) {
|
|
const date = new Date(recentNote.utcDateCreated);
|
|
|
|
menuItems.push({
|
|
label: becca_service.getNoteTitle(recentNote.noteId),
|
|
type: "normal",
|
|
sublabel: formatter.format(date),
|
|
click: () => openInSameTab(recentNote)
|
|
});
|
|
}
|
|
|
|
return menuItems;
|
|
}
|
|
|
|
const windowVisibilityMenuItems: Electron.MenuItemConstructorOptions[] = [];
|
|
|
|
// Only call getWindowTitle if windowVisibilityMap has more than one window
|
|
const showTitle = Object.keys(windowVisibilityMap).length > 1;
|
|
|
|
for (const idStr in windowVisibilityMap) {
|
|
const id = parseInt(idStr, 10); // Get the ID of the window and make sure it is a number
|
|
const isVisible = windowVisibilityMap[id];
|
|
const win = allWindows.find(w => w.id === id);
|
|
if (!win) {
|
|
continue;
|
|
}
|
|
windowVisibilityMenuItems.push({
|
|
label: showTitle ? `${t("tray.show-windows")}: ${getWindowTitle(win)}` : t("tray.show-windows"),
|
|
type: "checkbox",
|
|
checked: isVisible,
|
|
click: () => {
|
|
if (isVisible) {
|
|
win.hide();
|
|
windowVisibilityMap[id] = false;
|
|
} else {
|
|
ensureVisible(win);
|
|
windowVisibilityMap[id] = true;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
const contextMenu = Menu.buildFromTemplate([
|
|
...windowVisibilityMenuItems,
|
|
{ type: "separator" },
|
|
{
|
|
label: t("tray.open_new_window"),
|
|
type: "normal",
|
|
icon: getIconPath("new-window"),
|
|
click: () => openNewWindow()
|
|
},
|
|
{
|
|
label: t("tray.new-note"),
|
|
type: "normal",
|
|
icon: getIconPath("new-note"),
|
|
click: () => triggerKeyboardAction("createNoteIntoInbox")
|
|
},
|
|
{
|
|
label: t("tray.today"),
|
|
type: "normal",
|
|
icon: getIconPath("today"),
|
|
click: cls.wrap(() => openInSameTab(date_notes.getTodayNote()))
|
|
},
|
|
{
|
|
label: t("tray.bookmarks"),
|
|
type: "submenu",
|
|
icon: getIconPath("bookmarks"),
|
|
submenu: buildBookmarksMenu()
|
|
},
|
|
{
|
|
label: t("tray.recents"),
|
|
type: "submenu",
|
|
icon: getIconPath("recents"),
|
|
submenu: buildRecentNotesMenu()
|
|
},
|
|
{ type: "separator" },
|
|
{
|
|
label: t("tray.close"),
|
|
type: "normal",
|
|
icon: getIconPath("close"),
|
|
click: () => {
|
|
const windows = BrowserWindow.getAllWindows();
|
|
windows.forEach(window => {
|
|
window.close();
|
|
});
|
|
}
|
|
}
|
|
]);
|
|
|
|
tray?.setContextMenu(contextMenu);
|
|
}
|
|
|
|
function changeVisibility() {
|
|
const lastFocusedWindow = windowService.getLastFocusedWindow();
|
|
|
|
if (!lastFocusedWindow) {
|
|
return;
|
|
}
|
|
|
|
// If the window is visible, hide it
|
|
if (windowVisibilityMap[lastFocusedWindow.id]) {
|
|
lastFocusedWindow.hide();
|
|
} else {
|
|
lastFocusedWindow.show();
|
|
lastFocusedWindow.focus();
|
|
}
|
|
}
|
|
|
|
function createTray() {
|
|
if (optionService.getOptionBool("disableTray")) {
|
|
return;
|
|
}
|
|
|
|
tray = new Tray(getTrayIconPath());
|
|
tray.setToolTip(t("tray.tooltip"));
|
|
// Restore focus
|
|
tray.on("click", changeVisibility);
|
|
updateTrayMenu();
|
|
|
|
if (!isMac) {
|
|
// macOS uses template icons which work great on dark & light themes.
|
|
nativeTheme.on("updated", updateTrayMenu);
|
|
}
|
|
ipcMain.on("reload-tray", updateTrayMenu);
|
|
i18next.on("languageChanged", updateTrayMenu);
|
|
}
|
|
|
|
export default {
|
|
createTray,
|
|
updateTrayMenu
|
|
};
|