Launch at login option for Windows Desktop. (#10105)

Signed-off-by: Denis Gladkiy <denis.gladkiy@hardcoreeng.com>
This commit is contained in:
Denis 'GeneralGDA' Gladkiy
2025-10-17 09:50:11 +06:00
committed by GitHub
parent e573b19f72
commit 3e63a7a5df
13 changed files with 307 additions and 72 deletions
@@ -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)
})
})
+13 -5
View File
@@ -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
}
+39
View File
@@ -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
}
}
+9
View File
@@ -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)
+34 -27
View File
@@ -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<void> => {
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<void> {
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) => {
+17 -1
View File
@@ -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)
+47
View File
@@ -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
+15
View File
@@ -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, {
+11
View File
@@ -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,
+50 -33
View File
@@ -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<Config> | 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<void> {
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)
+15
View File
@@ -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'
+25 -5
View File
@@ -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<MenuBar> {
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 {
+19 -1
View File
@@ -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<boolean>
onMinimizeToTraySettingChanged: (callback: (enabled: boolean) => void) => void
isAutoLaunchEnabled: () => Promise<boolean>
onAutoLaunchSettingChanged: (callback: (enabled: boolean) => void) => void
}
export type SendCommandDelegate = (cmd: Command, ...args: any[]) => void