diff --git a/packages/uhk-agent/src/electron-main.ts b/packages/uhk-agent/src/electron-main.ts index 21715136e32..a1057a3297f 100644 --- a/packages/uhk-agent/src/electron-main.ts +++ b/packages/uhk-agent/src/electron-main.ts @@ -21,6 +21,7 @@ import { AppUpdateService } from './services/app-update.service'; import { AppService } from './services/app.service'; import { SudoService } from './services/sudo.service'; import { SmartMacroDocService } from './services/smart-macro-doc.service'; +import { TrayService } from './services/tray.service'; import isDev from 'electron-is-dev'; import { setMenu } from './electron-menu'; import { loadWindowState, saveWindowState } from './util/window'; @@ -58,6 +59,7 @@ let appService: AppService; let sudoService: SudoService; let packagesDir: string; let smartMacroDocService: SmartMacroDocService; +let trayService: TrayService; let areServicesInited = false; @@ -89,6 +91,7 @@ async function createWindow() { logger.misc('[Electron Main] Create new window.'); + const iconPath = path.join(import.meta.dirname, 'renderer/assets/images/agent-app-icon.png'); const loadedWindowState = loadWindowState(logger); if(!smartMacroDocService.isRunning) { await smartMacroDocService.start(); @@ -106,11 +109,14 @@ async function createWindow() { spellcheck: false, preload: path.join(import.meta.dirname, 'preload.js') }, - icon: path.join(import.meta.dirname, 'renderer/assets/images/agent-app-icon.png'), + icon: iconPath, backgroundColor: await getWindowBackgroundColor(), show: false }); + trayService = new TrayService(logger, iconPath); + trayService.init(win); + if (loadedWindowState.isFullScreen) { win.setFullScreen(true); } else if (loadedWindowState.isMaximized) { @@ -143,7 +149,15 @@ async function createWindow() { }); win.once('ready-to-show', () => { - win.show(); + void (async () => { + await trayService.initTrayIfEnabled(); + + if (options['start-minimized-to-tray']) { + trayService.startInTray(); + } else { + win.show(); + } + })(); }); win.webContents.on('did-finish-load', () => { @@ -273,6 +287,10 @@ if (isSecondInstance) { app.exit(); }); + app.on('before-quit', () => { + trayService?.markQuitting(); + }); + app.on('will-quit', () => { }); @@ -284,16 +302,15 @@ if (isSecondInstance) { .catch((error) => { logger.error('[Electron Main] when activating the app: ', error); }); + } else if (!win.isVisible()) { + trayService.showWindow(); } }); app.on('second-instance', () => { // Someone tried to run a second instance, we should focus our window. if (win) { - if (win.isMinimized()) { - win.restore(); - } - win.focus(); + trayService.showWindow(); } }); } diff --git a/packages/uhk-agent/src/services/tray.service.ts b/packages/uhk-agent/src/services/tray.service.ts new file mode 100644 index 00000000000..3300f3b77d2 --- /dev/null +++ b/packages/uhk-agent/src/services/tray.service.ts @@ -0,0 +1,364 @@ +import { app, BrowserWindow, ipcMain, Menu, nativeImage, Tray } from 'electron'; + +import { IpcEvents, LogService } from 'uhk-common'; +import { getApplicationSettingsFromStorage } from '../util/get-application-settings'; + +const MINIMIZE_SUPPRESS_MS = 500; + +export class TrayService { + private tray: Tray | null = null; + private contextMenu: Menu | null = null; + private hiddenToTray = false; + private isMaximized = false; + private isQuitting = false; + private suppressMinimizeToTray = false; + private suppressMinimizeTimeout: NodeJS.Timeout | null = null; + private trayActive = false; + private wasFullScreenBeforeTray = false; + private wasMaximizedBeforeTray = false; + private win: BrowserWindow | null = null; + + private readonly minimizeToTrayChangedHandler = (_event: Electron.IpcMainEvent, args: [boolean]) => { + this.setMinimizeToTrayEnabled(args[0] ?? false); + }; + + private readonly trayClickHandler = (): void => { + this.toggleWindow(); + }; + + constructor(private logService: LogService, + private iconPath: string) { + } + + init(win: BrowserWindow): void { + this.win = win; + this.isMaximized = win.isMaximized(); + + win.on('maximize', () => { + this.isMaximized = true; + }); + + win.on('unmaximize', () => { + if (this.suppressMinimizeToTray) { + return; + } + + // On Windows, unmaximize can fire immediately before minimize when hiding a + // maximized window. Defer clearing so minimize-to-tray keeps the state. + setImmediate(() => { + const currentWin = this.getWindow(); + if (!currentWin || currentWin.isMinimized()) { + return; + } + + this.isMaximized = false; + }); + }); + + win.on('minimize', () => { + void this.handleMinimize(); + }); + + win.on('close', () => { + if (this.isQuitting) { + this.destroy(); + } + }); + + ipcMain.on(IpcEvents.app.minimizeToTrayChanged, this.minimizeToTrayChangedHandler); + } + + setMinimizeToTrayEnabled(enabled: boolean): void { + if (enabled) { + this.ensureTray(); + this.logService.misc('[TrayService] Tray enabled'); + return; + } + + if (this.hiddenToTray) { + this.revealWindow(); + } + + this.destroyTrayIcon(); + this.logService.misc('[TrayService] Tray disabled'); + } + + async initTrayIfEnabled(): Promise { + const { minimizeToTray = false } = await getApplicationSettingsFromStorage(); + if (!minimizeToTray) { + return; + } + + this.ensureTray(); + } + + markQuitting(): void { + this.isQuitting = true; + } + + showWindow(): void { + this.revealWindow(); + } + + startInTray(): void { + this.ensureTray(); + this.hideToTray(); + } + + private getWindow(): BrowserWindow | null { + const win = this.win; + if (!win || win.isDestroyed()) { + return null; + } + + return win; + } + + private notifyMinimizeToTrayDisabledOnLinux(): void { + const win = this.getWindow(); + if (!win) { + return; + } + + win.webContents.send(IpcEvents.app.minimizeToTrayDisabledOnLinux); + } + + private runWithMinimizeSuppressed(action: () => void): void { + this.suppressMinimizeToTray = true; + if (this.suppressMinimizeTimeout) { + clearTimeout(this.suppressMinimizeTimeout); + } + + action(); + + this.suppressMinimizeTimeout = setTimeout(() => { + this.suppressMinimizeToTray = false; + this.suppressMinimizeTimeout = null; + }, MINIMIZE_SUPPRESS_MS); + } + + private captureWindowStateBeforeTray(win: BrowserWindow): void { + this.wasFullScreenBeforeTray = win.isFullScreen(); + this.wasMaximizedBeforeTray = this.isMaximized || win.isMaximized(); + } + + private hideToTray(): void { + const win = this.getWindow(); + if (!win) { + return; + } + + this.captureWindowStateBeforeTray(win); + this.hiddenToTray = true; + this.runWithMinimizeSuppressed(() => { + if (process.platform === 'linux') { + win.setSkipTaskbar(true); + } + + if (win.isMinimized()) { + win.restore(); + } + + win.hide(); + }); + this.logService.misc('[TrayService] Window hidden to tray', { + wasMaximized: this.wasMaximizedBeforeTray, + wasFullScreen: this.wasFullScreenBeforeTray, + }); + } + + private revealWindow(): void { + const win = this.getWindow(); + if (!win) { + return; + } + + const restoreMaximized = this.wasMaximizedBeforeTray; + const restoreFullScreen = this.wasFullScreenBeforeTray; + + // Defer until after tray click / context menu handling so focus is not stolen. + setImmediate(() => { + const currentWin = this.getWindow(); + if (!currentWin) { + return; + } + + this.hiddenToTray = false; + this.runWithMinimizeSuppressed(() => { + if (process.platform === 'linux') { + currentWin.setSkipTaskbar(false); + } + + currentWin.show(); + + if (currentWin.isMinimized()) { + currentWin.restore(); + } + + if (restoreFullScreen) { + currentWin.setFullScreen(true); + } else if (restoreMaximized) { + currentWin.maximize(); + } + + this.isMaximized = restoreMaximized; + currentWin.focus(); + }); + this.logService.misc('[TrayService] Window restored from tray', { + restoreMaximized, + restoreFullScreen, + }); + }); + } + + private toggleWindow(): void { + if (!this.trayActive) { + return; + } + + if (this.hiddenToTray) { + this.revealWindow(); + return; + } + + const win = this.getWindow(); + if (!win) { + return; + } + + setImmediate(() => { + if (this.hiddenToTray || !win.isVisible() || win.isMinimized()) { + this.revealWindow(); + return; + } + + this.hideToTray(); + }); + } + + private async handleMinimize(): Promise { + if (this.suppressMinimizeToTray || this.hiddenToTray) { + return; + } + + const win = this.getWindow(); + if (!win) { + return; + } + + const { minimizeToTray = false } = await getApplicationSettingsFromStorage(); + if (!minimizeToTray || this.suppressMinimizeToTray || this.hiddenToTray) { + return; + } + + this.ensureTray(); + this.hideToTray(); + } + + private buildTrayIcon(): Electron.NativeImage { + const icon = nativeImage.createFromPath(this.iconPath); + const trayIcon = process.platform === 'darwin' + ? icon.resize({ width: 16, height: 16 }) + : icon.resize({ width: 22, height: 22 }); + + if (process.platform === 'darwin') { + trayIcon.setTemplateImage(true); + } + + return trayIcon; + } + + private buildContextMenu(): Menu { + return Menu.buildFromTemplate([ + { + label: 'Show UHK Agent', + click: () => this.revealWindow(), + }, + { type: 'separator' }, + { + label: 'Quit', + click: () => { + this.isQuitting = true; + app.quit(); + }, + }, + ]); + } + + private createTrayInstance(): void { + this.tray = new Tray(this.buildTrayIcon()); + this.logService.misc('[TrayService] Tray instance created'); + } + + private activateTray(): void { + if (!this.tray || this.tray.isDestroyed()) { + return; + } + + this.tray.setImage(this.buildTrayIcon()); + this.tray.setToolTip('UHK Agent'); + this.contextMenu = this.buildContextMenu(); + + // setContextMenu shows the menu on right-click on Linux/Windows and on left-click on macOS. + // popUpContextMenu is not supported on Linux, so setContextMenu must be used there. + this.tray.setContextMenu(this.contextMenu); + + if (process.platform !== 'darwin') { + this.tray.removeAllListeners('click'); + this.tray.on('click', this.trayClickHandler); + } + + this.trayActive = true; + this.logService.misc('[TrayService] Tray icon activated'); + } + + private ensureTray(): void { + if (this.tray && !this.tray.isDestroyed()) { + if (!this.trayActive) { + this.activateTray(); + } + + return; + } + + this.createTrayInstance(); + this.activateTray(); + } + + private destroy(): void { + ipcMain.removeListener(IpcEvents.app.minimizeToTrayChanged, this.minimizeToTrayChangedHandler); + this.destroyTrayInstance(); + } + + private destroyTrayIcon(): void { + const hadActiveTray = this.trayActive; + + this.destroyTrayInstance(); + + // Linux uses Chromium's StatusNotifierItem integration, which does not reliably + // unregister tray icons when Tray.destroy() is called. The panel entry can remain + // visible (sometimes as a stale or broken icon) until Agent exits. Do not try to + // hide the icon with createEmpty() or a blank image — that produces a worse placeholder. + // See https://github.com/electron/electron/issues/49517 + if (process.platform === 'linux' && hadActiveTray) { + this.notifyMinimizeToTrayDisabledOnLinux(); + } + } + + private destroyTrayInstance(): void { + if (!this.tray || this.tray.isDestroyed()) { + this.tray = null; + this.contextMenu = null; + this.trayActive = false; + return; + } + + this.tray.removeAllListeners(); + this.tray.setContextMenu(null); + this.tray.destroy(); + this.tray = null; + this.contextMenu = null; + this.trayActive = false; + this.logService.misc('[TrayService] Tray icon destroyed'); + } +} diff --git a/packages/uhk-agent/src/util/command-line.ts b/packages/uhk-agent/src/util/command-line.ts index 6ac838b95f9..4a8ef8944c5 100644 --- a/packages/uhk-agent/src/util/command-line.ts +++ b/packages/uhk-agent/src/util/command-line.ts @@ -23,6 +23,7 @@ const optionDefinitions: commandLineArgs.OptionDefinition[] = [ { name: 'serial-number', type: String }, { name: 'simulate-invalid-codesign-signature', type: Boolean }, { name: 'spe', type: Boolean }, // simulate privilege escalation error + { name: 'start-minimized-to-tray', type: Boolean }, { name: 'usb-interface', type: Number }, { name: 'usb-non-blocking', type: Boolean }, { name: 'vid', type: Number }, @@ -131,6 +132,11 @@ const sections: commandLineUsage.Section[] = [ description: 'Simulate privilege escalation error', type: Boolean }, + { + name: 'start-minimized-to-tray', + description: 'Start the Agent window hidden in the system tray', + type: Boolean + }, { name: 'usb-interface', description: 'Use the specified USB interface id. If you set it you have to set the vid and pid too.', diff --git a/packages/uhk-agent/src/util/get-application-settings.ts b/packages/uhk-agent/src/util/get-application-settings.ts new file mode 100644 index 00000000000..1bca1cf00e9 --- /dev/null +++ b/packages/uhk-agent/src/util/get-application-settings.ts @@ -0,0 +1,17 @@ +import settings from 'electron-settings'; + +import { ApplicationSettings } from 'uhk-common'; + +const APPLICATION_SETTINGS_KEY = 'application-settings'; + +export async function getApplicationSettingsFromStorage(): Promise { + const value = await settings.get(APPLICATION_SETTINGS_KEY); + if (!value) { + return { + checkForUpdateOnStartUp: true, + everAttemptedSavingToKeyboard: false, + }; + } + + return JSON.parse(value as string); +} diff --git a/packages/uhk-agent/src/util/index.ts b/packages/uhk-agent/src/util/index.ts index 3774098410d..52ea4ffef16 100644 --- a/packages/uhk-agent/src/util/index.ts +++ b/packages/uhk-agent/src/util/index.ts @@ -5,6 +5,7 @@ export * from './copy-smart-macro-doc-to-webserver'; export * from './copy-smart-macro-loading-html'; export * from './create-png'; export * from './delete-user-config-history'; +export * from './get-application-settings'; export * from './get-default-firmware-path'; export * from './get-smart-macro-doc-root-path'; export * from './get-udev-file-content-async'; diff --git a/packages/uhk-common/src/models/application-settings.ts b/packages/uhk-common/src/models/application-settings.ts index 9f91a359c0d..5ed6a3894f2 100644 --- a/packages/uhk-common/src/models/application-settings.ts +++ b/packages/uhk-common/src/models/application-settings.ts @@ -26,6 +26,10 @@ export interface ApplicationSettings { * If extra module is connected then ignore this setting. */ keyboardHalvesAlwaysJoined?: boolean; + /** + * If true, minimizing the Agent window hides it to the system tray instead of the taskbar. + */ + minimizeToTray?: boolean; /** * Smart Macro panel width in percent; */ diff --git a/packages/uhk-common/src/models/command-line-args.ts b/packages/uhk-common/src/models/command-line-args.ts index 9251c05c890..0fd838040f3 100644 --- a/packages/uhk-common/src/models/command-line-args.ts +++ b/packages/uhk-common/src/models/command-line-args.ts @@ -92,6 +92,10 @@ export interface CommandLineArgs extends DeviceIdentifier { * simulate privilege escalation error */ spe?: boolean; + /** + * Start the Agent window hidden in the system tray. + */ + 'start-minimized-to-tray'?: boolean; /** * Use USB non-blocking communication */ diff --git a/packages/uhk-common/src/util/ipcEvents.ts b/packages/uhk-common/src/util/ipcEvents.ts index 0c8b05b524e..2ba4e23e9b0 100644 --- a/packages/uhk-common/src/util/ipcEvents.ts +++ b/packages/uhk-common/src/util/ipcEvents.ts @@ -6,6 +6,8 @@ export class App { public static readonly openConfigFolder = 'open-config-folder'; public static readonly openUrl = 'open-url'; public static readonly getConfig = 'app-get-config'; + public static readonly minimizeToTrayChanged = 'app-minimize-to-tray-changed'; + public static readonly minimizeToTrayDisabledOnLinux = 'app-minimize-to-tray-disabled-on-linux'; public static readonly setConfig = 'app-set-config'; } diff --git a/packages/uhk-web/src/app/components/agent/settings/settings.component.html b/packages/uhk-web/src/app/components/agent/settings/settings.component.html index 8563b1c6338..3c92992ccac 100644 --- a/packages/uhk-web/src/app/components/agent/settings/settings.component.html +++ b/packages/uhk-web/src/app/components/agent/settings/settings.component.html @@ -10,6 +10,22 @@

+
+
+ + +
+
+
@@ -72,3 +88,7 @@

The Follow operating system theme option may not be supported on all Linux distributions.

+ + +

On Linux, disabling this option stops minimize-to-tray behaviour immediately, but the tray icon may remain visible until Agent is restarted.

+
diff --git a/packages/uhk-web/src/app/components/agent/settings/settings.component.ts b/packages/uhk-web/src/app/components/agent/settings/settings.component.ts index 7087de85d47..b3ea24a37a1 100644 --- a/packages/uhk-web/src/app/components/agent/settings/settings.component.ts +++ b/packages/uhk-web/src/app/components/agent/settings/settings.component.ts @@ -12,16 +12,18 @@ import { getAnimationEnabled, getAppTheme, getIsAdvancedSettingsMenuVisible, + getMinimizeToTray, getOperatingSystem, getSupportedThemes, - keyboardHalvesAlwaysJoined + keyboardHalvesAlwaysJoined, + runningInElectron } from '../../../store'; import { State as UpdateSettingsState } from '../../../store/reducers/auto-update-settings'; import { CheckForUpdateNowAction, ToggleCheckForUpdateOnStartupAction } from '../../../store/actions/auto-update-settings'; -import { OpenConfigFolderAction, SetAppThemeAction, ToggleAnimationEnabledAction, ToggleKeyboardHalvesAlwaysJoinedAction } from '../../../store/actions/app'; +import { OpenConfigFolderAction, SetAppThemeAction, ToggleAnimationEnabledAction, ToggleKeyboardHalvesAlwaysJoinedAction, ToggleMinimizeToTrayAction } from '../../../store/actions/app'; import { ToggleAlwaysEnableAdvancedModeAction } from '../../../store/actions/advance-settings.action'; import { OperatingSystem } from '../../../models/operating-system'; @@ -37,6 +39,8 @@ import { OperatingSystem } from '../../../models/operating-system'; export class SettingsComponent { updateSettingsState$: Observable; animationEnabled$: Observable; + minimizeToTray$: Observable; + runningInElectron$: Observable; appTheme$: Observable; themes$: Observable; isLinux$: Observable; @@ -48,6 +52,8 @@ export class SettingsComponent { constructor(private store: Store) { this.updateSettingsState$ = store.select(appUpdateSettingsState); this.animationEnabled$ = store.select(getAnimationEnabled); + this.minimizeToTray$ = store.select(getMinimizeToTray); + this.runningInElectron$ = store.select(runningInElectron); this.appTheme$ = store.select(getAppTheme); this.themes$ = store.select(getSupportedThemes); this.isLinux$ = store.select(getOperatingSystem).pipe(map(os => os === OperatingSystem.Linux)); @@ -80,6 +86,10 @@ export class SettingsComponent { this.store.dispatch(new ToggleKeyboardHalvesAlwaysJoinedAction(enabled)); } + toggleMinimizeToTray(enabled: boolean): void { + this.store.dispatch(new ToggleMinimizeToTrayAction(enabled)); + } + toggleAlwaysEnableAdvancedMode(enabled: boolean): void { this.store.dispatch(new ToggleAlwaysEnableAdvancedModeAction(enabled)); } diff --git a/packages/uhk-web/src/app/services/app-renderer.service.ts b/packages/uhk-web/src/app/services/app-renderer.service.ts index abe1235b226..9573965bbb5 100644 --- a/packages/uhk-web/src/app/services/app-renderer.service.ts +++ b/packages/uhk-web/src/app/services/app-renderer.service.ts @@ -1,9 +1,9 @@ import { Injectable, NgZone } from '@angular/core'; import { Action, Store } from '@ngrx/store'; -import { AppStartInfo, IpcEvents, LogService } from 'uhk-common'; +import { AppStartInfo, IpcEvents, LogService, NotificationType } from 'uhk-common'; import { AppState } from '../store'; -import { ElectronMainLogReceivedAction, ProcessAppStartInfoAction } from '../store/actions/app'; +import { ElectronMainLogReceivedAction, ProcessAppStartInfoAction, ShowNotificationAction } from '../store/actions/app'; import { IpcCommonRenderer } from './ipc-common-renderer'; @Injectable() @@ -36,11 +36,23 @@ export class AppRendererService { this.ipcRenderer.send(IpcEvents.app.openUrl, url); } + setMinimizeToTray(enabled: boolean): void { + this.logService.misc(`[AppRendererService] set minimize to tray: ${enabled}`); + this.ipcRenderer.send(IpcEvents.app.minimizeToTrayChanged, enabled); + } + private registerEvents() { this.ipcRenderer.on(IpcEvents.app.getAppStartInfoReply, (event: string, arg: AppStartInfo) => { this.dispatchStoreAction(new ProcessAppStartInfoAction(arg)); }); + this.ipcRenderer.on(IpcEvents.app.minimizeToTrayDisabledOnLinux, () => { + this.dispatchStoreAction(new ShowNotificationAction({ + type: NotificationType.Info, + message: 'The tray icon will disappear after you restart Agent.' + })); + }); + this.ipcRenderer.on('__ELECTRON_LOG_IPC__', (event: string, { level, data }) => { const message = []; diff --git a/packages/uhk-web/src/app/store/actions/app.ts b/packages/uhk-web/src/app/store/actions/app.ts index 555cf4eb3dd..8bb9d45620a 100644 --- a/packages/uhk-web/src/app/store/actions/app.ts +++ b/packages/uhk-web/src/app/store/actions/app.ts @@ -29,6 +29,7 @@ export enum ActionTypes { SetupPermissionError = '[app] Setup permission error', ToggleAnimationEnabled = '[app] Toggle animation enabled', ToggleKeyboardHalvesAlwaysJoined = '[app] Toggle keyboard halves always joined', + ToggleMinimizeToTray = '[app] Toggle minimize to tray', SetAppTheme = '[app] Set application theme', LoadAppStartInfo = '[app] Load app start info', StartKeypressCapturing = '[app] Start keypress capturing', @@ -173,6 +174,13 @@ export class ToggleKeyboardHalvesAlwaysJoinedAction implements Action { } } +export class ToggleMinimizeToTrayAction implements Action { + type = ActionTypes.ToggleMinimizeToTray; + + constructor(public payload: boolean) { + } +} + export class SetAppThemeAction implements Action { type = ActionTypes.SetAppTheme; @@ -235,6 +243,7 @@ export type Actions | SetupPermissionErrorAction | ToggleAnimationEnabledAction | ToggleKeyboardHalvesAlwaysJoinedAction + | ToggleMinimizeToTrayAction | SetAppThemeAction | LoadAppStartInfoAction | StartKeypressCapturingAction diff --git a/packages/uhk-web/src/app/store/effects/app.ts b/packages/uhk-web/src/app/store/effects/app.ts index 0aa3b5cdf50..228556e4e16 100644 --- a/packages/uhk-web/src/app/store/effects/app.ts +++ b/packages/uhk-web/src/app/store/effects/app.ts @@ -26,6 +26,7 @@ import { SaveApplicationSettingsSuccessAction, SetAppThemeAction, ShowNotificationAction, + ToggleMinimizeToTrayAction, UndoLastAction } from '../actions/app'; import { ActionTypes as UpdateActionTypes } from '../actions/auto-update-settings'; @@ -60,6 +61,7 @@ export class ApplicationEffects { everAttemptedSavingToKeyboard: false, animationEnabled: true, keyboardHalvesAlwaysJoined: false, + minimizeToTray: false, ...appSettings }; @@ -146,6 +148,7 @@ export class ApplicationEffects { ActionTypes.SetAppTheme, ActionTypes.ToggleAnimationEnabled, ActionTypes.ToggleKeyboardHalvesAlwaysJoined, + ActionTypes.ToggleMinimizeToTray, AdvanceSettingsActionTypes.toggleAlwaysEnableAdvancedMode, UpdateActionTypes.ToggleCheckForUpdateOnStartup, DeviceActionTypes.SaveConfiguration, @@ -176,6 +179,20 @@ export class ApplicationEffects { { dispatch: false } ); + minimizeToTrayChanged$ = createEffect(() => this.actions$ + .pipe( + ofType(ActionTypes.ToggleMinimizeToTray), + map(action => action.payload), + withLatestFrom(this.store.select(runningInElectron)), + tap(([enabled, inElectron]) => { + if (inElectron) { + this.appRendererService.setMinimizeToTray(enabled); + } + }) + ), + { dispatch: false } + ); + navigateTo$ = createEffect(() => this.actions$ .pipe( ofType(ActionTypes.NavigateTo), diff --git a/packages/uhk-web/src/app/store/index.ts b/packages/uhk-web/src/app/store/index.ts index 359e1157c14..e3629d7ef92 100644 --- a/packages/uhk-web/src/app/store/index.ts +++ b/packages/uhk-web/src/app/store/index.ts @@ -202,6 +202,7 @@ export const firmwareUpgradeAllowed = createSelector(runningOnNotSupportedWindow export const getEverAttemptedSavingToKeyboard = createSelector(appState, fromApp.getEverAttemptedSavingToKeyboard); export const getUdevFileContent = createSelector(appState, fromApp.getUdevFileContent); export const getAnimationEnabled = createSelector(appState, fromApp.getAnimationEnabled); +export const getMinimizeToTray = createSelector(appState, fromApp.getMinimizeToTray); export const getAppTheme = createSelector(appState, fromApp.getAppTheme); export const getUhkThemeColors = createSelector(getAppTheme, (theme): UhkThemeColors => { return defaultUhkThemeColors(theme); @@ -849,6 +850,7 @@ export const getApplicationSettings = createSelector( appTheme: app.appTheme, backlightingColorPalette, keyboardHalvesAlwaysJoined, + minimizeToTray: app.minimizeToTray, alwaysEnableAdvancedMode, smartMacroPanelWidth }; diff --git a/packages/uhk-web/src/app/store/reducers/app.reducer.ts b/packages/uhk-web/src/app/store/reducers/app.reducer.ts index 2c197e58b24..c735a4f9993 100644 --- a/packages/uhk-web/src/app/store/reducers/app.reducer.ts +++ b/packages/uhk-web/src/app/store/reducers/app.reducer.ts @@ -20,6 +20,7 @@ const DEFAULT_ERROR_PANEL_HEIGHT = 10; export interface State { appTheme: AppTheme; animationEnabled: boolean; + minimizeToTray: boolean; errorPanelHeight: number; isRunningOnWayland: boolean; started: boolean; @@ -43,6 +44,7 @@ export interface State { export const initialState: State = { appTheme: AppTheme.System, animationEnabled: true, + minimizeToTray: false, errorPanelHeight: DEFAULT_ERROR_PANEL_HEIGHT, isRunningOnWayland: false, started: false, @@ -207,7 +209,8 @@ export function reducer( errorPanelHeight: settings.errorPanelHeight || DEFAULT_ERROR_PANEL_HEIGHT, everAttemptedSavingToKeyboard: settings.everAttemptedSavingToKeyboard, animationEnabled: settings.animationEnabled, - appTheme: settings.appTheme || AppTheme.System + appTheme: settings.appTheme || AppTheme.System, + minimizeToTray: settings.minimizeToTray ?? false, }; } @@ -223,6 +226,12 @@ export function reducer( animationEnabled: (action as App.ToggleAnimationEnabledAction).payload }; + case App.ActionTypes.ToggleMinimizeToTray: + return { + ...state, + minimizeToTray: (action as App.ToggleMinimizeToTrayAction).payload + }; + case App.ActionTypes.SetAppTheme: return { ...state, @@ -266,6 +275,7 @@ export const keypressCapturing = (state: State): boolean => state.keypressCaptur export const getEverAttemptedSavingToKeyboard = (state: State): boolean => state.everAttemptedSavingToKeyboard; export const getUdevFileContent = (state: State): string => state.udevFileContent; export const getAnimationEnabled = (state: State): boolean => state.animationEnabled; +export const getMinimizeToTray = (state: State): boolean => state.minimizeToTray; export const getAppTheme = (state: State): AppTheme => state.appTheme; export const getHardwareConfiguration = (state: State): HardwareConfiguration => state.hardwareConfig; export const getPlatform = (state: State): string => state.platform;