diff --git a/resources/js/electron-plugin/dist/libs/menubar/Menubar.js b/resources/js/electron-plugin/dist/libs/menubar/Menubar.js new file mode 100644 index 00000000..8d28be49 --- /dev/null +++ b/resources/js/electron-plugin/dist/libs/menubar/Menubar.js @@ -0,0 +1,201 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +import { EventEmitter } from 'events'; +import fs from 'fs'; +import path from 'path'; +import { BrowserWindow, Tray } from 'electron'; +import Positioner from '../positioner/index.js'; +import { cleanOptions } from './util/cleanOptions.js'; +import { getWindowPosition } from './util/getWindowPosition.js'; +export class Menubar extends EventEmitter { + constructor(app, options) { + super(); + this._blurTimeout = null; + this._app = app; + this._options = cleanOptions(options); + this._isVisible = false; + if (app.isReady()) { + process.nextTick(() => this.appReady().catch((err) => console.error('menubar: ', err))); + } + else { + app.on('ready', () => { + this.appReady().catch((err) => console.error('menubar: ', err)); + }); + } + } + get app() { + return this._app; + } + get positioner() { + if (!this._positioner) { + throw new Error('Please access `this.positioner` after the `after-create-window` event has fired.'); + } + return this._positioner; + } + get tray() { + if (!this._tray) { + throw new Error('Please access `this.tray` after the `ready` event has fired.'); + } + return this._tray; + } + get window() { + return this._browserWindow; + } + getOption(key) { + return this._options[key]; + } + hideWindow() { + if (!this._browserWindow || !this._isVisible) { + return; + } + this.emit('hide'); + this._browserWindow.hide(); + this.emit('after-hide'); + this._isVisible = false; + if (this._blurTimeout) { + clearTimeout(this._blurTimeout); + this._blurTimeout = null; + } + } + setOption(key, value) { + this._options[key] = value; + } + showWindow(trayPos) { + return __awaiter(this, void 0, void 0, function* () { + if (!this.tray) { + throw new Error('Tray should have been instantiated by now'); + } + if (!this._browserWindow) { + yield this.createWindow(); + } + if (!this._browserWindow) { + throw new Error('Window has been initialized just above. qed.'); + } + if (['win32', 'linux'].includes(process.platform)) { + this._options.windowPosition = getWindowPosition(this.tray); + } + this.emit('show'); + if (trayPos && trayPos.x !== 0) { + this._cachedBounds = trayPos; + } + else if (this._cachedBounds) { + trayPos = this._cachedBounds; + } + else if (this.tray.getBounds) { + trayPos = this.tray.getBounds(); + } + let noBoundsPosition = undefined; + if ((trayPos === undefined || trayPos.x === 0) && + this._options.windowPosition && + this._options.windowPosition.startsWith('tray')) { + noBoundsPosition = + process.platform === 'win32' ? 'bottomRight' : 'topRight'; + } + const position = this.positioner.calculate(this._options.windowPosition || noBoundsPosition, trayPos); + const x = this._options.browserWindow.x !== undefined + ? this._options.browserWindow.x + : position.x; + const y = this._options.browserWindow.y !== undefined + ? this._options.browserWindow.y + : position.y; + this._browserWindow.setPosition(Math.round(x), Math.round(y)); + this._browserWindow.show(); + this._isVisible = true; + this.emit('after-show'); + return; + }); + } + appReady() { + return __awaiter(this, void 0, void 0, function* () { + if (this.app.dock && !this._options.showDockIcon) { + this.app.dock.hide(); + } + if (this._options.activateWithApp) { + this.app.on('activate', (_event, hasVisibleWindows) => { + if (!hasVisibleWindows) { + this.showWindow().catch(console.error); + } + }); + } + let trayImage = this._options.icon || path.join(this._options.dir, 'IconTemplate.png'); + if (typeof trayImage === 'string' && !fs.existsSync(trayImage)) { + trayImage = path.join(__dirname, '..', 'assets', 'IconTemplate.png'); + } + const defaultClickEvent = this._options.showOnRightClick + ? 'right-click' + : 'click'; + this._tray = this._options.tray || new Tray(trayImage); + if (!this.tray) { + throw new Error('Tray has been initialized above'); + } + this.tray.on(defaultClickEvent, this.clicked.bind(this)); + this.tray.on('double-click', this.clicked.bind(this)); + this.tray.setToolTip(this._options.tooltip); + if (!this._options.windowPosition) { + this._options.windowPosition = getWindowPosition(this.tray); + } + if (this._options.preloadWindow) { + yield this.createWindow(); + } + this.emit('ready'); + }); + } + clicked(event, bounds) { + return __awaiter(this, void 0, void 0, function* () { + if (event && (event.shiftKey || event.ctrlKey || event.metaKey)) { + return this.hideWindow(); + } + if (this._blurTimeout) { + clearInterval(this._blurTimeout); + } + if (this._browserWindow && this._isVisible) { + return this.hideWindow(); + } + this._cachedBounds = bounds || this._cachedBounds; + yield this.showWindow(this._cachedBounds); + }); + } + createWindow() { + return __awaiter(this, void 0, void 0, function* () { + this.emit('create-window'); + const defaults = { + show: false, + frame: false, + }; + this._browserWindow = new BrowserWindow(Object.assign(Object.assign({}, defaults), this._options.browserWindow)); + this._positioner = new Positioner(this._browserWindow); + this._browserWindow.on('blur', () => { + if (!this._browserWindow) { + return; + } + this._browserWindow.isAlwaysOnTop() + ? this.emit('focus-lost') + : (this._blurTimeout = setTimeout(() => { + this.hideWindow(); + }, 100)); + }); + if (this._options.showOnAllWorkspaces !== false) { + this._browserWindow.setVisibleOnAllWorkspaces(true, { + skipTransformProcessType: true, + }); + } + this._browserWindow.on('close', this.windowClear.bind(this)); + this.emit('before-load'); + if (this._options.index !== false) { + yield this._browserWindow.loadURL(this._options.index, this._options.loadUrlOptions); + } + this.emit('after-create-window'); + }); + } + windowClear() { + this._browserWindow = undefined; + this.emit('after-close'); + } +} diff --git a/resources/js/electron-plugin/dist/libs/menubar/index.js b/resources/js/electron-plugin/dist/libs/menubar/index.js new file mode 100644 index 00000000..d996c034 --- /dev/null +++ b/resources/js/electron-plugin/dist/libs/menubar/index.js @@ -0,0 +1,7 @@ +import { app } from 'electron'; +import { Menubar } from './Menubar.js'; +export * from './util/getWindowPosition.js'; +export { Menubar }; +export function menubar(options) { + return new Menubar(app, options); +} diff --git a/resources/js/electron-plugin/dist/libs/menubar/types.js b/resources/js/electron-plugin/dist/libs/menubar/types.js new file mode 100644 index 00000000..cb0ff5c3 --- /dev/null +++ b/resources/js/electron-plugin/dist/libs/menubar/types.js @@ -0,0 +1 @@ +export {}; diff --git a/resources/js/electron-plugin/dist/libs/menubar/util/cleanOptions.js b/resources/js/electron-plugin/dist/libs/menubar/util/cleanOptions.js new file mode 100644 index 00000000..d6b7ac0b --- /dev/null +++ b/resources/js/electron-plugin/dist/libs/menubar/util/cleanOptions.js @@ -0,0 +1,38 @@ +import path from 'path'; +import url from 'url'; +import { app } from 'electron'; +const DEFAULT_WINDOW_HEIGHT = 400; +const DEFAULT_WINDOW_WIDTH = 400; +export function cleanOptions(opts) { + const options = Object.assign({}, opts); + if (options.activateWithApp === undefined) { + options.activateWithApp = true; + } + if (!options.dir) { + options.dir = app.getAppPath(); + } + if (!path.isAbsolute(options.dir)) { + options.dir = path.resolve(options.dir); + } + if (options.index === undefined) { + options.index = url.format({ + pathname: path.join(options.dir, 'index.html'), + protocol: 'file:', + slashes: true, + }); + } + options.loadUrlOptions = options.loadUrlOptions || {}; + options.tooltip = options.tooltip || ''; + if (!options.browserWindow) { + options.browserWindow = {}; + } + options.browserWindow.width = + options.browserWindow.width !== undefined + ? options.browserWindow.width + : DEFAULT_WINDOW_WIDTH; + options.browserWindow.height = + options.browserWindow.height !== undefined + ? options.browserWindow.height + : DEFAULT_WINDOW_HEIGHT; + return options; +} diff --git a/resources/js/electron-plugin/dist/libs/menubar/util/getWindowPosition.js b/resources/js/electron-plugin/dist/libs/menubar/util/getWindowPosition.js new file mode 100644 index 00000000..1c290434 --- /dev/null +++ b/resources/js/electron-plugin/dist/libs/menubar/util/getWindowPosition.js @@ -0,0 +1,46 @@ +import { screen as electronScreen } from 'electron'; +const isLinux = process.platform === 'linux'; +const trayToScreenRects = (tray) => { + const { workArea, bounds: screenBounds } = electronScreen.getDisplayMatching(tray.getBounds()); + workArea.x -= screenBounds.x; + workArea.y -= screenBounds.y; + return [screenBounds, workArea]; +}; +export function taskbarLocation(tray) { + const [screenBounds, workArea] = trayToScreenRects(tray); + if (workArea.x > 0) { + if (isLinux && workArea.y > 0) + return 'top'; + return 'left'; + } + if (workArea.y > 0) { + return 'top'; + } + if (workArea.width < screenBounds.width) { + return 'right'; + } + return 'bottom'; +} +export function getWindowPosition(tray) { + switch (process.platform) { + case 'darwin': + return 'trayCenter'; + case 'linux': + case 'win32': { + const traySide = taskbarLocation(tray); + if (traySide === 'top') { + return isLinux ? 'topRight' : 'trayCenter'; + } + if (traySide === 'bottom') { + return 'bottomRight'; + } + if (traySide === 'left') { + return 'bottomLeft'; + } + if (traySide === 'right') { + return 'bottomRight'; + } + } + } + return 'topRight'; +} diff --git a/resources/js/electron-plugin/dist/libs/positioner/index.js b/resources/js/electron-plugin/dist/libs/positioner/index.js new file mode 100644 index 00000000..2608c4ed --- /dev/null +++ b/resources/js/electron-plugin/dist/libs/positioner/index.js @@ -0,0 +1,113 @@ +"use strict"; +class Positioner { + constructor(browserWindow) { + this.browserWindow = browserWindow; + this.electronScreen = require("electron").screen; + } + _getCoords(position, trayPosition) { + let screenSize = this._getScreenSize(trayPosition); + let windowSize = this._getWindowSize(); + if (trayPosition === undefined) + trayPosition = {}; + let positions = { + trayLeft: { + x: Math.floor(trayPosition.x), + y: screenSize.y, + }, + trayBottomLeft: { + x: Math.floor(trayPosition.x), + y: Math.floor(screenSize.height - (windowSize[1] - screenSize.y)), + }, + trayRight: { + x: Math.floor(trayPosition.x - windowSize[0] + trayPosition.width), + y: screenSize.y, + }, + trayBottomRight: { + x: Math.floor(trayPosition.x - windowSize[0] + trayPosition.width), + y: Math.floor(screenSize.height - (windowSize[1] - screenSize.y)), + }, + trayCenter: { + x: Math.floor(trayPosition.x - windowSize[0] / 2 + trayPosition.width / 2), + y: screenSize.y, + }, + trayBottomCenter: { + x: Math.floor(trayPosition.x - windowSize[0] / 2 + trayPosition.width / 2), + y: Math.floor(screenSize.height - (windowSize[1] - screenSize.y)), + }, + topLeft: { + x: screenSize.x, + y: screenSize.y, + }, + topRight: { + x: Math.floor(screenSize.x + (screenSize.width - windowSize[0])), + y: screenSize.y, + }, + bottomLeft: { + x: screenSize.x, + y: Math.floor(screenSize.height - (windowSize[1] - screenSize.y)), + }, + bottomRight: { + x: Math.floor(screenSize.x + (screenSize.width - windowSize[0])), + y: Math.floor(screenSize.height - (windowSize[1] - screenSize.y)), + }, + topCenter: { + x: Math.floor(screenSize.x + (screenSize.width / 2 - windowSize[0] / 2)), + y: screenSize.y, + }, + bottomCenter: { + x: Math.floor(screenSize.x + (screenSize.width / 2 - windowSize[0] / 2)), + y: Math.floor(screenSize.height - (windowSize[1] - screenSize.y)), + }, + leftCenter: { + x: screenSize.x, + y: screenSize.y + + Math.floor(screenSize.height / 2) - + Math.floor(windowSize[1] / 2), + }, + rightCenter: { + x: Math.floor(screenSize.x + (screenSize.width - windowSize[0])), + y: screenSize.y + + Math.floor(screenSize.height / 2) - + Math.floor(windowSize[1] / 2), + }, + center: { + x: Math.floor(screenSize.x + (screenSize.width / 2 - windowSize[0] / 2)), + y: Math.floor((screenSize.height + screenSize.y) / 2 - windowSize[1] / 2), + }, + }; + if (position.substr(0, 4) === "tray") { + if (positions[position].x + windowSize[0] > + screenSize.width + screenSize.x) { + return { + x: positions["topRight"].x, + y: positions[position].y, + }; + } + } + return positions[position]; + } + _getWindowSize() { + return this.browserWindow.getSize(); + } + _getScreenSize(trayPosition) { + if (trayPosition) { + return this.electronScreen.getDisplayMatching(trayPosition) + .workArea; + } + else { + return this.electronScreen.getDisplayNearestPoint(this.electronScreen.getCursorScreenPoint()).workArea; + } + } + move(position, trayPos) { + var coords = this._getCoords(position, trayPos); + this.browserWindow.setPosition(coords.x, coords.y); + } + calculate(position, trayPos) { + var coords = this._getCoords(position, trayPos); + return { + x: coords.x, + y: coords.y, + }; + } +} +export default Positioner; diff --git a/resources/js/electron-plugin/dist/server/api/menuBar.js b/resources/js/electron-plugin/dist/server/api/menuBar.js index 21ee49d4..0271f7c3 100644 --- a/resources/js/electron-plugin/dist/server/api/menuBar.js +++ b/resources/js/electron-plugin/dist/server/api/menuBar.js @@ -2,7 +2,7 @@ import express from "express"; import { app, Menu, Tray } from "electron"; import { compileMenu } from "./helper/index.js"; import state from "../state.js"; -import { menubar } from "menubar"; +import { menubar } from "../../libs/menubar/index.js"; import { notifyLaravel } from "../utils.js"; import { fileURLToPath } from 'url'; import { enable } from "@electron/remote/main/index.js"; diff --git a/resources/js/electron-plugin/src/libs/menubar/Menubar.ts b/resources/js/electron-plugin/src/libs/menubar/Menubar.ts new file mode 100644 index 00000000..71fb39d5 --- /dev/null +++ b/resources/js/electron-plugin/src/libs/menubar/Menubar.ts @@ -0,0 +1,326 @@ +import { EventEmitter } from 'events'; +import fs from 'fs'; +import path from 'path'; +import { BrowserWindow, Tray } from 'electron'; +import Positioner from '../positioner/index.js'; + +import type { Options } from './types.js'; +import { cleanOptions } from './util/cleanOptions.js'; +import { getWindowPosition } from './util/getWindowPosition.js'; + +/** + * The main Menubar class. + * + * @noInheritDoc + */ +export class Menubar extends EventEmitter { + private _app: Electron.App; + private _browserWindow?: BrowserWindow; + private _blurTimeout: NodeJS.Timeout | null = null; // track blur events with timeout + private _isVisible: boolean; // track visibility + private _cachedBounds?: Electron.Rectangle; // _cachedBounds are needed for double-clicked event + private _options: Options; + private _positioner: Positioner | undefined; + private _tray?: Tray; + + constructor(app: Electron.App, options?: Partial) { + super(); + this._app = app; + this._options = cleanOptions(options); + this._isVisible = false; + + if (app.isReady()) { + // See https://github.com/maxogden/menubar/pull/151 + process.nextTick(() => + this.appReady().catch((err) => console.error('menubar: ', err)), + ); + } else { + app.on('ready', () => { + this.appReady().catch((err) => console.error('menubar: ', err)); + }); + } + } + + /** + * The Electron [App](https://electronjs.org/docs/api/app) + * instance. + */ + get app(): Electron.App { + return this._app; + } + + /** + * The [electron-positioner](https://github.com/jenslind/electron-positioner) + * instance. + */ + get positioner(): Positioner { + if (!this._positioner) { + throw new Error( + 'Please access `this.positioner` after the `after-create-window` event has fired.', + ); + } + + return this._positioner; + } + + /** + * The Electron [Tray](https://electronjs.org/docs/api/tray) instance. + */ + get tray(): Tray { + if (!this._tray) { + throw new Error( + 'Please access `this.tray` after the `ready` event has fired.', + ); + } + + return this._tray; + } + + /** + * The Electron [BrowserWindow](https://electronjs.org/docs/api/browser-window) + * instance, if it's present. + */ + get window(): BrowserWindow | undefined { + return this._browserWindow; + } + + /** + * Retrieve a menubar option. + * + * @param key - The option key to retrieve, see {@link Options}. + */ + getOption(key: K): Options[K] { + return this._options[key]; + } + + /** + * Hide the menubar window. + */ + hideWindow(): void { + if (!this._browserWindow || !this._isVisible) { + return; + } + this.emit('hide'); + this._browserWindow.hide(); + this.emit('after-hide'); + this._isVisible = false; + if (this._blurTimeout) { + clearTimeout(this._blurTimeout); + this._blurTimeout = null; + } + } + + /** + * Change an option after menubar is created. + * + * @param key - The option key to modify, see {@link Options}. + * @param value - The value to set. + */ + setOption(key: K, value: Options[K]): void { + this._options[key] = value; + } + + /** + * Show the menubar window. + * + * @param trayPos - The bounds to show the window in. + */ + async showWindow(trayPos?: Electron.Rectangle): Promise { + if (!this.tray) { + throw new Error('Tray should have been instantiated by now'); + } + + if (!this._browserWindow) { + await this.createWindow(); + } + + // Use guard for TypeScript, to avoid ! everywhere + if (!this._browserWindow) { + throw new Error('Window has been initialized just above. qed.'); + } + + // 'Windows' taskbar: sync windows position each time before showing + // https://github.com/maxogden/menubar/issues/232 + if (['win32', 'linux'].includes(process.platform)) { + // Fill in this._options.windowPosition when taskbar position is available + this._options.windowPosition = getWindowPosition(this.tray); + } + + this.emit('show'); + + if (trayPos && trayPos.x !== 0) { + // Cache the bounds + this._cachedBounds = trayPos; + } else if (this._cachedBounds) { + // Cached value will be used if showWindow is called without bounds data + trayPos = this._cachedBounds; + } else if (this.tray.getBounds) { + // Get the current tray bounds + trayPos = this.tray.getBounds(); + } + + // Default the window to the right if `trayPos` bounds are undefined or null. + let noBoundsPosition = undefined; + if ( + (trayPos === undefined || trayPos.x === 0) && + this._options.windowPosition && + this._options.windowPosition.startsWith('tray') + ) { + noBoundsPosition = + process.platform === 'win32' ? 'bottomRight' : 'topRight'; + } + + const position = this.positioner.calculate( + this._options.windowPosition || noBoundsPosition, + trayPos, + ) as { x: number; y: number }; + + // Not using `||` because x and y can be zero. + const x = + this._options.browserWindow.x !== undefined + ? this._options.browserWindow.x + : position.x; + const y = + this._options.browserWindow.y !== undefined + ? this._options.browserWindow.y + : position.y; + + // `.setPosition` crashed on non-integers + // https://github.com/maxogden/menubar/issues/233 + this._browserWindow.setPosition(Math.round(x), Math.round(y)); + this._browserWindow.show(); + this._isVisible = true; + this.emit('after-show'); + return; + } + + private async appReady(): Promise { + if (this.app.dock && !this._options.showDockIcon) { + this.app.dock.hide(); + } + + if (this._options.activateWithApp) { + this.app.on('activate', (_event, hasVisibleWindows) => { + if (!hasVisibleWindows) { + this.showWindow().catch(console.error); + } + }); + } + + let trayImage = + this._options.icon || path.join(this._options.dir, 'IconTemplate.png'); + if (typeof trayImage === 'string' && !fs.existsSync(trayImage)) { + trayImage = path.join(__dirname, '..', 'assets', 'IconTemplate.png'); // Default cat icon + } + + const defaultClickEvent = this._options.showOnRightClick + ? 'right-click' + : 'click'; + + this._tray = this._options.tray || new Tray(trayImage); + // Type guards for TS not to complain + if (!this.tray) { + throw new Error('Tray has been initialized above'); + } + this.tray.on( + defaultClickEvent as Parameters[0], + this.clicked.bind(this), + ); + this.tray.on('double-click', this.clicked.bind(this)); + this.tray.setToolTip(this._options.tooltip); + + if (!this._options.windowPosition) { + this._options.windowPosition = getWindowPosition(this.tray); + } + + if (this._options.preloadWindow) { + await this.createWindow(); + } + + this.emit('ready'); + } + + /** + * Callback on tray icon click or double-click. + * + * @param e + * @param bounds + */ + private async clicked( + event?: Electron.KeyboardEvent, + bounds?: Electron.Rectangle, + ): Promise { + if (event && (event.shiftKey || event.ctrlKey || event.metaKey)) { + return this.hideWindow(); + } + + // if blur was invoked clear timeout + if (this._blurTimeout) { + clearInterval(this._blurTimeout); + } + + if (this._browserWindow && this._isVisible) { + return this.hideWindow(); + } + + this._cachedBounds = bounds || this._cachedBounds; + await this.showWindow(this._cachedBounds); + } + + private async createWindow(): Promise { + this.emit('create-window'); + + // We add some default behavior for menubar's browserWindow, to make it + // look like a menubar + const defaults = { + show: false, // Don't show it at first + frame: false, // Remove window frame + }; + + this._browserWindow = new BrowserWindow({ + ...defaults, + ...this._options.browserWindow, + }); + + this._positioner = new Positioner(this._browserWindow); + + this._browserWindow.on('blur', () => { + if (!this._browserWindow) { + return; + } + + // hack to close if icon clicked when open + this._browserWindow.isAlwaysOnTop() + ? this.emit('focus-lost') + : (this._blurTimeout = setTimeout(() => { + this.hideWindow(); + }, 100)); + }); + + if (this._options.showOnAllWorkspaces !== false) { + // https://github.com/electron/electron/issues/37832#issuecomment-1497882944 + this._browserWindow.setVisibleOnAllWorkspaces(true, { + skipTransformProcessType: true, // Avoid damaging the original visible state of app.dock + }); + } + + this._browserWindow.on('close', this.windowClear.bind(this)); + + this.emit('before-load'); + + // If the user explicity set options.index to false, we don't loadURL + // https://github.com/maxogden/menubar/issues/255 + if (this._options.index !== false) { + await this._browserWindow.loadURL( + this._options.index, + this._options.loadUrlOptions, + ); + } + this.emit('after-create-window'); + } + + private windowClear(): void { + this._browserWindow = undefined; + this.emit('after-close'); + } +} diff --git a/resources/js/electron-plugin/src/libs/menubar/ambient.d.ts b/resources/js/electron-plugin/src/libs/menubar/ambient.d.ts new file mode 100644 index 00000000..2ff6872e --- /dev/null +++ b/resources/js/electron-plugin/src/libs/menubar/ambient.d.ts @@ -0,0 +1,11 @@ +// TODO https://github.com/jenslind/electron-positioner/issues/15 +declare module 'electron-positioner' { + export default class { + constructor(window: Electron.BrowserWindow); + + calculate( + position?: string, + rectangle?: Electron.Rectangle, + ): { x: number; y: number }; + } +} diff --git a/resources/js/electron-plugin/src/libs/menubar/index.ts b/resources/js/electron-plugin/src/libs/menubar/index.ts new file mode 100644 index 00000000..6eba24dd --- /dev/null +++ b/resources/js/electron-plugin/src/libs/menubar/index.ts @@ -0,0 +1,27 @@ +/** + * Entry point of menubar + * @example + * ```typescript + * import { menubar } from 'menubar'; + * ``` + */ + +/** */ + +import { app } from 'electron'; + +import { Menubar } from './Menubar.js'; +import type { Options } from './types.js'; + +export * from './util/getWindowPosition.js'; +export { Menubar }; + +/** + * Factory function to create a menubar application + * + * @param options - Options for creating a menubar application, see + * {@link Options} + */ +export function menubar(options?: Partial): Menubar { + return new Menubar(app, options); +} diff --git a/resources/js/electron-plugin/src/libs/menubar/types.ts b/resources/js/electron-plugin/src/libs/menubar/types.ts new file mode 100644 index 00000000..cf37f654 --- /dev/null +++ b/resources/js/electron-plugin/src/libs/menubar/types.ts @@ -0,0 +1,104 @@ +import type { + BrowserWindowConstructorOptions, + LoadURLOptions, + Tray, +} from 'electron'; + +/** + * Options for creating a menubar application + */ +export interface Options { + /** + * Listen on `app.on('activate')` to open menubar when app is activated. + * @default `true` + */ + activateWithApp?: boolean; + /** + * An Electron BrowserWindow instance, or an options object to be passed into + * the BrowserWindow constructor. + * @example + * ```typescript + * const options = { height: 640, width: 480 }; + * + * const mb = new Menubar({ + * browserWindow: options + * }); + * ``` + */ + browserWindow: BrowserWindowConstructorOptions; + /** + * The app source directory. + */ + dir: string; + /** + * The png icon to use for the menubar. A good size to start with is 20x20. + * To support retina, supply a 2x sized image (e.g. 40x40) with @2x added to + * the end of the name, so icon.png and icon@2x.png and Electron will + * automatically use your @2x version on retina screens. + */ + icon?: string | Electron.NativeImage; + /** + * The URL to load the menubar's browserWindow with. The url can be a remote + * address (e.g. `http://`) or a path to a local HTML file using the + * `file://` protocol. If false, then menubar won't call `loadURL` on + * start. + * @default `file:// + options.dir + index.html` + * @see https://electronjs.org/docs/api/browser-window#winloadurlurl-options + */ + index: string | false; + /** + * The options passed when loading the index URL in the menubar's + * browserWindow. Everything browserWindow.loadURL supports is supported; + * this object is simply passed onto browserWindow.loadURL + * @default `{}` + * @see https://electronjs.org/docs/api/browser-window#winloadurlurl-options + */ + loadUrlOptions?: LoadURLOptions; + /** + * Create BrowserWindow instance before it is used -- increasing resource + * usage, but making the click on the menubar load faster. + */ + preloadWindow?: boolean; + /** + * Configure the visibility of the application dock icon, macOS only. Calls + * [`app.dock.hide`](https://electronjs.org/docs/api/app#appdockhide-macos). + */ + showDockIcon?: boolean; + /** + * Makes the window available on all OS X workspaces. Calls + * [`setVisibleOnAllWorkspaces`](https://electronjs.org/docs/api/browser-window#winsetvisibleonallworkspacesvisible-options). + */ + showOnAllWorkspaces?: boolean; + /** + * Show the window on 'right-click' event instead of regular 'click'. + */ + showOnRightClick?: boolean; + /** + * Menubar tray icon tooltip text. Calls [`tray.setTooltip`](https://electronjs.org/docs/api/tray#traysettooltiptooltip). + */ + tooltip: string; + /** + * An electron Tray instance. If provided, `options.icon` will be ignored. + */ + tray?: Tray; + /** + * Sets the window position (x and y will still override this), check + * electron-positioner docs for valid values. + */ + windowPosition?: + | 'trayLeft' + | 'trayBottomLeft' + | 'trayRight' + | 'trayBottomRight' + | 'trayCenter' + | 'trayBottomCenter' + | 'topLeft' + | 'topRight' + | 'bottomLeft' + | 'bottomRight' + | 'topCenter' + | 'bottomCenter' + | 'leftCenter' + | 'rightCenter' + | 'center'; +} diff --git a/resources/js/electron-plugin/src/libs/menubar/util/cleanOptions.ts b/resources/js/electron-plugin/src/libs/menubar/util/cleanOptions.ts new file mode 100644 index 00000000..d677340d --- /dev/null +++ b/resources/js/electron-plugin/src/libs/menubar/util/cleanOptions.ts @@ -0,0 +1,67 @@ +/** + * @ignore + */ + +/** */ + +import path from 'path'; +import url from 'url'; +import { app } from 'electron'; + +import type { Options } from '../types.js'; + +const DEFAULT_WINDOW_HEIGHT = 400; +const DEFAULT_WINDOW_WIDTH = 400; + +/** + * Take as input some options, and return a sanitized version of it. + * + * @param opts - The options to clean. + * @ignore + */ +export function cleanOptions(opts?: Partial): Options { + const options: Partial = { ...opts }; + + if (options.activateWithApp === undefined) { + options.activateWithApp = true; + } + if (!options.dir) { + options.dir = app.getAppPath(); + } + if (!path.isAbsolute(options.dir)) { + options.dir = path.resolve(options.dir); + } + // Note: options.index can be `false` + if (options.index === undefined) { + options.index = url.format({ + pathname: path.join(options.dir, 'index.html'), + protocol: 'file:', + slashes: true, + }); + } + options.loadUrlOptions = options.loadUrlOptions || {}; + + options.tooltip = options.tooltip || ''; + + // `icon`, `preloadWindow`, `showDockIcon`, `showOnAllWorkspaces`, + // `showOnRightClick` don't need any special treatment + + // Now we take care of `browserWindow` + if (!options.browserWindow) { + options.browserWindow = {}; + } + + // Set width/height on options to be usable before the window is created + options.browserWindow.width = + // Note: not using `options.browserWindow.width || DEFAULT_WINDOW_WIDTH` so + // that users can put a 0 width + options.browserWindow.width !== undefined + ? options.browserWindow.width + : DEFAULT_WINDOW_WIDTH; + options.browserWindow.height = + options.browserWindow.height !== undefined + ? options.browserWindow.height + : DEFAULT_WINDOW_HEIGHT; + + return options as Options; +} diff --git a/resources/js/electron-plugin/src/libs/menubar/util/getWindowPosition.ts b/resources/js/electron-plugin/src/libs/menubar/util/getWindowPosition.ts new file mode 100644 index 00000000..a4010a6b --- /dev/null +++ b/resources/js/electron-plugin/src/libs/menubar/util/getWindowPosition.ts @@ -0,0 +1,107 @@ +/** + * Utilities to get taskbar position and consequently menubar's position + */ + +/** */ + +import { type Rectangle, type Tray, screen as electronScreen } from 'electron'; + +const isLinux = process.platform === 'linux'; + +const trayToScreenRects = (tray: Tray): [Rectangle, Rectangle] => { + // There may be more than one screen, so we need to figure out on which screen our tray icon lives. + const { workArea, bounds: screenBounds } = electronScreen.getDisplayMatching( + tray.getBounds(), + ); + + workArea.x -= screenBounds.x; + workArea.y -= screenBounds.y; + + return [screenBounds, workArea]; +}; + +type TaskbarLocation = 'top' | 'bottom' | 'left' | 'right'; + +/** + * Determine taskbard location: "top", "bottom", "left" or "right". + * + * Only tested on Windows for now, and only used in Windows. + * + * @param tray - The Electron Tray instance. + */ +export function taskbarLocation(tray: Tray): TaskbarLocation { + const [screenBounds, workArea] = trayToScreenRects(tray); + + // TASKBAR LEFT + if (workArea.x > 0) { + // Most likely Ubuntu hence assuming the window should be on top + if (isLinux && workArea.y > 0) return 'top'; + // The workspace starts more on the right + return 'left'; + } + + // TASKBAR TOP + if (workArea.y > 0) { + return 'top'; + } + + // TASKBAR RIGHT + // Here both workArea.y and workArea.x are 0 so we can no longer leverage them. + // We can use the workarea and display width though. + // Determine taskbar location + if (workArea.width < screenBounds.width) { + // The taskbar is either on the left or right, but since the LEFT case was handled above, + // we can be sure we're dealing with a right taskbar + return 'right'; + } + + // TASKBAR BOTTOM + // Since all the other cases were handled, we can be sure we're dealing with a bottom taskbar + return 'bottom'; +} + +type WindowPosition = + | 'trayCenter' + | 'topRight' + | 'trayBottomCenter' + | 'bottomLeft' + | 'bottomRight'; + +/** + * Depending on where the taskbar is, determine where the window should be + * positioned. + * + * @param tray - The Electron Tray instance. + */ +export function getWindowPosition(tray: Tray): WindowPosition { + switch (process.platform) { + // macOS + // Supports top taskbars + case 'darwin': + return 'trayCenter'; + // Linux + // Windows + // Supports top/bottom/left/right taskbar + case 'linux': + case 'win32': { + const traySide = taskbarLocation(tray); + + // Assign position for menubar + if (traySide === 'top') { + return isLinux ? 'topRight' : 'trayCenter'; + } + if (traySide === 'bottom') { + return 'bottomRight'; + } + if (traySide === 'left') { + return 'bottomLeft'; + } + if (traySide === 'right') { + return 'bottomRight'; + } + } + } + + // When we really don't know, we just show the menubar on the top-right + return 'topRight'; +} diff --git a/resources/js/electron-plugin/src/libs/positioner/README.md b/resources/js/electron-plugin/src/libs/positioner/README.md new file mode 100644 index 00000000..b901efd3 --- /dev/null +++ b/resources/js/electron-plugin/src/libs/positioner/README.md @@ -0,0 +1,5 @@ +# Electron Positioner Library + +This is a vendored copy of the [electron-positioner](https://github.com/jenslind/electron-positioner) library. Which is the `menubar` vendored lib's only dependency. + +The lib has not been updated for a long time. Since we're vendoring `menubar` we've decided to pull this in too, instead of depending on another unmaintained library in package.json. diff --git a/resources/js/electron-plugin/src/libs/positioner/index.ts b/resources/js/electron-plugin/src/libs/positioner/index.ts new file mode 100644 index 00000000..eacd4c31 --- /dev/null +++ b/resources/js/electron-plugin/src/libs/positioner/index.ts @@ -0,0 +1,173 @@ +"use strict"; + +class Positioner { + browserWindow: any; + electronScreen: any; + + constructor(browserWindow: any) { + this.browserWindow = browserWindow; + this.electronScreen = require("electron").screen; + } + + _getCoords(position, trayPosition) { + let screenSize = this._getScreenSize(trayPosition); + let windowSize = this._getWindowSize(); + + if (trayPosition === undefined) trayPosition = {}; + + // Positions + let positions = { + trayLeft: { + x: Math.floor(trayPosition.x), + y: screenSize.y, + }, + trayBottomLeft: { + x: Math.floor(trayPosition.x), + y: Math.floor( + screenSize.height - (windowSize[1] - screenSize.y), + ), + }, + trayRight: { + x: Math.floor( + trayPosition.x - windowSize[0] + trayPosition.width, + ), + y: screenSize.y, + }, + trayBottomRight: { + x: Math.floor( + trayPosition.x - windowSize[0] + trayPosition.width, + ), + y: Math.floor( + screenSize.height - (windowSize[1] - screenSize.y), + ), + }, + trayCenter: { + x: Math.floor( + trayPosition.x - windowSize[0] / 2 + trayPosition.width / 2, + ), + y: screenSize.y, + }, + trayBottomCenter: { + x: Math.floor( + trayPosition.x - windowSize[0] / 2 + trayPosition.width / 2, + ), + y: Math.floor( + screenSize.height - (windowSize[1] - screenSize.y), + ), + }, + topLeft: { + x: screenSize.x, + y: screenSize.y, + }, + topRight: { + x: Math.floor( + screenSize.x + (screenSize.width - windowSize[0]), + ), + y: screenSize.y, + }, + bottomLeft: { + x: screenSize.x, + y: Math.floor( + screenSize.height - (windowSize[1] - screenSize.y), + ), + }, + bottomRight: { + x: Math.floor( + screenSize.x + (screenSize.width - windowSize[0]), + ), + y: Math.floor( + screenSize.height - (windowSize[1] - screenSize.y), + ), + }, + topCenter: { + x: Math.floor( + screenSize.x + (screenSize.width / 2 - windowSize[0] / 2), + ), + y: screenSize.y, + }, + bottomCenter: { + x: Math.floor( + screenSize.x + (screenSize.width / 2 - windowSize[0] / 2), + ), + y: Math.floor( + screenSize.height - (windowSize[1] - screenSize.y), + ), + }, + leftCenter: { + x: screenSize.x, + y: + screenSize.y + + Math.floor(screenSize.height / 2) - + Math.floor(windowSize[1] / 2), + }, + rightCenter: { + x: Math.floor( + screenSize.x + (screenSize.width - windowSize[0]), + ), + y: + screenSize.y + + Math.floor(screenSize.height / 2) - + Math.floor(windowSize[1] / 2), + }, + center: { + x: Math.floor( + screenSize.x + (screenSize.width / 2 - windowSize[0] / 2), + ), + y: Math.floor( + (screenSize.height + screenSize.y) / 2 - windowSize[1] / 2, + ), + }, + }; + + // Default to right if the window is bigger than the space left. + // Because on Windows the window might get out of bounds and dissappear. + if (position.substr(0, 4) === "tray") { + if ( + positions[position].x + windowSize[0] > + screenSize.width + screenSize.x + ) { + return { + x: positions["topRight"].x, + y: positions[position].y, + }; + } + } + + return positions[position]; + } + + _getWindowSize() { + return this.browserWindow.getSize(); + } + + _getScreenSize(trayPosition) { + if (trayPosition) { + return this.electronScreen.getDisplayMatching(trayPosition) + .workArea; + } else { + return this.electronScreen.getDisplayNearestPoint( + this.electronScreen.getCursorScreenPoint(), + ).workArea; + } + } + + move(position, trayPos) { + // Get positions coords + var coords = this._getCoords(position, trayPos); + + // Set the windows position + this.browserWindow.setPosition(coords.x, coords.y); + } + + calculate(position, trayPos) { + // Get positions coords + var coords = this._getCoords(position, trayPos); + + return { + x: coords.x, + y: coords.y, + }; + } +} + +export default Positioner; diff --git a/resources/js/electron-plugin/src/server/api/menuBar.ts b/resources/js/electron-plugin/src/server/api/menuBar.ts index 90b3b48b..af718df6 100644 --- a/resources/js/electron-plugin/src/server/api/menuBar.ts +++ b/resources/js/electron-plugin/src/server/api/menuBar.ts @@ -2,7 +2,7 @@ import express from "express"; import { app, Menu, Tray } from "electron"; import { compileMenu } from "./helper/index.js"; import state from "../state.js"; -import { menubar } from "menubar"; +import { menubar } from "../../libs/menubar/index.js"; import { notifyLaravel } from "../utils.js"; import { fileURLToPath } from 'url' import { enable } from "@electron/remote/main/index.js"; diff --git a/resources/js/package-lock.json b/resources/js/package-lock.json index b1ed4a29..f702d234 100644 --- a/resources/js/package-lock.json +++ b/resources/js/package-lock.json @@ -22,7 +22,6 @@ "fs-extra": "^11.2.0", "get-port": "^7.1.0", "kill-sync": "^1.0.3", - "menubar": "^9.5.1", "nodemon": "^3.1.9", "ps-node": "^0.1.6", "tree-kill": "^1.2.2", @@ -47,9 +46,9 @@ "@typescript-eslint/parser": "^8.18.1", "@vue/eslint-config-prettier": "^10.1.0", "cross-env": "^10.0.0", - "electron": "^32.2.7", + "electron": "^38.0.0", "electron-builder": "^25.1.8", - "electron-chromedriver": "^32.2.6", + "electron-chromedriver": "^38.0.0", "electron-vite": "^4.0.0", "eslint": "^9.17.0", "eslint-config-prettier": "^10.0.0", @@ -2853,9 +2852,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.34.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.34.0.tgz", - "integrity": "sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw==", + "version": "9.35.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.35.0.tgz", + "integrity": "sha512-30iXE9whjlILfWobBkNerJo+TXYsgVM5ERQwMcMKCHckHflCmf7wXDAHlARoWnh0s1U72WqlbeyE7iAcCzuCPw==", "dev": true, "license": "MIT", "engines": { @@ -5515,9 +5514,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001739", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001739.tgz", - "integrity": "sha512-y+j60d6ulelrNSwpPyrHdl+9mJnQzHBr08xm48Qno0nSk4h3Qojh+ziv2qE6rXf4k3tadF4o1J/1tAbVm1NtnA==", + "version": "1.0.30001741", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001741.tgz", + "integrity": "sha512-QGUGitqsc8ARjLdgAfxETDhRbJ0REsP6O3I96TAth/mVjh2cYzN2u+3AzPP3aVSm2FehEItaJw1xd+IGBXWeSw==", "dev": true, "funding": [ { @@ -6646,14 +6645,14 @@ } }, "node_modules/electron": { - "version": "32.3.3", - "resolved": "https://registry.npmjs.org/electron/-/electron-32.3.3.tgz", - "integrity": "sha512-7FT8tDg+MueAw8dBn5LJqDvlM4cZkKJhXfgB3w7P5gvSoUQVAY6LIQcXJxgL+vw2rIRY/b9ak7ZBFbCMF2Bk4w==", + "version": "38.0.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-38.0.0.tgz", + "integrity": "sha512-egljptiPJqbL/oamFCEY+g3RNeONWTVxZSGeyLqzK8xq106JhzuxnhJZ3sxt4DzJFaofbGyGJA37Oe9d+gVzYw==", "hasInstallScript": true, "license": "MIT", "dependencies": { "@electron/get": "^2.0.0", - "@types/node": "^20.9.0", + "@types/node": "^22.7.7", "extract-zip": "^2.0.1" }, "bin": { @@ -6762,9 +6761,9 @@ } }, "node_modules/electron-chromedriver": { - "version": "32.3.3", - "resolved": "https://registry.npmjs.org/electron-chromedriver/-/electron-chromedriver-32.3.3.tgz", - "integrity": "sha512-HVyd2Fa4kRuw1GIlJFr9Uw5ut5sfsgk0NgQ05KXdpVZs95LwJch96acE4BecHO1v8ruWzHZT0QIMcQxDNCHUiw==", + "version": "38.0.0", + "resolved": "https://registry.npmjs.org/electron-chromedriver/-/electron-chromedriver-38.0.0.tgz", + "integrity": "sha512-8GEBO0dighodFqRrpyWon28y861jmy/wxXukU3SBQ4ohOmud2gxGvmhSxKynCENz25JgxGgj18l8lDRwsg9GSQ==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -6825,12 +6824,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/electron-positioner": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/electron-positioner/-/electron-positioner-4.1.0.tgz", - "integrity": "sha512-726DfbI9ZNoCg+Fcu6XLuTKTnzf+6nFqv7h+K/V6Ug7IbaPMI7s9S8URnGtWFCy5N5PL4HSzRFF2mXuinftDdg==", - "license": "MIT" - }, "node_modules/electron-publish": { "version": "25.1.7", "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-25.1.7.tgz", @@ -7008,15 +7001,6 @@ "node": ">=8.0.0" } }, - "node_modules/electron/node_modules/@types/node": { - "version": "20.19.13", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.13.tgz", - "integrity": "sha512-yCAeZl7a0DxgNVteXFHt9+uyFbqXGy/ShC4BlcHkoE0AfGXYv/BUiplV72DjMYXHDBXFjhvr6DD1NiRVfB4j8g==", - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, "node_modules/emoji-regex": { "version": "10.5.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.5.0.tgz", @@ -7432,6 +7416,19 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/@eslint/js": { + "version": "9.34.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.34.0.tgz", + "integrity": "sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, "node_modules/eslint/node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -9768,18 +9765,6 @@ "node": ">= 0.8" } }, - "node_modules/menubar": { - "version": "9.5.1", - "resolved": "https://registry.npmjs.org/menubar/-/menubar-9.5.1.tgz", - "integrity": "sha512-swfgKal+DTgJINay36X+LGBSqyFKS4d9FyJ2w0s/4MtO7/UGplEZqluLTnq4xgLNxNjMWhXycOELP+rRYpTagA==", - "license": "BSD-2-Clause", - "dependencies": { - "electron-positioner": "^4.1.0" - }, - "peerDependencies": { - "electron": ">=9.0.0 <33.0.0" - } - }, "node_modules/meow": { "version": "13.2.0", "resolved": "https://registry.npmjs.org/meow/-/meow-13.2.0.tgz", diff --git a/resources/js/package.json b/resources/js/package.json index 30d691ee..c84a150e 100644 --- a/resources/js/package.json +++ b/resources/js/package.json @@ -51,7 +51,6 @@ "fs-extra": "^11.2.0", "get-port": "^7.1.0", "kill-sync": "^1.0.3", - "menubar": "^9.5.1", "nodemon": "^3.1.9", "ps-node": "^0.1.6", "tree-kill": "^1.2.2", @@ -76,9 +75,9 @@ "@typescript-eslint/parser": "^8.18.1", "@vue/eslint-config-prettier": "^10.1.0", "cross-env": "^10.0.0", - "electron": "^32.2.7", + "electron": "^38.0.0", "electron-builder": "^25.1.8", - "electron-chromedriver": "^32.2.6", + "electron-chromedriver": "^38.0.0", "electron-vite": "^4.0.0", "eslint": "^9.17.0", "eslint-config-prettier": "^10.0.0", diff --git a/resources/js/yarn.lock b/resources/js/yarn.lock index d296defe..2f1a21a1 100644 --- a/resources/js/yarn.lock +++ b/resources/js/yarn.lock @@ -1100,7 +1100,12 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/js@^9.17.0", "@eslint/js@9.34.0": +"@eslint/js@^9.17.0": + version "9.35.0" + resolved "https://registry.npmjs.org/@eslint/js/-/js-9.35.0.tgz" + integrity sha512-30iXE9whjlILfWobBkNerJo+TXYsgVM5ERQwMcMKCHckHflCmf7wXDAHlARoWnh0s1U72WqlbeyE7iAcCzuCPw== + +"@eslint/js@9.34.0": version "9.34.0" resolved "https://registry.npmjs.org/@eslint/js/-/js-9.34.0.tgz" integrity sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw== @@ -1446,20 +1451,13 @@ resolved "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz" integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA== -"@types/node@*", "@types/node@^18.0.0 || ^20.0.0 || >=22.0.0", "@types/node@^20.19.0 || >=22.12.0", "@types/node@^22.10.2": +"@types/node@*", "@types/node@^18.0.0 || ^20.0.0 || >=22.0.0", "@types/node@^20.19.0 || >=22.12.0", "@types/node@^22.10.2", "@types/node@^22.7.7": version "22.18.1" resolved "https://registry.npmjs.org/@types/node/-/node-22.18.1.tgz" integrity sha512-rzSDyhn4cYznVG+PCzGe1lwuMYJrcBS1fc3JqSa2PvtABwWo+dZ1ij5OVok3tqfpEBCBoaR4d7upFJk73HRJDw== dependencies: undici-types "~6.21.0" -"@types/node@^20.9.0": - version "20.19.13" - resolved "https://registry.npmjs.org/@types/node/-/node-20.19.13.tgz" - integrity sha512-yCAeZl7a0DxgNVteXFHt9+uyFbqXGy/ShC4BlcHkoE0AfGXYv/BUiplV72DjMYXHDBXFjhvr6DD1NiRVfB4j8g== - dependencies: - undici-types "~6.21.0" - "@types/normalize-package-data@^2.4.3": version "2.4.4" resolved "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz" @@ -2262,9 +2260,9 @@ callsites@^3.0.0: integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== caniuse-lite@^1.0.30001737: - version "1.0.30001739" - resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001739.tgz" - integrity sha512-y+j60d6ulelrNSwpPyrHdl+9mJnQzHBr08xm48Qno0nSk4h3Qojh+ziv2qE6rXf4k3tadF4o1J/1tAbVm1NtnA== + version "1.0.30001741" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001741.tgz" + integrity sha512-QGUGitqsc8ARjLdgAfxETDhRbJ0REsP6O3I96TAth/mVjh2cYzN2u+3AzPP3aVSm2FehEItaJw1xd+IGBXWeSw== chai@^5.2.0: version "5.3.3" @@ -2765,10 +2763,10 @@ electron-builder@^25.1.8: simple-update-notifier "2.0.0" yargs "^17.6.2" -electron-chromedriver@^32.2.6: - version "32.3.3" - resolved "https://registry.npmjs.org/electron-chromedriver/-/electron-chromedriver-32.3.3.tgz" - integrity sha512-HVyd2Fa4kRuw1GIlJFr9Uw5ut5sfsgk0NgQ05KXdpVZs95LwJch96acE4BecHO1v8ruWzHZT0QIMcQxDNCHUiw== +electron-chromedriver@^38.0.0: + version "38.0.0" + resolved "https://registry.npmjs.org/electron-chromedriver/-/electron-chromedriver-38.0.0.tgz" + integrity sha512-8GEBO0dighodFqRrpyWon28y861jmy/wxXukU3SBQ4ohOmud2gxGvmhSxKynCENz25JgxGgj18l8lDRwsg9GSQ== dependencies: "@electron/get" "^2.0.1" extract-zip "^2.0.0" @@ -2796,11 +2794,6 @@ electron-is-dev@^3.0.1: resolved "https://registry.npmjs.org/electron-is-dev/-/electron-is-dev-3.0.1.tgz" integrity sha512-8TjjAh8Ec51hUi3o4TaU0mD3GMTOESi866oRNavj9A3IQJ7pmv+MJVmdZBFGw4GFT36X7bkqnuDNYvkQgvyI8Q== -electron-positioner@^4.1.0: - version "4.1.0" - resolved "https://registry.npmjs.org/electron-positioner/-/electron-positioner-4.1.0.tgz" - integrity sha512-726DfbI9ZNoCg+Fcu6XLuTKTnzf+6nFqv7h+K/V6Ug7IbaPMI7s9S8URnGtWFCy5N5PL4HSzRFF2mXuinftDdg== - electron-publish@25.1.7: version "25.1.7" resolved "https://registry.npmjs.org/electron-publish/-/electron-publish-25.1.7.tgz" @@ -2861,13 +2854,13 @@ electron-window-state@^5.0.3: jsonfile "^4.0.0" mkdirp "^0.5.1" -electron@^32.2.7, "electron@>= 13.0.0", electron@>=13.0.0, "electron@>=9.0.0 <33.0.0": - version "32.3.3" - resolved "https://registry.npmjs.org/electron/-/electron-32.3.3.tgz" - integrity sha512-7FT8tDg+MueAw8dBn5LJqDvlM4cZkKJhXfgB3w7P5gvSoUQVAY6LIQcXJxgL+vw2rIRY/b9ak7ZBFbCMF2Bk4w== +electron@^38.0.0, "electron@>= 13.0.0", electron@>=13.0.0: + version "38.0.0" + resolved "https://registry.npmjs.org/electron/-/electron-38.0.0.tgz" + integrity sha512-egljptiPJqbL/oamFCEY+g3RNeONWTVxZSGeyLqzK8xq106JhzuxnhJZ3sxt4DzJFaofbGyGJA37Oe9d+gVzYw== dependencies: "@electron/get" "^2.0.0" - "@types/node" "^20.9.0" + "@types/node" "^22.7.7" extract-zip "^2.0.1" emoji-regex@^10.3.0: @@ -4441,13 +4434,6 @@ media-typer@^1.1.0: resolved "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz" integrity sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw== -menubar@^9.5.1: - version "9.5.1" - resolved "https://registry.npmjs.org/menubar/-/menubar-9.5.1.tgz" - integrity sha512-swfgKal+DTgJINay36X+LGBSqyFKS4d9FyJ2w0s/4MtO7/UGplEZqluLTnq4xgLNxNjMWhXycOELP+rRYpTagA== - dependencies: - electron-positioner "^4.1.0" - meow@^13.2.0: version "13.2.0" resolved "https://registry.npmjs.org/meow/-/meow-13.2.0.tgz"