From c87354be22477c2215a6b014d7c180a1e6d55c2c Mon Sep 17 00:00:00 2001 From: Kifungo A <45813955+adkif@users.noreply.github.com> Date: Thu, 5 Mar 2026 19:21:38 +0200 Subject: [PATCH] [Fix] Electron plugin window management (#9571) * feat(desktop-window): improve plugin marketplace window lifecycle management - Format constructor parameters across multiple lines for better readability - Add close event listener to properly destroy window on close - Implement show() override to restore minimized windows and focus on display - Implement close() override with destruction state checks - Add isDestroyed() helper method to safely check window destruction state - Prevent operations on destroyed windows to avoid runtime errors * feat(desktop-window): enhance splash screen lifecycle management - Add close event handler to properly cleanup splash screen on window close - Add isDestroyed() method to safely check window destruction state - Unregister splash screen from WindowManager when window closes - Sort import statements alphabetically for consistency - Reformat code indentation from spaces to tabs for consistency with codebase - Improve JSDoc comments for better documentation clarity - Ensure proper resource cleanup and prevent memory leaks during splash screen lifecycle * feat(desktop-window): enhance always-on window lifecycle and formatting - Reorganize imports alphabetically for better maintainability - Add readonly modifier to manager property for immutability - Improve ternary operator formatting for better readability - Add close event listener to properly destroy window on close - Add isDestroyed() helper method to check window destruction state - Add override keyword to show() and hide() methods for clarity - Add new close() method override with destruction checks - Add destruction checks in show() method to prevent operations on destroyed windows - Improve code consistency and prevent potential memory leaks from unclosed windows * feat(desktop-lib): enhance window lifecycle management and code organization - Remove empty constructor from AppWindowManager class - Add null checks in pluginsWindow and alwaysOnWindow getters to handle destroyed windows - Update window initialization methods to check if window exists and is not destroyed before reusing - Simplify window close event handlers by using 'closed' event instead of 'close' with manual cleanup - Improve initSettingWindow formatting for better readability - Refactor initPluginsWindow to consolidate window creation and event handling logic - Refactor initAlwaysOnWindow to improve code clarity and reduce redundant checks - Update return type annotations for pluginsWindow and alwaysOnWindow getters to include null - Consolidate settingShow method signature formatting * feat(desktop-lib): enhance menu visibility and tray icon initialization - Add plugin menu visibility control based on authentication state in desktop-menu - Import AppWindowManager at top of desktop-menu for consistent ordering - Make TrayIcon class properties readonly for immutability - Add PluginEventManager initialization in TrayIcon constructor - Notify plugin event manager on authentication state changes - Use consistent property references (this.config, this.dependencies) throughout TrayIcon - Improve code organization and dependency injection patterns * feat(desktop-lib): enhance settings command window lifecycle and type safety - Add IPathWindow interface import for improved type safety - Replace any type with readonly IPathWindow in constructor parameter - Extract timeTrackerUi from windowPath for cleaner variable access - Refactor window initialization logic to check if window exists and is not destroyed - Consolidate window show operation to execute after initialization or reuse - Improve error handling by using consistent error variable naming - Simplify loadSetting call by using extracted timeTrackerUi variable - Enhance window lifecycle management to prevent duplicate initialization attempts * feat(desktop-lib): refine unauthenticated menu strategy and remove plugins - Remove OpenPluginsCommand import and plugins menu item from unauthenticated menu - Update timer menu item label from 'TIMER_TRACKER.MENU.OPEN_TIMER' to 'LOGIN_PAGE.TITLE' - Change timer menu accelerator from 'CmdOrCtrl+O' to 'CmdOrCtrl+L' - Simplify unauthenticated user menu by removing plugin access option * feat(desktop-timer): refactor window lifecycle - Simplify function parameter type definitions by collapsing multi-line signatures - Refactor always-on window initialization to use direct variable assignment instead of property access - Improve splash screen lifecycle management with closed event handler - Refactor language change callback to use arrow function syntax - Simplify plugins window closing logic with null check instead of optional chaining * feat(desktop-window): improve isDestroyed() null safety checks - Refactor AlwaysOn.isDestroyed() to explicitly check for null browserWindow - Refactor PluginMarketplaceWindow.isDestroyed() to explicitly check for null browserWindow - Replace ternary operator with early return pattern for improved readability - Ensure consistent null handling across window lifecycle implementations * feat(desktop-menu): improve null safety for auth store retrieval - Add null coalescing operator to handle undefined LocalStore.getStore() response - Prevent potential runtime errors when auth store is not initialized - Ensure pluginMenu visibility logic operates on valid object reference * feat(desktop-splash-screen): improve isDestroyed() null safety checks - Change isDestroyed() method from private to public for external access - Add explicit null check for browserWindow before calling isDestroyed() - Return true immediately if browserWindow is not initialized - Improve type safety and prevent potential null reference errors * feat(desktop-lib): improve settings window initialization order - Move initSettingWindow call after onReady listener registration - Ensure IPC listener is attached before window initialization begins - Improve reliability of settings window ready event handling - Maintain proper error handling with listener cleanup on failure * fix: plashScreen correctly calls this.manager.unregister(RegisteredWindow.SPLASH) inside its close event handler to remove the stale reference from the WindowManager. Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * fix: calls unregister(RegisteredWindow.PLUGINS) on close Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * fix: unregister window references on close for AlwaysOn, SplashScreen, and PluginMarketplaceWindow --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- apps/desktop-timer/src/index.ts | 25 ++--- .../desktop-lib/src/lib/app-window-manager.ts | 76 +++++++------ packages/desktop-lib/src/lib/desktop-ipc.ts | 17 +-- packages/desktop-lib/src/lib/desktop-menu.ts | 5 +- .../tray/commands/open-settings-command.ts | 23 ++-- .../src/lib/tray/handlers/auth-ipc-handler.ts | 3 +- .../unauthenticated-menu-strategy.ts | 15 +-- .../desktop-lib/src/lib/tray/tray-icon.ts | 32 +++--- packages/desktop-window/src/lib/always-on.ts | 74 +++++++++---- .../src/lib/desktop-splash-screen.ts | 102 +++++++++++------- .../src/lib/plugin-marketplace.window.ts | 47 +++++++- 11 files changed, 264 insertions(+), 155 deletions(-) diff --git a/apps/desktop-timer/src/index.ts b/apps/desktop-timer/src/index.ts index f6626f41e6..8780aa61e2 100644 --- a/apps/desktop-timer/src/index.ts +++ b/apps/desktop-timer/src/index.ts @@ -27,6 +27,7 @@ initSentry(); remoteMain.initialize(); +import { DesktopSetupConfig } from '@gauzy/contracts'; import { AppError, AppMenu, @@ -59,7 +60,6 @@ import { setLaunchPathAndLoad, SplashScreen } from '@gauzy/desktop-window'; -import { DesktopSetupConfig } from '@gauzy/contracts'; import { fork } from 'child_process'; import { autoUpdater } from 'electron-updater'; @@ -229,11 +229,7 @@ function initializeAppManager() { appWindowManager.preloadPath = pathWindow.preloadPath; } -function setGlobalVariable(configs: { - isLocalServer?: boolean; - serverUrl?: string; - port?: string; -}) { +function setGlobalVariable(configs: { isLocalServer?: boolean; serverUrl?: string; port?: string }) { global.variableGlobal = { API_BASE_URL: getApiBaseUrl(configs || {}), IS_INTEGRATED_DESKTOP: configs?.isLocalServer @@ -314,17 +310,14 @@ async function startServer(setupConfig: DesktopSetupConfig, restart = false) { // Create the tray icon and menu tray = TrayIconFactory.create(environment, pathWindow, path.join(__dirname, 'assets', 'icons', 'tray', 'icon.png')); // Language change - TranslateService.onLanguageChange(() => { - new AppMenu(timeTrackerWindow, settingsWindow, updaterWindow, knex, pathWindow, null, false); - }); + TranslateService.onLanguageChange(() => + new AppMenu(timeTrackerWindow, settingsWindow, updaterWindow, knex, pathWindow, null, false) + ); return true; } -const getApiBaseUrl = (configs: { - serverUrl?: string; - port?: string; -}) => { +const getApiBaseUrl = (configs: { serverUrl?: string; port?: string }) => { if (configs.serverUrl) return configs.serverUrl; else { return configs.port ? `http://localhost:${configs.port}` : `http://localhost:${environment.API_DEFAULT_PORT}`; @@ -334,8 +327,10 @@ const getApiBaseUrl = (configs: { async function launchSplashScreen() { try { splashScreen = new SplashScreen(pathWindow.timeTrackerUi); + splashScreen.browserWindow.on('closed', () => (splashScreen = null)); await splashScreen.loadURL(); splashScreen.show(); + } catch (error) { console.error(error); throw new AppError('MAINLOADSPLASH', error); @@ -384,8 +379,8 @@ async function launchWidget(settings: any) { const auth = store.get('auth'); if (settings?.alwaysOn && auth?.token && !auth?.isLogout) { - await appWindowManager.initAlwaysOnWindow(pathWindow.timeTrackerUi); - appWindowManager.alwaysOnWindow.show(); + const alwaysOnWindow = await appWindowManager.initAlwaysOnWindow(pathWindow.timeTrackerUi); + alwaysOnWindow.show(); } } diff --git a/packages/desktop-lib/src/lib/app-window-manager.ts b/packages/desktop-lib/src/lib/app-window-manager.ts index ed6064fa66..35d6644e9a 100644 --- a/packages/desktop-lib/src/lib/app-window-manager.ts +++ b/packages/desktop-lib/src/lib/app-window-manager.ts @@ -1,14 +1,14 @@ -import { LocalStore } from './desktop-store'; -import { BrowserWindow } from 'electron'; import { + AlwaysOn, createAboutWindow, - createSetupWindow, createImageViewerWindow, createSettingsWindow, + createSetupWindow, PluginMarketplaceWindow, - AlwaysOn, setLaunchPathAndLoad } from '@gauzy/desktop-window'; +import { BrowserWindow } from 'electron'; +import { LocalStore } from './desktop-store'; import { DesktopUpdater } from './desktop-updater'; enum WindowName { @@ -34,7 +34,6 @@ export class AppWindowManager { private _preloadPath: string = null; private static instance: AppWindowManager; - constructor() { }; get settingWindow(): BrowserWindow { return this._settingWindow; @@ -52,11 +51,19 @@ export class AppWindowManager { return this._imageView; } - get pluginsWindow(): PluginMarketplaceWindow { + get pluginsWindow(): PluginMarketplaceWindow | null { + // Return null if window is destroyed + if (this._pluginsWindow?.isDestroyed()) { + this._pluginsWindow = null; + } return this._pluginsWindow; } - get alwaysOnWindow(): AlwaysOn { + get alwaysOnWindow(): AlwaysOn | null { + // Return null if window is destroyed + if (this._alwaysOnWindow?.isDestroyed()) { + this._alwaysOnWindow = null; + } return this._alwaysOnWindow; } @@ -102,43 +109,48 @@ export class AppWindowManager { } async initSettingWindow(filePath: string, preloadPath?: string, launch = false): Promise { - if (this._settingWindow) { + if (this._settingWindow && !this._settingWindow.isDestroyed()) { return this._settingWindow; } - this._settingWindow = await createSettingsWindow(this._settingWindow, filePath, this._preloadPath || preloadPath, false, launch); + + this._settingWindow = await createSettingsWindow( + this._settingWindow, + filePath, + this._preloadPath || preloadPath, + false, + launch + ); this.eventCloseWindow(this._settingWindow, WindowName.SETTING); return this._settingWindow; } async initPluginsWindow(filePath: string, preloadPath?: string): Promise { - if (!this._pluginsWindow) { - this._pluginsWindow = new PluginMarketplaceWindow(filePath, this._preloadPath || preloadPath); - await this._pluginsWindow.loadURL(); + // If window exists and is not destroyed, return it + if (this._pluginsWindow && !this._pluginsWindow.isDestroyed()) { + return this._pluginsWindow; } - this._pluginsWindow.browserWindow.removeAllListeners('close'); - this._pluginsWindow.browserWindow.on('close', () => { - if (!this._pluginsWindow?.browserWindow?.isDestroyed()) { - this._pluginsWindow?.browserWindow?.destroy(); - } - - this._pluginsWindow = null; - }); + // Create new window instance + this._pluginsWindow = new PluginMarketplaceWindow(filePath, this._preloadPath || preloadPath); + // Handle window close event to clean up reference + this._pluginsWindow.browserWindow.on('closed', () => (this._pluginsWindow = null)); + // Load the window URL + await this._pluginsWindow.loadURL(); + // Return the new window instance return this._pluginsWindow; } async initAlwaysOnWindow(filePath: string): Promise { try { - if (!this._alwaysOnWindow || this._alwaysOnWindow?.browserWindow?.isDestroyed()) { - this._alwaysOnWindow = new AlwaysOn(filePath); - await this._alwaysOnWindow.loadURL(); - this._alwaysOnWindow.browserWindow.removeAllListeners('close'); - this._alwaysOnWindow.browserWindow.on('close', () => { - if (!this._alwaysOnWindow?.browserWindow?.isDestroyed()) { - this._alwaysOnWindow?.browserWindow?.destroy(); - } - this._alwaysOnWindow = null; - }); + if (this._alwaysOnWindow && !this._alwaysOnWindow.isDestroyed()) { + return this._alwaysOnWindow; } + // Create new AlwaysOn window instance + this._alwaysOnWindow = new AlwaysOn(filePath); + // Handle window close event to clean up reference + this._alwaysOnWindow.browserWindow.on('closed', () => (this._alwaysOnWindow = null)); + // Load the window URL + await this._alwaysOnWindow.loadURL(); + // Return the new window instance return this._alwaysOnWindow; } catch (error) { console.error('Failed to initialize always-on window', error); @@ -175,9 +187,7 @@ export class AppWindowManager { } } - settingShow( - nav: 'goto_top_menu' | 'goto_update' - ) { + settingShow(nav: 'goto_top_menu' | 'goto_update') { const appSetting = LocalStore.getStore('appSetting'); const config = LocalStore.getStore('configs'); const auth = LocalStore.getStore('auth'); diff --git a/packages/desktop-lib/src/lib/desktop-ipc.ts b/packages/desktop-lib/src/lib/desktop-ipc.ts index a85b546f58..52618acd91 100644 --- a/packages/desktop-lib/src/lib/desktop-ipc.ts +++ b/packages/desktop-lib/src/lib/desktop-ipc.ts @@ -1,4 +1,9 @@ -import { DesktopSetupConfig, IActivityWatchEventResult, TimerActionTypeEnum, TimerSyncStateEnum } from '@gauzy/contracts'; +import { + DesktopSetupConfig, + IActivityWatchEventResult, + TimerActionTypeEnum, + TimerSyncStateEnum +} from '@gauzy/contracts'; import { AkitaStorageEngine, WindowManager, logger as log } from '@gauzy/desktop-core'; import { ScreenCaptureNotification } from '@gauzy/desktop-window'; import { BrowserWindow, app, desktopCapturer, ipcMain, screen, systemPreferences } from 'electron'; @@ -911,9 +916,7 @@ export function ipcTimer( ipcMain.once('setting_window_ready', () => { appWindowManager.settingShow('goto_top_menu'); }); - await appWindowManager.loadSetting( - windowPath.timeTrackerUi - ); + await appWindowManager.loadSetting(windowPath.timeTrackerUi); } else { appWindowManager.settingShow('goto_top_menu'); } @@ -1151,14 +1154,14 @@ export function ipcTimer( const setting = LocalStore.getStore('appSetting'); const auth = LocalStore.getStore('auth'); if (setting?.alwaysOn && auth?.employeeId) { - await appWindowManager.initAlwaysOnWindow(windowPath.timeTrackerUi); - appWindowManager.alwaysOnWindow.show?.(); + const alwaysOnWindow = await appWindowManager.initAlwaysOnWindow(windowPath.timeTrackerUi); + alwaysOnWindow?.show(); } }); ipcMain.on('hide_ao', (event, arg) => { if (appWindowManager.alwaysOnWindow) { - appWindowManager.alwaysOnWindow.browserWindow?.close?.(); + appWindowManager.alwaysOnWindow.close(); } }); diff --git a/packages/desktop-lib/src/lib/desktop-menu.ts b/packages/desktop-lib/src/lib/desktop-menu.ts index 6d9aefb0b0..51ae0e8ae6 100644 --- a/packages/desktop-lib/src/lib/desktop-menu.ts +++ b/packages/desktop-lib/src/lib/desktop-menu.ts @@ -1,12 +1,12 @@ import { logger } from '@gauzy/desktop-core'; import { createAboutWindow } from '@gauzy/desktop-window'; import { BrowserWindow, ipcMain, Menu, MenuItemConstructorOptions, shell } from 'electron'; +import { AppWindowManager } from './app-window-manager'; import { LocalStore } from './desktop-store'; import { TimerService } from './offline'; import { PluginManager } from './plugin-system/data-access/plugin-manager'; import { PluginEventManager } from './plugin-system/events/plugin-event.manager'; import { TranslateService } from './translation'; -import { AppWindowManager } from './app-window-manager'; export class AppMenu { public menu: MenuItemConstructorOptions[] = []; @@ -267,11 +267,14 @@ export class AppMenu { public get pluginMenu(): MenuItemConstructorOptions { // Retrieve submenu items from the plugin manager const pluginSubmenu = this.pluginManager.getMenuPlugins(); + const { employeeId = null, isLogout = false } = LocalStore.getStore('auth') || {}; + const visible = !!employeeId && !isLogout; // Return the plugin menu structure return { id: 'plugin-menu', label: TranslateService.instant('TIMER_TRACKER.SETTINGS.PLUGINS'), + visible, submenu: [ { label: TranslateService.instant('TIMER_TRACKER.MENU.INSTALL_PLUGIN'), diff --git a/packages/desktop-lib/src/lib/tray/commands/open-settings-command.ts b/packages/desktop-lib/src/lib/tray/commands/open-settings-command.ts index b9f61fd97f..eb9598c12b 100644 --- a/packages/desktop-lib/src/lib/tray/commands/open-settings-command.ts +++ b/packages/desktop-lib/src/lib/tray/commands/open-settings-command.ts @@ -1,29 +1,30 @@ import { ipcMain } from 'electron'; import { AppWindowManager } from '../../app-window-manager'; +import { IPathWindow } from '../../interfaces/i-path-window'; import { MenuCommand } from './menu-command'; export class OpenSettingsCommand extends MenuCommand { - constructor(private windowPath: any) { + constructor(private readonly windowPath: IPathWindow) { super(); } public async execute(): Promise { const appWindowManager = AppWindowManager.getInstance(); - if (!appWindowManager.settingWindow) { + const { timeTrackerUi } = this.windowPath; + + if (appWindowManager.settingWindow && !appWindowManager.settingWindow.isDestroyed()) { + appWindowManager.settingShow('goto_top_menu'); + } else { const onReady = () => appWindowManager.settingShow('goto_top_menu'); ipcMain.once('setting_window_ready', onReady); try { - await appWindowManager.initSettingWindow(this.windowPath.timeTrackerUi); - await appWindowManager.loadSetting( - this.windowPath.timeTrackerUi, - ); - } catch (err) { + await appWindowManager.initSettingWindow(timeTrackerUi); + await appWindowManager.loadSetting(timeTrackerUi); + } catch (error) { ipcMain.removeListener('setting_window_ready', onReady); - throw err; + throw error; } - appWindowManager.settingWindow?.show?.(); - } else { - appWindowManager.settingShow('goto_top_menu'); } + appWindowManager.settingWindow?.show?.(); } } diff --git a/packages/desktop-lib/src/lib/tray/handlers/auth-ipc-handler.ts b/packages/desktop-lib/src/lib/tray/handlers/auth-ipc-handler.ts index 5cb5a12e96..d7e296db3b 100644 --- a/packages/desktop-lib/src/lib/tray/handlers/auth-ipc-handler.ts +++ b/packages/desktop-lib/src/lib/tray/handlers/auth-ipc-handler.ts @@ -116,7 +116,8 @@ export class AuthIPCHandler { private closePluginsWindow(): void { try { const appWindowManager = AppWindowManager.getInstance(); - appWindowManager.pluginsWindow?.close?.(); + const pluginsWindow = appWindowManager.pluginsWindow; + if (pluginsWindow) pluginsWindow.close(); } catch (error) { console.error('An error occurred while closing plugin window', error); } diff --git a/packages/desktop-lib/src/lib/tray/strategies/unauthenticated-menu-strategy.ts b/packages/desktop-lib/src/lib/tray/strategies/unauthenticated-menu-strategy.ts index 8c546daee4..d9d146c07f 100644 --- a/packages/desktop-lib/src/lib/tray/strategies/unauthenticated-menu-strategy.ts +++ b/packages/desktop-lib/src/lib/tray/strategies/unauthenticated-menu-strategy.ts @@ -4,7 +4,6 @@ import { CheckUpdateCommand, ExitAppCommand, OpenMainWindowCommand, - OpenPluginsCommand, OpenSettingsCommand, OpenTimerCommand } from '../commands'; @@ -41,8 +40,8 @@ export class UnauthenticatedMenuStrategy implements IMenuStrategy { menu.push( new MenuItemBuilder() .withId('3') - .withLabel(this.translationService.instant('TIMER_TRACKER.MENU.OPEN_TIMER')) - .withAccelerator('CmdOrCtrl+O') + .withLabel(this.translationService.instant('LOGIN_PAGE.TITLE')) + .withAccelerator('CmdOrCtrl+L') .withCommand(new OpenTimerCommand(this.windowService)) .build() ); @@ -58,16 +57,6 @@ export class UnauthenticatedMenuStrategy implements IMenuStrategy { .build() ); - // Plugins - menu.push( - new MenuItemBuilder() - .withId('6-0') - .withLabel(this.translationService.instant('TIMER_TRACKER.SETTINGS.PLUGINS')) - .withAccelerator('CmdOrCtrl+P') - .withCommand(new OpenPluginsCommand(this.windowPath)) - .build() - ); - // Check update menu.push( new MenuItemBuilder() diff --git a/packages/desktop-lib/src/lib/tray/tray-icon.ts b/packages/desktop-lib/src/lib/tray/tray-icon.ts index 821a245a46..75c84717ef 100644 --- a/packages/desktop-lib/src/lib/tray/tray-icon.ts +++ b/packages/desktop-lib/src/lib/tray/tray-icon.ts @@ -1,6 +1,7 @@ import { RegisteredWindow } from '@gauzy/desktop-core'; import { ipcMain, nativeImage, Tray } from 'electron'; import * as path from 'node:path'; +import { PluginEventManager } from '../plugin-system/events/plugin-event.manager'; import { TrayIPCHandler } from './handlers/tray-ipc-handler'; import { IConfigStore, ITranslationService, ITrayIconConfig, IWindowService } from './interfaces'; import { TrayMenuManager } from './managers/tray-menu-manager'; @@ -11,29 +12,33 @@ import { ILanguageObserver, LanguageChangeSubject } from './observer/language-su * DOES NOT recreate on language change - only rebuilds menu */ export class TrayIcon implements ILanguageObserver { - private tray: Tray; - private menuManager: TrayMenuManager; - private trayIPCHandler: TrayIPCHandler; - private languageSubject: LanguageChangeSubject; + private readonly tray: Tray; + private readonly menuManager: TrayMenuManager; + private readonly trayIPCHandler: TrayIPCHandler; + private readonly languageSubject: LanguageChangeSubject; + private readonly pluginEventManager: PluginEventManager; constructor( - config: ITrayIconConfig, - private dependencies: { + private readonly config: ITrayIconConfig, + private readonly dependencies: { windowService: IWindowService; configStore: IConfigStore; translationService: ITranslationService; } ) { + // Initialize plugin event manager + this.pluginEventManager = PluginEventManager.getInstance(); + // Create tray icon - this.tray = this.createTray(config.iconPath); + this.tray = this.createTray(this.config.iconPath); // Create menu manager with Strategy pattern this.menuManager = new TrayMenuManager( this.tray, - dependencies.translationService, - dependencies.windowService, - dependencies.configStore, - config.windowPath + this.dependencies.translationService, + this.dependencies.windowService, + this.dependencies.configStore, + this.config.windowPath ); // Build initial menu @@ -42,8 +47,8 @@ export class TrayIcon implements ILanguageObserver { // Setup IPC handlers this.trayIPCHandler = new TrayIPCHandler( this.menuManager, - dependencies.windowService, - dependencies.configStore + this.dependencies.windowService, + this.dependencies.configStore ); this.trayIPCHandler.setupHandlers(); @@ -73,6 +78,7 @@ export class TrayIcon implements ILanguageObserver { */ onAuthStateChanged(isAuthenticated: boolean, authData?: any): void { this.menuManager.updateAuthState(isAuthenticated, authData); + this.pluginEventManager.notify(); } private createTray(iconPath: string): Tray { diff --git a/packages/desktop-window/src/lib/always-on.ts b/packages/desktop-window/src/lib/always-on.ts index bad2fee778..c8de5023de 100644 --- a/packages/desktop-window/src/lib/always-on.ts +++ b/packages/desktop-window/src/lib/always-on.ts @@ -1,17 +1,17 @@ -import { screen } from 'electron'; import { - IBaseWindow, BaseWindow, - WindowManager, DefaultWindow, + IBaseWindow, + RegisteredWindow, WindowConfig, - RegisteredWindow + WindowManager } from '@gauzy/desktop-core'; +import { screen } from 'electron'; export class AlwaysOn extends BaseWindow implements IBaseWindow { private static WIDTH: number = 60; private static HEIGHT: number = 110; - private manager = WindowManager.getInstance(); + private readonly manager = WindowManager.getInstance(); constructor( private readonly path?: string, @@ -32,42 +32,74 @@ export class AlwaysOn extends BaseWindow implements IBaseWindow { roundedCorners: true, width: AlwaysOn.WIDTH, height: AlwaysOn.HEIGHT, - ...(isExpandMode ? { - transparent: true - } : { - opacity: 0.8 - }), + ...(isExpandMode + ? { + transparent: true + } + : { + opacity: 0.8 + }), alwaysOnTop: true, center: false, x: 16, y: Math.floor((screen.getPrimaryDisplay().workAreaSize.height - AlwaysOn.HEIGHT) / 2), - ...(contextIsolation && preloadPath ? { - webPreferences: { - nodeIntegration: false, - contextIsolation: true, - sandbox: false, - webSecurity: false, - preload: preloadPath - }, - } : {}) + ...(contextIsolation && preloadPath + ? { + webPreferences: { + nodeIntegration: false, + contextIsolation: true, + sandbox: false, + webSecurity: false, + preload: preloadPath + } + } + : {}) }) ) ); this.browserWindow.setMenuBarVisibility(false); this.manager.register(RegisteredWindow.WIDGET, this); this.manager.overrideSystemContextMenu(this.browserWindow); + this.browserWindow.on('close', () => { + if (this.isDestroyed()) { + return; + } + this.browserWindow.destroy(); + }); + + this.browserWindow.on('closed', () => { + this.manager.unregister(RegisteredWindow.WIDGET); + }); } - public show(): void { + public override show(): void { + if (this.isDestroyed()) { + return; + } this.onTop(); super.show(); } - public hide(): void { + public override hide(): void { this.undoOnTop(); super.hide(); } + public override close(): void { + if (this.isDestroyed()) { + return; + } + this.hide(); + super.close(); + } + + public isDestroyed(): boolean { + if(!this.browserWindow) { + return true; + } + return this.browserWindow.isDestroyed(); + } + private onTop(): void { this.browserWindow.setSkipTaskbar(true); this.browserWindow.setVisibleOnAllWorkspaces(true, { diff --git a/packages/desktop-window/src/lib/desktop-splash-screen.ts b/packages/desktop-window/src/lib/desktop-splash-screen.ts index 8d7b1aa3d3..9e0791a0a3 100644 --- a/packages/desktop-window/src/lib/desktop-splash-screen.ts +++ b/packages/desktop-window/src/lib/desktop-splash-screen.ts @@ -1,10 +1,10 @@ import { - IBaseWindow, BaseWindow, - WindowManager, DefaultWindow, + IBaseWindow, + RegisteredWindow, WindowConfig, - RegisteredWindow + WindowManager } from '@gauzy/desktop-core'; /** @@ -12,51 +12,75 @@ import { * This class is responsible for creating and managing the splash screen. */ export class SplashScreen extends BaseWindow implements IBaseWindow { - private readonly manager = WindowManager.getInstance(); + private readonly manager = WindowManager.getInstance(); - /** - * Initializes a new instance of the SplashScreen class. - * - * @param {string} path - The file path to load in the splash screen. - */ - constructor(public path: string, public preloadPath?: string, public contextIsolation?: boolean) { - // Configure the splash screen with default properties - super( - new DefaultWindow( - new WindowConfig('/splash-screen', path, { - frame: false, - resizable: false, - width: 300, - height: 240 - }) - ) - ); + /** + * Initializes a new instance of the SplashScreen class. + * + * @param {string} path - The file path to load in the splash screen. + */ + constructor( + public path: string, + public preloadPath?: string, + public contextIsolation?: boolean + ) { + // Configure the splash screen with default properties + super( + new DefaultWindow( + new WindowConfig('/splash-screen', path, { + frame: false, + resizable: false, + width: 300, + height: 240 + }) + ) + ); - // Disable the menu bar for the splash screen - this.initializeWindowSettings(); + // Disable the menu bar for the splash screen + this.initializeWindowSettings(); if (contextIsolation) { this.config.options.webPreferences.contextIsolation = true; this.config.options.webPreferences.preload = preloadPath; this.config.options.webPreferences.nodeIntegration = false; } - // Register the splash screen with the WindowManager - this.registerWindow(); + // Register the splash screen with the WindowManager + this.registerWindow(); this.manager.overrideSystemContextMenu(this.browserWindow); - } + // Handle the close event to ensure proper cleanup + this.browserWindow.on('close', () => { + if (this.isDestroyed()) return; + this.browserWindow.destroy(); + }); - /** - * Initializes the settings for the splash screen window. - * Disables the menu bar and applies additional configurations. - */ - private initializeWindowSettings(): void { - this.browserWindow.setMenuBarVisibility(false); - } + this.browserWindow.on('closed', () => { + this.manager.unregister(RegisteredWindow.SPLASH); + }); + } - /** - * Registers the splash screen with the WindowManager. - */ - private registerWindow(): void { - this.manager.register(RegisteredWindow.SPLASH, this); - } + /** + * Initializes the settings for the splash screen window. + * Disables the menu bar and applies additional configurations. + */ + private initializeWindowSettings(): void { + this.browserWindow.setMenuBarVisibility(false); + } + + /** + * Registers the splash screen with the WindowManager. + */ + private registerWindow(): void { + this.manager.register(RegisteredWindow.SPLASH, this); + } + + /** + * Checks if the splash screen window has been destroyed. + * @return {boolean} `true` if the window is destroyed; otherwise, `false`. + */ + public isDestroyed(): boolean { + if (!this.browserWindow) { + return true; + } + return this.browserWindow.isDestroyed(); + } } diff --git a/packages/desktop-window/src/lib/plugin-marketplace.window.ts b/packages/desktop-window/src/lib/plugin-marketplace.window.ts index 850ce12fcb..84d3ffdc91 100644 --- a/packages/desktop-window/src/lib/plugin-marketplace.window.ts +++ b/packages/desktop-window/src/lib/plugin-marketplace.window.ts @@ -15,7 +15,11 @@ export class PluginMarketplaceWindow extends BaseWindow implements IBaseWindow { * * @param {string} path - The file path to load in the plugin marketplace window. */ - constructor(public path: string, public preloadPath?: string, public contextIsolation?: boolean) { + constructor( + public path: string, + public preloadPath?: string, + public contextIsolation?: boolean + ) { // Configure the plugin marketplace window with default properties super( new DefaultWindow( @@ -37,6 +41,47 @@ export class PluginMarketplaceWindow extends BaseWindow implements IBaseWindow { // Register the plugin marketplace window with the WindowManager this.registerWindow(); this.manager.overrideSystemContextMenu(this.browserWindow); + + this.browserWindow.on('close', () => { + if (this.isDestroyed()) return; + this.browserWindow.destroy(); + }); + + this.browserWindow.on('closed', () => { + this.manager.unregister(RegisteredWindow.PLUGINS); + }); + } + + public override show(): void { + if (this.isDestroyed()) { + return; + } + + if (this.browserWindow.isMinimized()) { + this.browserWindow.restore(); + } + + super.show(); + this.browserWindow.focus(); + } + + public override close(): void { + if (this.isDestroyed()) { + return; + } + + super.close(); + } + + /** + * Checks if the plugin marketplace window has been destroyed. + * @return {boolean} `true` if the window is destroyed; otherwise, `false`. + */ + public isDestroyed(): boolean { + if (!this.browserWindow) { + return true; + } + return this.browserWindow.isDestroyed(); } /**