diff --git a/buildAssets/icons/icon16.png b/buildAssets/icons/icon16.png new file mode 100644 index 0000000..ae51b51 Binary files /dev/null and b/buildAssets/icons/icon16.png differ diff --git a/package-lock.json b/package-lock.json index 193961a..e1da818 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2349,6 +2349,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.16.1.tgz", "integrity": "sha512-SxdPak/5bO0EnGktV05+Hq8oatjAYVY3Zh2bye9pGZy6+jwyR3LG3YKkV4YatlsgqXP28BTeVm9pqwJM96vf2A==", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "7.16.1", @@ -2382,6 +2383,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.16.1.tgz", "integrity": "sha512-u+1Qx86jfGQ5i4JjK33/FnawZRpsLxRnKzGE6EABZ40KxVT/vWsiZFEBBHjFOljmmV3MBYOHEKi0Jm9hbAOClA==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "@typescript-eslint/scope-manager": "7.16.1", "@typescript-eslint/types": "7.16.1", @@ -2410,6 +2412,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.16.1.tgz", "integrity": "sha512-nYpyv6ALte18gbMz323RM+vpFpTjfNdyakbf3nsLvF43uF9KeNC289SUEW3QLZ1xPtyINJ1dIsZOuWuSRIWygw==", "dev": true, + "license": "MIT", "dependencies": { "@typescript-eslint/types": "7.16.1", "@typescript-eslint/visitor-keys": "7.16.1" @@ -2427,6 +2430,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.16.1.tgz", "integrity": "sha512-rbu/H2MWXN4SkjIIyWcmYBjlp55VT+1G3duFOIukTNFxr9PI35pLc2ydwAfejCEitCv4uztA07q0QWanOHC7dA==", "dev": true, + "license": "MIT", "dependencies": { "@typescript-eslint/typescript-estree": "7.16.1", "@typescript-eslint/utils": "7.16.1", @@ -2454,6 +2458,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.16.1.tgz", "integrity": "sha512-AQn9XqCzUXd4bAVEsAXM/Izk11Wx2u4H3BAfQVhSfzfDOm/wAON9nP7J5rpkCxts7E5TELmN845xTUCQrD1xIQ==", "dev": true, + "license": "MIT", "engines": { "node": "^18.18.0 || >=20.0.0" }, @@ -2467,6 +2472,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.16.1.tgz", "integrity": "sha512-0vFPk8tMjj6apaAZ1HlwM8w7jbghC8jc1aRNJG5vN8Ym5miyhTQGMqU++kuBFDNKe9NcPeZ6x0zfSzV8xC1UlQ==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "@typescript-eslint/types": "7.16.1", "@typescript-eslint/visitor-keys": "7.16.1", @@ -2495,6 +2501,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.16.1.tgz", "integrity": "sha512-WrFM8nzCowV0he0RlkotGDujx78xudsxnGMBHI88l5J8wEhED6yBwaSLP99ygfrzAjsQvcYQ94quDwI0d7E1fA==", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@typescript-eslint/scope-manager": "7.16.1", @@ -2517,6 +2524,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.16.1.tgz", "integrity": "sha512-Qlzzx4sE4u3FsHTPQAAQFJFNOuqtuY0LFrZHwQ8IHK705XxBiWOFkfKRWu6niB7hwfgnwIpO4jTC75ozW1PHWg==", "dev": true, + "license": "MIT", "dependencies": { "@typescript-eslint/types": "7.16.1", "eslint-visitor-keys": "^3.4.3" @@ -3216,6 +3224,7 @@ "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -3461,6 +3470,7 @@ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, + "license": "MIT", "dependencies": { "fill-range": "^7.1.1" }, @@ -4573,6 +4583,7 @@ "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", "dev": true, + "license": "MIT", "dependencies": { "path-type": "^4.0.0" }, @@ -6118,16 +6129,17 @@ "license": "MIT" }, "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", - "micromatch": "^4.0.4" + "micromatch": "^4.0.8" }, "engines": { "node": ">=8.6.0" @@ -6138,6 +6150,7 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, + "license": "ISC", "dependencies": { "is-glob": "^4.0.1" }, @@ -6220,6 +6233,7 @@ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, + "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -6689,6 +6703,7 @@ "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", "dev": true, + "license": "MIT", "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", @@ -7332,6 +7347,7 @@ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.12.0" } @@ -8200,6 +8216,7 @@ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 8" } @@ -8209,6 +8226,7 @@ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, + "license": "MIT", "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" @@ -9122,6 +9140,7 @@ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -10003,6 +10022,7 @@ "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -10508,6 +10528,7 @@ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, + "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, @@ -10536,9 +10557,9 @@ } }, "node_modules/ts-api-utils": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", - "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", "dev": true, "license": "MIT", "engines": { diff --git a/src/main/MainRunner.ts b/src/main/MainRunner.ts index 2055ef0..49f1334 100644 --- a/src/main/MainRunner.ts +++ b/src/main/MainRunner.ts @@ -1,6 +1,18 @@ -import { app, BrowserWindow, RenderProcessGoneDetails } from 'electron' -import Constants from './utils/Constants' +import { app, BrowserWindow, RenderProcessGoneDetails, BrowserWindowConstructorOptions } from 'electron' +import Constants, { TrayOptions } from './utils/Constants' import IPCs from './IPCs' +import { createTray, hideWindow, showWindow } from './tray.ts' + +const options = { + width: Constants.IS_DEV_ENV ? 1500 : 1200, + height: 650, + tray: { + // all optional values from DEFAULT_TRAY_OPTIONS can de defined here + enabled: true, + menu: false, // true, to use a tray menu ; false to toggle visibility on click on tray icon + trayWindow: false // true, to use a tray floating window attached to top try icon + } +} const exitApp = (mainWindow: BrowserWindow): void => { if (mainWindow && !mainWindow.isDestroyed()) { @@ -10,15 +22,46 @@ const exitApp = (mainWindow: BrowserWindow): void => { app.exit() } -export const createMainWindow = async (mainWindow: BrowserWindow): Promise => { - mainWindow = new BrowserWindow({ +export const createMainWindow = async (): Promise => { + let opt: BrowserWindowConstructorOptions = { title: Constants.APP_NAME, show: false, - width: Constants.IS_DEV_ENV ? 1500 : 1200, - height: 650, + width: options.width, + height: options.height, useContentSize: true, - webPreferences: Constants.DEFAULT_WEB_PREFERENCES - }) + webPreferences: Constants.DEFAULT_WEB_PREFERENCES, + frame: true + } + const trayOptions: TrayOptions = (options.tray?.enabled) ? { + ...Constants.DEFAULT_TRAY_OPTIONS, + ...options.tray + }: { + ...Constants.DEFAULT_TRAY_OPTIONS, + enabled: false + } + + // trayWindow requires tray.enabled=true + if (trayOptions.enabled && trayOptions.trayWindow){ + opt = { + ...opt, + width: options.width , + height: options.height, + maxWidth: options.width, + maxHeight: options.height, + show: false, + frame: false, + fullscreenable: false, + hiddenInMissionControl: true, + resizable: false, + transparent: true, + alwaysOnTop: true, + webPreferences: { + ...Constants.DEFAULT_WEB_PREFERENCES, + backgroundThrottling: false + } + } + } + const mainWindow = new BrowserWindow(opt) mainWindow.setMenu(null) @@ -28,17 +71,28 @@ export const createMainWindow = async (mainWindow: BrowserWindow): Promise { - if (Constants.IS_DEV_ENV) { + if (Constants.IS_DEV_ENV && Constants.IS_DEVTOOLS) { mainWindow.webContents.openDevTools() } }) - mainWindow.once('ready-to-show', (): void => { - mainWindow.setAlwaysOnTop(true) - mainWindow.show() - mainWindow.focus() - mainWindow.setAlwaysOnTop(false) - }) + if (trayOptions.enabled) { + createTray(mainWindow, trayOptions); + } + + if (trayOptions.enabled && trayOptions.trayWindow) { + hideWindow(mainWindow); + if (trayOptions.showAtStartup){ + showWindow(mainWindow) + } + }else{ + mainWindow.once('ready-to-show', (): void => { + mainWindow.setAlwaysOnTop(true) + mainWindow.show() + mainWindow.focus() + mainWindow.setAlwaysOnTop(false) + }) + } // Initialize IPC Communication IPCs.initialize() diff --git a/src/main/index.ts b/src/main/index.ts index 8dc301c..b3a1666 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -18,12 +18,12 @@ app.on('ready', async () => { } */ - mainWindow = await createMainWindow(mainWindow) + mainWindow = await createMainWindow() }) app.on('activate', async () => { if (!mainWindow) { - mainWindow = await createMainWindow(mainWindow) + mainWindow = await createMainWindow() } }) diff --git a/src/main/tray.ts b/src/main/tray.ts new file mode 100644 index 0000000..f416218 --- /dev/null +++ b/src/main/tray.ts @@ -0,0 +1,111 @@ +import { app, screen, Menu, Tray, BrowserWindow } from 'electron' +import Constants from './utils/Constants.ts' +import { debounce } from './utils/Util.ts' +let tray +let trayOptions; + +export function createTray(window: BrowserWindow, options) { + trayOptions = options || Constants.DEFAULT_TRAY_OPTIONS; + // menu or trayWindow, you need to choose + if (trayOptions.trayWindow){ + trayOptions.menu = false + } + + tray = new Tray('buildAssets/icons/icon16.png'); + tray.setToolTip(trayOptions.tooltip); + if (trayOptions.menu) { + const contextMenu = Menu.buildFromTemplate([ + { + label: 'Show App', + click: () => { + showWindow(window) + } + }, + { + label: 'Hide App', + click: () => { + hideWindow(window) + } + }, + { + label: 'Exit', + click: () => { + app.quit() + } + } + ]) + // tray icon only with classic window + tray.setContextMenu(contextMenu) + } else { + // handle click on tray icon + tray.on('right-click', function (event) { + debounce(() => toggleWindow(window)); + }); + tray.on('click', function (event) { + debounce(() => toggleWindow(window)); + }); + // no menu for tray window + window.setMenu(null) + tray.setContextMenu(null) + } + // align at startup + alignWindow(window) + return tray +} + +export function hideWindow(window: BrowserWindow) { + window.hide(); + // if (!trayOptions.trayWindow) return; + // hide window when click elsewhere on screen + window.on("blur", () => { + // dont close if devtools + if (!window.webContents.isDevToolsOpened()) { + window.hide(); + } + }); +} + +export function toggleWindow(window: BrowserWindow) { + if (window.isVisible()) { + hideWindow(window); + }else{ + showWindow(window); + } +} + +export function showWindow(window: BrowserWindow) { + window.show() + alignWindow(window) +} + +export function alignWindow(window: BrowserWindow) { + if (!trayOptions.trayWindow) return; + + const b = window.getBounds() + const position = calculateWindowPosition(b); + window.setBounds({ + width: b.width, + height: b.height, + x: position.x, + y: position.y + }); +} + +function calculateWindowPosition(b) { + const margin = trayOptions.margin; + const screenBounds = screen.getPrimaryDisplay().size; + const trayBounds = tray.getBounds(); + const bottom = trayBounds.y > screenBounds.height / 2 ; + const x = Math.floor(trayBounds.x - b.width/2 - margin.x + trayBounds.width / 2); + const y = bottom ? + Math.floor(trayBounds.y - b.height - margin.y + trayBounds.height / 2) + : + Math.floor(trayBounds.y + margin.y + trayBounds.height / 2) + ; + + // constraint into screen + return { + x: Math.max(0, Math.min(screenBounds.width - b.width, x)), + y: Math.max(0, Math.min(screenBounds.height - b.height, y)) + }; +} diff --git a/src/main/utils/Constants.ts b/src/main/utils/Constants.ts index 2a541ad..43f029f 100644 --- a/src/main/utils/Constants.ts +++ b/src/main/utils/Constants.ts @@ -4,6 +4,15 @@ import { fileURLToPath } from 'url' const __dirname = dirname(fileURLToPath(import.meta.url)) +export interface TrayOptions { + enabled: boolean, + trayWindow: boolean, + menu: boolean, + tooltip: string, + margin: {x: number, y: number}, + showAtStartup: boolean +} + export default class Constants { // Display app name (uppercase first letter) static APP_NAME = name.charAt(0).toUpperCase() + name.slice(1) @@ -12,6 +21,10 @@ export default class Constants { static IS_DEV_ENV = process.env.NODE_ENV === 'development' + // To show devtools at startup. It requires IS_DEV_ENV=true. + // Note: For debugging purpose, window won't be closed if click elsewhere, if devtools is open. + static IS_DEVTOOLS = true + static IS_MAC = process.platform === 'darwin' static DEFAULT_WEB_PREFERENCES = { @@ -21,6 +34,17 @@ export default class Constants { preload: join(__dirname, '../preload/index.js') } + static DEFAULT_TRAY_OPTIONS : TrayOptions = { + enabled: false, + trayWindow: false, + menu: false, + tooltip: 'Vutron App', + margin: {x:0, y:0}, + showAtStartup: false + } + static APP_INDEX_URL_DEV = `${debug.env.VITE_DEV_SERVER_URL}/index.html` static APP_INDEX_URL_PROD = join(__dirname, '../index.html') } + + diff --git a/src/main/utils/Util.ts b/src/main/utils/Util.ts new file mode 100644 index 0000000..3cb030e --- /dev/null +++ b/src/main/utils/Util.ts @@ -0,0 +1,14 @@ + +export function debounce(func, timeout = 200){ + let timer; + return function(...args) { + if (!timer) { + func.apply(this, args); + } + clearTimeout(timer); + timer = setTimeout(() => { + timer = undefined; + }, timeout); + }(); +} +