From 3e63a7a5dfef56c8b2d1c062b844f19ac79c84d4 Mon Sep 17 00:00:00 2001 From: Denis 'GeneralGDA' Gladkiy Date: Fri, 17 Oct 2025 09:50:11 +0600 Subject: [PATCH] Launch at login option for Windows Desktop. (#10105) Signed-off-by: Denis Gladkiy --- desktop/src/__test__/main/settings.test.ts | 13 ++++ desktop/src/main/customMenu.ts | 18 +++-- desktop/src/main/osIntegration.ts | 39 ++++++++++ desktop/src/main/settings.ts | 9 +++ desktop/src/main/start.ts | 61 +++++++++------- desktop/src/ui/index.ts | 18 ++++- desktop/src/ui/ipcMessages.ts | 47 ++++++++++++ desktop/src/ui/notifications.ts | 15 ++++ desktop/src/ui/platform.ts | 11 +++ desktop/src/ui/preload.ts | 83 +++++++++++++--------- desktop/src/ui/screenShare.ts | 15 ++++ desktop/src/ui/titleBarMenu.ts | 30 ++++++-- desktop/src/ui/types.ts | 20 +++++- 13 files changed, 307 insertions(+), 72 deletions(-) create mode 100644 desktop/src/main/osIntegration.ts create mode 100644 desktop/src/ui/ipcMessages.ts diff --git a/desktop/src/__test__/main/settings.test.ts b/desktop/src/__test__/main/settings.test.ts index 0c6a356457..d13fee0517 100644 --- a/desktop/src/__test__/main/settings.test.ts +++ b/desktop/src/__test__/main/settings.test.ts @@ -155,6 +155,19 @@ describe('Settings', () => { systemUnderTest.setMinimizeToTrayEnabled(value) expect(systemUnderTest.isMinimizeToTrayEnabled()).toBe(value) + expect(systemUnderTest.isAutoLaunchEnabled()).toBe(false) + }) + }) + + describe('setAutoLaunchEnabled', () => { + test.each([ + { value: true, description: 'stored value is true' }, + { value: false, description: 'stored value is false' } + ])('$description', ({ value }) => { + systemUnderTest.setAutoLaunchEnabled(value) + + expect(systemUnderTest.isAutoLaunchEnabled()).toBe(value) + expect(systemUnderTest.isMinimizeToTrayEnabled()).toBe(false) }) }) diff --git a/desktop/src/main/customMenu.ts b/desktop/src/main/customMenu.ts index 84b20eac2c..ef28ed9b35 100644 --- a/desktop/src/main/customMenu.ts +++ b/desktop/src/main/customMenu.ts @@ -15,9 +15,10 @@ import { BrowserWindow } from 'electron' import { MenuBarAction, CommandLogout, CommandSelectWorkspace, CommandOpenSettings } from '../ui/types' -import { TrayController } from './tray' +import { OsIntegration } from './osIntegration' +import { IpcMessage } from '../ui/ipcMessages' -export function dispatchMenuBarAction (mainWindow: BrowserWindow | undefined, action: MenuBarAction, tray: TrayController | undefined): void { +export function dispatchMenuBarAction (mainWindow: BrowserWindow | undefined, action: MenuBarAction, os: OsIntegration | undefined): void { if (mainWindow == null) { return } @@ -88,9 +89,16 @@ export function dispatchMenuBarAction (mainWindow: BrowserWindow | undefined, ac mainWindow.setFullScreen(!mainWindow.isFullScreen()) break case 'toggle-minimize-to-tray': { - if (tray != null) { - const newSetting = tray.toggleMinimizeToTray() - mainWindow.webContents.send('minimize-to-tray-setting-changed', newSetting) + if (os != null) { + const newSetting = os.getTray().toggleMinimizeToTray() + mainWindow.webContents.send(IpcMessage.MinimizeToTraySettingChanged, newSetting) + } + break + } + case 'toggle-auto-launch': { + if (os != null) { + const newSetting = os.toggleAutoLaunch() + mainWindow.webContents.send(IpcMessage.AutoLaunchSettingChanged, newSetting) } break } diff --git a/desktop/src/main/osIntegration.ts b/desktop/src/main/osIntegration.ts new file mode 100644 index 0000000000..c8229c49b1 --- /dev/null +++ b/desktop/src/main/osIntegration.ts @@ -0,0 +1,39 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import { app } from 'electron' +import { TrayController } from './tray' +import { Settings } from './settings' + +export class OsIntegration { + constructor ( + private readonly settings: Settings, + private readonly tray: TrayController) { + } + + getTray (): TrayController { + return this.tray + } + + toggleAutoLaunch (): boolean { + const settings = app.getLoginItemSettings() + const previousValue = settings.openAtLogin + const newValue = !previousValue + settings.openAtLogin = newValue + app.setLoginItemSettings(settings) + this.settings.setAutoLaunchEnabled(newValue) + return newValue + } +} diff --git a/desktop/src/main/settings.ts b/desktop/src/main/settings.ts index 53f9e244ab..2eaf8cb46e 100644 --- a/desktop/src/main/settings.ts +++ b/desktop/src/main/settings.ts @@ -19,6 +19,7 @@ import { PackedConfig } from './config' export class Settings { private static readonly SETTINGS_KEY_SERVER = 'server' private static readonly SETTINGS_KEY_MINIMIZE_TO_TRAY = 'minimize-to-tray' + private static readonly SETTINGS_KEY_AUTO_LAUNCH = 'auto-launch' private static readonly SETTINGS_KEY_WINDOW_BOUNDS = 'windowBounds' private readonly store: Store @@ -41,10 +42,18 @@ export class Settings { return (this.store as any).get(Settings.SETTINGS_KEY_MINIMIZE_TO_TRAY) as boolean ?? false } + isAutoLaunchEnabled (): boolean { + return (this.store as any).get(Settings.SETTINGS_KEY_AUTO_LAUNCH) as boolean ?? false + } + setMinimizeToTrayEnabled (enabled: boolean): void { (this.store as any).set(Settings.SETTINGS_KEY_MINIMIZE_TO_TRAY, enabled) } + setAutoLaunchEnabled (enabled: boolean): void { + (this.store as any).set(Settings.SETTINGS_KEY_AUTO_LAUNCH, enabled) + } + setServerUrl (serverUrl: string): void { const sanitizedUrl: string = Settings.sanitizeUrl(serverUrl) ;(this.store as any).set(Settings.SETTINGS_KEY_SERVER, sanitizedUrl) diff --git a/desktop/src/main/start.ts b/desktop/src/main/start.ts index eef60b5b07..ffe16d40ff 100644 --- a/desktop/src/main/start.ts +++ b/desktop/src/main/start.ts @@ -35,6 +35,8 @@ import { readPackedConfig } from './config' import { Settings } from './settings' import { TrayController } from './tray' import { getFileInPublicBundledFolder } from './path' +import { OsIntegration } from './osIntegration' +import { IpcMessage } from '../ui/ipcMessages' let isQuiting = false @@ -211,7 +213,7 @@ function runTheApp (): void { void (async (): Promise => { await window.loadFile(containerPagePath) - window.webContents.send('handle-auth', urlObj.searchParams.get('token')) + window.webContents.send(IpcMessage.HandleAuth, urlObj.searchParams.get('token')) })() } }) @@ -231,7 +233,7 @@ function runTheApp (): void { url: item.getURL(), savePath: item.getSavePath() } - window.webContents.send('handle-download-item', download) + window.webContents.send(IpcMessage.HandleDownloadItem, download) } notifyDownloadUpdated() @@ -275,7 +277,7 @@ function runTheApp (): void { hookOpenWindow(mainWindow) function minimizeToTrayIsOn (): boolean { - return settings.isMinimizeToTrayEnabled() && trayController != null + return settings.isMinimizeToTrayEnabled() && osIntegration != null } mainWindow.on('close', (event: Event) => { @@ -299,7 +301,7 @@ function runTheApp (): void { }) function sendWindowMaximizedMessage (maximized: boolean): void { - mainWindow?.webContents.send('window-state-changed', maximized ? 'maximized' : 'unmaximized') + mainWindow?.webContents.send(IpcMessage.WindowStateChanged, maximized ? 'maximized' : 'unmaximized') } mainWindow.on('focus', () => { @@ -313,7 +315,7 @@ function runTheApp (): void { mainWindow.on('blur', () => { globalShortcut.unregister(CloseTabHotKey) - mainWindow?.webContents.send('window-focus-loss') + mainWindow?.webContents.send(IpcMessage.WindowFocusLoss) }) mainWindow.on('maximize', () => { @@ -361,10 +363,11 @@ function runTheApp (): void { } } - let trayController: TrayController | undefined + let osIntegration: OsIntegration | undefined void app.whenReady().then(() => { - trayController = new TrayController(settings, activateWindow, quitApplication) + const trayController = new TrayController(settings, activateWindow, quitApplication) + osIntegration = new OsIntegration(settings, trayController) }) if (isWindows) { @@ -379,40 +382,40 @@ function runTheApp (): void { showSelectAll: false }) - ipcMain.on('set-badge', (_event: any, badge: number) => { + ipcMain.on(IpcMessage.SetBadge, (_event: any, badge: number) => { app.dock?.setBadge(badge > 0 ? `${badge}` : '') app.badgeCount = badge if (isWindows && winBadge !== undefined) { winBadge.update(badge) } - trayController?.updateTrayBadge(badge) + osIntegration?.getTray().updateTrayBadge(badge) }) - ipcMain.on('dock-bounce', (_event: any) => { + ipcMain.on(IpcMessage.DockBounce, (_event: any) => { app.dock?.bounce('informational') }) - ipcMain.on('send-notification', (_event: any, notificationParams: NotificationParams) => { + ipcMain.on(IpcMessage.SendNotification, (_event: any, notificationParams: NotificationParams) => { if (Notification.isSupported()) { const notification = new Notification(notificationParams) notification.on('click', () => { mainWindow?.show() - mainWindow?.webContents.send('handle-notification-navigation', notificationParams) + mainWindow?.webContents.send(IpcMessage.HandleNotificationNavigation, notificationParams) }) notification.show() } }) - ipcMain.on('set-title', (event: any, title: string) => { + ipcMain.on(IpcMessage.SetTitle, (event: any, title: string) => { const webContents = event.sender const window = BrowserWindow.fromWebContents(webContents) window?.setTitle(title) }) - ipcMain.on('set-combined-config', (_event: any, config: Config) => { + ipcMain.on(IpcMessage.SetCombinedConfig, (_event: any, config: Config) => { log.info('Config set: ', config) setupCookieHandler(config) @@ -444,7 +447,7 @@ function runTheApp (): void { void autoUpdater.checkForUpdatesAndNotify() }) - ipcMain.handle('get-main-config', (_event: any, _path: any) => { + ipcMain.handle(IpcMessage.GetMainConfig, (_event: any, _path: any) => { const cfg = { CONFIG_URL: process.env.CONFIG_URL ?? '', FRONT_URL, @@ -454,11 +457,11 @@ function runTheApp (): void { } return cfg }) - ipcMain.handle('get-host', (_event: any, _path: any) => { + ipcMain.handle(IpcMessage.GetHost, (_event: any, _path: any) => { return new URL(FRONT_URL).host }) - ipcMain.on('set-front-cookie', function (event: any, host: string, name: string, value: string) { + ipcMain.on(IpcMessage.SetFrontCookie, function (event: any, host: string, name: string, value: string) { const webContents = event.sender const win = BrowserWindow.fromWebContents(webContents) const cv: CookiesSetDetails = { @@ -474,11 +477,11 @@ function runTheApp (): void { void win?.webContents?.session.cookies.set(cv) }) - ipcMain.handle('window-minimize', () => { + ipcMain.handle(IpcMessage.WindowMinimize, () => { mainWindow?.minimize() }) - ipcMain.handle('window-maximize', () => { + ipcMain.handle(IpcMessage.WindowMaximize, () => { if (mainWindow != null) { if (mainWindow.isMaximized()) { mainWindow.unmaximize() @@ -488,26 +491,26 @@ function runTheApp (): void { } }) - ipcMain.handle('window-close', () => { + ipcMain.handle(IpcMessage.WindowClose, () => { mainWindow?.close() }) - ipcMain.handle('get-is-os-using-dark-theme', () => { + ipcMain.handle(IpcMessage.GetIsOsUsingDarkTheme, () => { return nativeTheme.shouldUseDarkColors }) - ipcMain.handle('menu-action', async (_event: any, action: MenuBarAction) => { - dispatchMenuBarAction(mainWindow, action, trayController) + ipcMain.handle(IpcMessage.MenuAction, async (_event: any, action: MenuBarAction) => { + dispatchMenuBarAction(mainWindow, action, osIntegration) }) if (isWindows) { - ipcMain.on('rebuild-user-jump-list', (_event: any, spares: JumpListSpares) => { + ipcMain.on(IpcMessage.RebuildUserJumpList, (_event: any, spares: JumpListSpares) => { rebuildJumpList(spares) }) } - ipcMain.handle('get-screen-access', () => systemPreferences.getMediaAccessStatus('screen') === 'granted') - ipcMain.handle('get-screen-sources', () => { + ipcMain.handle(IpcMessage.GetScreenAccess, () => systemPreferences.getMediaAccessStatus('screen') === 'granted') + ipcMain.handle(IpcMessage.GetScreenSources, () => { return desktopCapturer.getSources({ types: ['window', 'screen'], fetchWindowIcons: true, thumbnailSize: { width: 225, height: 135 } }).then(async sources => { return sources.map((source: any) => { return { @@ -523,6 +526,10 @@ function runTheApp (): void { return settings.isMinimizeToTrayEnabled() }) + ipcMain.handle(IpcMessage.GetAutoLaunchEnabled, () => { + return settings.isAutoLaunchEnabled() + }) + async function onReady (): Promise { await createWindow() @@ -605,7 +612,7 @@ function runTheApp (): void { return } mainWindow.setProgressBar(percent / 100) - mainWindow.webContents.send('handle-update-download-progress', percent) + mainWindow.webContents.send(IpcMessage.HandleUpdateDownloadProgress, percent) } autoUpdater.on('update-downloaded', (_info: any) => { diff --git a/desktop/src/ui/index.ts b/desktop/src/ui/index.ts index e5ec3d6454..6aa383ed5c 100644 --- a/desktop/src/ui/index.ts +++ b/desktop/src/ui/index.ts @@ -1,3 +1,18 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + import { loginId } from '@hcengineering/login' import { loveId } from '@hcengineering/love' import { timeId } from '@hcengineering/time' @@ -32,6 +47,7 @@ import { ipcMainExposed } from './typesUtils' import { themeStore, ThemeVariant } from '@hcengineering/theme' import type { Application } from '@hcengineering/workbench' import { isAllowedToRole } from '@hcengineering/workbench-resources' +import { IpcMessage } from './ipcMessages' function currentOsIsWindows (): boolean { return (window as any).windowsPlatform === true @@ -231,7 +247,7 @@ window.addEventListener('DOMContentLoaded', () => { void handleDownloadItem(item) }) - ipcMain.on('start-backup', () => { + ipcMain.on(IpcMessage.StartBackup, () => { // We need to obtain current token and endpoint and trigger backup const token = getMetadata(presentation.metadata.Token) const endpoint = getMetadata(presentation.metadata.Endpoint) diff --git a/desktop/src/ui/ipcMessages.ts b/desktop/src/ui/ipcMessages.ts new file mode 100644 index 0000000000..759c593d35 --- /dev/null +++ b/desktop/src/ui/ipcMessages.ts @@ -0,0 +1,47 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +export const IpcMessage = { + AutoLaunchSettingChanged: 'auto-launch-setting-changed', + GetAutoLaunchEnabled: 'get-auto-launch-enabled', + MinimizeToTraySettingChanged: 'minimize-to-tray-setting-changed', + GetMinimizeToTrayEnabled: 'get-minimize-to-tray-enabled', + RebuildUserJumpList: 'rebuild-user-jump-list', + StartBackup: 'start-backup', + CancelBackup: 'cancel-backup', + GetScreenSources: 'get-screen-sources', + GetScreenAccess: 'get-screen-access', + SetFrontCookie: 'set-front-cookie', + HandleDownloadItem: 'handle-download-item', + SetBadge: 'set-badge', + SetTitle: 'set-title', + DockBounce: 'dock-bounce', + SendNotification: 'send-notification', + WindowMinimize: 'window-minimize', + WindowMaximize: 'window-maximize', + WindowClose: 'window-close', + WindowStateChanged: 'window-state-changed', + WindowFocusLoss: 'window-focus-loss', + GetIsOsUsingDarkTheme: 'get-is-os-using-dark-theme', + MenuAction: 'menu-action', + GetMainConfig: 'get-main-config', + SetCombinedConfig: 'set-combined-config', + GetHost: 'get-host', + HandleDeepLink: 'handle-deep-link', + OnDeepLinkHandler: 'on-deep-link-handler', + HandleNotificationNavigation: 'handle-notification-navigation', + HandleUpdateDownloadProgress: 'handle-update-download-progress', + HandleAuth: 'handle-auth' +} as const diff --git a/desktop/src/ui/notifications.ts b/desktop/src/ui/notifications.ts index f604ced72d..579fe78c9a 100644 --- a/desktop/src/ui/notifications.ts +++ b/desktop/src/ui/notifications.ts @@ -1,3 +1,18 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + import { formatName, getPersonByPersonId } from '@hcengineering/contact' import { Ref, SortingOrder, TxOperations } from '@hcengineering/core' import notification, { diff --git a/desktop/src/ui/platform.ts b/desktop/src/ui/platform.ts index f714f43059..abe1899a7b 100644 --- a/desktop/src/ui/platform.ts +++ b/desktop/src/ui/platform.ts @@ -1,6 +1,17 @@ // // Copyright © 2023 Hardcore Engineering Inc. // +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// import { Plugin, diff --git a/desktop/src/ui/preload.ts b/desktop/src/ui/preload.ts index a5e324c43c..57fe324203 100644 --- a/desktop/src/ui/preload.ts +++ b/desktop/src/ui/preload.ts @@ -1,11 +1,22 @@ -// preload.js +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// import { contextBridge, ipcRenderer } from 'electron' import { BrandingMap, Config, IPCMainExposed, JumpListSpares, MenuBarAction, NotificationParams } from './types' +import { IpcMessage } from './ipcMessages' -/** - * @public - */ export function concatLink (host: string, path: string): string { if (!host.endsWith('/') && !path.startsWith('/')) { return `${host}/${path}` @@ -46,50 +57,50 @@ let configPromise: Promise | undefined const expose: IPCMainExposed = { setBadge: (badge: number) => { - ipcRenderer.send('set-badge', badge) + ipcRenderer.send(IpcMessage.SetBadge, badge) }, setTitle: (title: string) => { - ipcRenderer.send('set-title', title) + ipcRenderer.send(IpcMessage.SetTitle, title) }, dockBounce: () => { - ipcRenderer.send('dock-bounce') + ipcRenderer.send(IpcMessage.DockBounce) }, sendNotification: (notificationParams: NotificationParams) => { - ipcRenderer.send('send-notification', notificationParams) + ipcRenderer.send(IpcMessage.SendNotification, notificationParams) }, minimizeWindow: () => { - ipcRenderer.invoke('window-minimize') + void ipcRenderer.invoke(IpcMessage.WindowMinimize) }, maximizeWindow: () => { - ipcRenderer.invoke('window-maximize') + void ipcRenderer.invoke(IpcMessage.WindowMaximize) }, closeWindow: () => { - ipcRenderer.invoke('window-close') + void ipcRenderer.invoke(IpcMessage.WindowClose) }, onWindowStateChange: (callback) => { - ipcRenderer.on('window-state-changed', callback) + ipcRenderer.on(IpcMessage.WindowStateChanged, callback) }, onWindowFocusLoss: (callback) => { - ipcRenderer.on('window-focus-loss', callback) + ipcRenderer.on(IpcMessage.WindowFocusLoss, callback) }, isOsUsingDarkTheme: async () => { - return await ipcRenderer.invoke('get-is-os-using-dark-theme') + return await ipcRenderer.invoke(IpcMessage.GetIsOsUsingDarkTheme) }, executeMenuBarAction: (action: MenuBarAction) => { - ipcRenderer.invoke('menu-action', action) + void ipcRenderer.invoke(IpcMessage.MenuAction, action) }, config: async () => { if (configPromise === undefined) { configPromise = new Promise((resolve, reject) => { - ipcRenderer.invoke('get-main-config').then( + ipcRenderer.invoke(IpcMessage.GetMainConfig).then( async (mainConfig) => { const serverConfig = await loadServerConfig(concatLink(mainConfig.FRONT_URL, mainConfig.CONFIG_URL)) const combinedConfig = { @@ -103,7 +114,7 @@ const expose: IPCMainExposed = { VERSION: mainConfig.VERSION } - ipcRenderer.send('set-combined-config', combinedConfig) + ipcRenderer.send(IpcMessage.SetCombinedConfig, combinedConfig) resolve(combinedConfig) }, @@ -121,7 +132,7 @@ const expose: IPCMainExposed = { const branding: BrandingMap = await ( await fetch(cfg.BRANDING_URL ?? concatLink(cfg.FRONT_URL, 'branding.json'), { keepalive: true }) ).json() - const host = await ipcRenderer.invoke('get-host') + const host = await ipcRenderer.invoke(IpcMessage.GetHost) return branding[host] ?? {} }, on: (event: string, op: (...args: any[]) => void) => { @@ -132,58 +143,64 @@ const expose: IPCMainExposed = { }, handleDeepLink: (callback) => { - ipcRenderer.on('handle-deep-link', (event, value) => { + ipcRenderer.on(IpcMessage.HandleDeepLink, (event, value) => { try { if (typeof value === 'string' && value !== '') { callback(value) } } catch (e) { - // Just do nothing. Nothing is ok if there is something with URL + // Just do nothing. Nothing is ok if there is something with URL. } }) - ipcRenderer.send('on-deep-link-handler') + ipcRenderer.send(IpcMessage.OnDeepLinkHandler) }, handleNotificationNavigation: (callback) => { - ipcRenderer.on('handle-notification-navigation', (event, notificationParams) => { + ipcRenderer.on(IpcMessage.HandleNotificationNavigation, (event, notificationParams) => { callback(notificationParams) }) }, handleUpdateDownloadProgress: (callback) => { - ipcRenderer.on('handle-update-download-progress', (event, value) => { + ipcRenderer.on(IpcMessage.HandleUpdateDownloadProgress, (event, value) => { callback(value) }) }, handleAuth: (callback) => { - ipcRenderer.on('handle-auth', (event, value) => { + ipcRenderer.on(IpcMessage.HandleAuth, (event, value) => { callback(value) }) }, handleDownloadItem: (callback) => { - ipcRenderer.on('handle-download-item', (event, value) => { + ipcRenderer.on(IpcMessage.HandleDownloadItem, (event, value) => { callback(value) }) }, async setFrontCookie (host: string, name: string, value: string): Promise { - ipcRenderer.send('set-front-cookie', host, name, value) + ipcRenderer.send(IpcMessage.SetFrontCookie, host, name, value) }, - getScreenAccess: () => ipcRenderer.invoke('get-screen-access'), - getScreenSources: () => ipcRenderer.invoke('get-screen-sources'), - cancelBackup: () => { ipcRenderer.send('cancel-backup') }, - startBackup: (token, endpoint, wsIds) => { ipcRenderer.send('start-backup', token, endpoint, wsIds) }, + getScreenAccess: () => ipcRenderer.invoke(IpcMessage.GetScreenAccess), + getScreenSources: () => ipcRenderer.invoke(IpcMessage.GetScreenSources), + cancelBackup: () => { ipcRenderer.send(IpcMessage.CancelBackup) }, + startBackup: (token, endpoint, wsIds) => { ipcRenderer.send(IpcMessage.StartBackup, token, endpoint, wsIds) }, - rebuildJumpList: (spares: JumpListSpares) => { ipcRenderer.send('rebuild-user-jump-list', spares) }, + rebuildJumpList: (spares: JumpListSpares) => { ipcRenderer.send(IpcMessage.RebuildUserJumpList, spares) }, isMinimizeToTrayEnabled: async () => { - return await ipcRenderer.invoke('get-minimize-to-tray-enabled') + return await ipcRenderer.invoke(IpcMessage.GetMinimizeToTrayEnabled) }, onMinimizeToTraySettingChanged: (callback: (enabled: boolean) => void) => { - ipcRenderer.on('minimize-to-tray-setting-changed', (_event, enabled: boolean) => { callback(enabled) }) + ipcRenderer.on(IpcMessage.MinimizeToTraySettingChanged, (_event, enabled: boolean) => { callback(enabled) }) + }, + isAutoLaunchEnabled: async () => { + return await ipcRenderer.invoke(IpcMessage.GetAutoLaunchEnabled) + }, + onAutoLaunchSettingChanged: (callback: (enabled: boolean) => void) => { + ipcRenderer.on(IpcMessage.AutoLaunchSettingChanged, (_event, enabled: boolean) => { callback(enabled) }) } } contextBridge.exposeInMainWorld('electron', expose) diff --git a/desktop/src/ui/screenShare.ts b/desktop/src/ui/screenShare.ts index 10554b617c..dfe6fa7b93 100644 --- a/desktop/src/ui/screenShare.ts +++ b/desktop/src/ui/screenShare.ts @@ -1,3 +1,18 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + import log from 'electron-log' import love from '@hcengineering/love' import { setCustomCreateScreenTracks } from '@hcengineering/love-resources' diff --git a/desktop/src/ui/titleBarMenu.ts b/desktop/src/ui/titleBarMenu.ts index 6121d38224..43847b2d67 100644 --- a/desktop/src/ui/titleBarMenu.ts +++ b/desktop/src/ui/titleBarMenu.ts @@ -19,13 +19,19 @@ import { TitleBarMenuState } from './titleBarMenuState' import { ThemeVariant, type ThemeVariantType } from '@hcengineering/theme' const ToggleMinimizeToTrayAction: MenuBarAction = 'toggle-minimize-to-tray' +const ToggleAutoLaunchAction: MenuBarAction = 'toggle-auto-launch' const LabelMinimizeToTrayEnabled = '☑ Minimize to tray' const LabelMinimizeToTrayDisabled = '☐ Minimize to tray' +const LabelAutoLaunchEnabled = '☑ Launch at Login' +const LabelAutoLaunchDisabled = '☐ Launch at Login' + export async function setupTitleBarMenu (ipcMain: IPCMainExposed, root: HTMLElement): Promise { const themeManager = new ThemeManager(ThemeVariant.Light) - const menuManager = new MenuBarManager(root, await ipcMain.isMinimizeToTrayEnabled()) + const minimizeToTrayEnabled = await ipcMain.isMinimizeToTrayEnabled() + const isAutoLaunchEnabled = await ipcMain.isAutoLaunchEnabled() + const menuManager = new MenuBarManager(root, minimizeToTrayEnabled, isAutoLaunchEnabled) const menuBar = menuManager.getView() @@ -47,6 +53,13 @@ export async function setupTitleBarMenu (ipcMain: IPCMainExposed, root: HTMLElem } }) + ipcMain.onAutoLaunchSettingChanged((enabled) => { + const toggle = root.querySelector(`[data-action="${ToggleAutoLaunchAction}"]`) + if (toggle != null) { + toggle.textContent = enabled ? LabelAutoLaunchEnabled : LabelAutoLaunchDisabled + } + }) + ipcMain.onWindowStateChange((_event, state) => { const maximizeButton = root.querySelector('#maximize-button') if (maximizeButton != null) { @@ -70,7 +83,7 @@ export class MenuBar { } } -export function buildHulyApplicationMenu (minimizeToTrayEnabled: boolean): HTMLElement { +export function buildHulyApplicationMenu (minimizeToTrayEnabled: boolean, autoLaunchEnabled: boolean): HTMLElement { const menuBuilder = new MenuBuilder() const MenuFileIndex = 0 @@ -106,9 +119,12 @@ export function buildHulyApplicationMenu (minimizeToTrayEnabled: boolean): HTMLE .addMenuItem(MenuViewIndex, 'Toggle Fullscreen', 'toggle-fullscreen', 'F11', 'l') const MenuWindowIndex = 3 + const ToggleMinimizeToTrayLabel = minimizeToTrayEnabled ? LabelMinimizeToTrayEnabled : LabelMinimizeToTrayDisabled - menuBuilder.addTopLevelMenu('Window', 'w') + const ToggleAutoLaunchLabel = autoLaunchEnabled ? LabelAutoLaunchEnabled : LabelAutoLaunchDisabled + menuBuilder.addTopLevelMenu('System', 's') .addMenuItem(MenuWindowIndex, ToggleMinimizeToTrayLabel, 'toggle-minimize-to-tray', undefined, 'm') + .addMenuItem(MenuWindowIndex, ToggleAutoLaunchLabel, 'toggle-auto-launch', undefined, 'a') return menuBuilder.build() } @@ -298,7 +314,11 @@ class MenuBarManager { private readonly StateStyleKeyboardSelected = 'desktop-app-keyboard-selected' private readonly StateStyleAltModeActive = 'desktop-app-alt-active' - constructor (private readonly root: HTMLElement, minimizeToTrayEnabled: boolean) { + constructor ( + private readonly root: HTMLElement, + minimizeToTrayEnabled: boolean, + autoLaunchEnabled: boolean + ) { this.state = new TitleBarMenuState( () => this.topLevelMenus().length, (topLevelMenuIndex: number) => { @@ -307,7 +327,7 @@ class MenuBarManager { } ) - this.view = buildHulyApplicationMenu(minimizeToTrayEnabled) + this.view = buildHulyApplicationMenu(minimizeToTrayEnabled, autoLaunchEnabled) } public getView (): HTMLElement { diff --git a/desktop/src/ui/types.ts b/desktop/src/ui/types.ts index 12dd6a584b..ab9d0340df 100644 --- a/desktop/src/ui/types.ts +++ b/desktop/src/ui/types.ts @@ -1,3 +1,18 @@ +// +// Copyright © 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + import { DownloadItem } from '@hcengineering/desktop-downloads' import { ScreenSource } from '@hcengineering/love' import { Plugin } from '@hcengineering/platform' @@ -122,7 +137,8 @@ export const MenuBarActions = [ 'zoom-out', 'restore-size', 'toggle-fullscreen', - 'toggle-minimize-to-tray'] as const + 'toggle-minimize-to-tray', + 'toggle-auto-launch'] as const export type MenuBarAction = typeof MenuBarActions[number] @@ -171,6 +187,8 @@ export interface IPCMainExposed { isMinimizeToTrayEnabled: () => Promise onMinimizeToTraySettingChanged: (callback: (enabled: boolean) => void) => void + isAutoLaunchEnabled: () => Promise + onAutoLaunchSettingChanged: (callback: (enabled: boolean) => void) => void } export type SendCommandDelegate = (cmd: Command, ...args: any[]) => void