From 9cb0e6b4bf3733b8f50aa7af51487a9fad054e12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A1szl=C3=B3=20Monda?= Date: Sun, 21 Jun 2026 10:22:48 +0200 Subject: [PATCH 1/4] Add progress bars for configuration load and save. Report byte-accurate progress over IPC during keyboard config transfer, show a grayscale bar on the startup loading screen, and fill the Save to keyboard button while saving. Closes #2355 Co-authored-by: Cursor --- .../uhk-agent/src/services/device.service.ts | 39 +++++++++- packages/uhk-common/src/util/ipcEvents.ts | 2 + packages/uhk-usb/src/uhk-operations.ts | 73 ++++++++++++++++--- .../progress-button.component.html | 17 +++-- .../progress-button.component.scss | 20 +++++ .../progress-button.component.ts | 3 - .../uhk-message/uhk-message.component.html | 33 ++++++--- .../uhk-message/uhk-message.component.scss | 33 +++++++++ .../uhk-message/uhk-message.component.ts | 2 + .../pages/loading-page/loading-device.page.ts | 12 ++- .../app/services/device-renderer.service.ts | 10 +++ packages/uhk-web/src/app/store/actions/app.ts | 9 +++ .../uhk-web/src/app/store/actions/device.ts | 9 +++ packages/uhk-web/src/app/store/index.ts | 1 + .../src/app/store/reducers/app.reducer.ts | 22 +++++- .../uhk-web/src/app/store/reducers/device.ts | 23 +++++- .../store/reducers/progress-button-state.ts | 4 +- packages/uhk-web/src/styles/themes/_dark.scss | 3 + .../uhk-web/src/styles/themes/_light.scss | 3 + 19 files changed, 283 insertions(+), 35 deletions(-) diff --git a/packages/uhk-agent/src/services/device.service.ts b/packages/uhk-agent/src/services/device.service.ts index ad9f2f647dd..d8c1be42d13 100644 --- a/packages/uhk-agent/src/services/device.service.ts +++ b/packages/uhk-agent/src/services/device.service.ts @@ -111,6 +111,7 @@ export class DeviceService { private leftHalfZephyrLogService: ZephyrLogService; private queueManager = new QueueManager(); private wasCalledSaveUserConfiguration = false; + private loadConfigurationsInProgress = false; private isI2cDebuggingEnabled = false; private i2cWatchdogRecoveryCounter = -1; private savedState: DeviceConnectionState; @@ -355,16 +356,34 @@ export class DeviceService { * @returns {Promise} */ public async loadConfigurations(event: Electron.IpcMainEvent, args): Promise { + if (this.loadConfigurationsInProgress) { + this.logService.misc('[DeviceService] load user configuration already in progress, skipping'); + return; + } + + this.loadConfigurationsInProgress = true; this.logService.misc('[DeviceService] load user configuration'); let response: ConfigurationReply; + let progress = 0; try { await this.stopPollUhkDevice(); + const sendProgress = (percent: number) => { + progress = Math.max(progress, Math.min(100, percent)); + event.sender.send(IpcEvents.device.loadConfigurationProgress, progress); + }; + + sendProgress(0); await this.operations.waitUntilKeyboardBusy(); - const result = await this.operations.loadConfigurations(); + sendProgress(3); + const result = await this.operations.loadConfigurations((percent) => { + sendProgress(3 + Math.round(percent * 0.82)); + }); + sendProgress(88); const modules: HardwareModules = await this.getHardwareModules(false); + sendProgress(95); const hardwareConfig = getHardwareConfigFromDeviceResponse(result.hardwareConfiguration); const uniqueId = hardwareConfig.uniqueId; @@ -385,6 +404,7 @@ export class DeviceService { info: BackupUserConfigurationInfo.Unknown } }; + sendProgress(100); } catch (error) { response = { success: false, @@ -393,6 +413,7 @@ export class DeviceService { } finally { await this.device.close(); this.startPollUhkDevice(); + this.loadConfigurationsInProgress = false; } event.sender.send(IpcEvents.device.loadConfigurationReply, JSON.stringify(response)); @@ -1298,18 +1319,32 @@ export class DeviceService { try { await this.stopPollUhkDevice(); + + let progress = 0; + const sendProgress = (percent: number) => { + progress = Math.max(progress, Math.min(100, percent)); + event.sender.send(IpcEvents.device.saveUserConfigurationProgress, progress); + }; + + sendProgress(0); await backupUserConfiguration(data); + sendProgress(1); this.logService.config('[DeviceService] User configuration will be saved', data.configuration); const buffer = mapObjectToUserConfigBinaryBuffer(data.configuration); - await this.operations.saveUserConfiguration(buffer); + await this.operations.saveUserConfiguration(buffer, (percent) => { + sendProgress(1 + Math.round(percent * 0.94)); + }); + this._checkStatusBuffer = true; if (data.saveInHistory) { + sendProgress(97); await saveUserConfigHistoryAsync(buffer, data.deviceId, data.uniqueId); await this.loadUserConfigFromHistory(event); } + sendProgress(100); response.success = true; } catch (error) { this.logService.error('[DeviceService] Transferring error', error); diff --git a/packages/uhk-common/src/util/ipcEvents.ts b/packages/uhk-common/src/util/ipcEvents.ts index 61916764982..6144399d7e8 100644 --- a/packages/uhk-common/src/util/ipcEvents.ts +++ b/packages/uhk-common/src/util/ipcEvents.ts @@ -45,8 +45,10 @@ export class Device { public static readonly setPrivilegeOnLinuxReply = 'set-privilege-on-linux-reply'; public static readonly deviceConnectionStateChanged = 'device-connection-state-changed'; public static readonly saveUserConfiguration = 'device-save-user-configuration'; + public static readonly saveUserConfigurationProgress = 'device-save-user-configuration-progress'; public static readonly saveUserConfigurationReply = 'device-save-user-configuration-reply'; public static readonly loadConfigurations = 'device-load-configuration'; + public static readonly loadConfigurationProgress = 'device-load-configuration-progress'; public static readonly loadConfigurationReply = 'device-load-configuration-reply'; public static readonly updateFirmware = 'device-update-firmware'; public static readonly updateFirmwareJson = 'device-update-firmware-json'; diff --git a/packages/uhk-usb/src/uhk-operations.ts b/packages/uhk-usb/src/uhk-operations.ts index ff41a8f074b..815726f7358 100644 --- a/packages/uhk-usb/src/uhk-operations.ts +++ b/packages/uhk-usb/src/uhk-operations.ts @@ -308,11 +308,36 @@ export class UhkOperations { * Return with the actual UserConfiguration from UHK Device * @returns {Promise} */ - public async loadConfigurations(): Promise { + public async loadConfigurations(onProgress?: (percent: number) => void): Promise { try { await this.waitUntilKeyboardBusy(); - const userConfiguration = await this.loadConfiguration(ConfigBufferId.validatedUserConfig); - const hardwareConfiguration = await this.loadConfiguration(ConfigBufferId.hardwareConfig); + onProgress?.(0); + + const configSizes = await this.getConfigSizesFromKeyboard(); + let userConfigSize = configSizes.userConfig; + const hardwareConfigSize = configSizes.hardwareConfig; + + const reportTransferProgress = (userOffset: number, hardwareOffset: number) => { + const totalBytes = Math.max(userConfigSize + hardwareConfigSize, 1); + const transferredBytes = userOffset + hardwareOffset; + + onProgress?.(Math.round(Math.min(transferredBytes / totalBytes, 1) * 100)); + }; + + const userConfiguration = await this.loadConfiguration( + ConfigBufferId.validatedUserConfig, + (offset, configSize) => { + userConfigSize = configSize; + reportTransferProgress(offset, 0); + } + ); + const hardwareConfiguration = await this.loadConfiguration( + ConfigBufferId.hardwareConfig, + (offset) => { + reportTransferProgress(userConfigSize, offset); + } + ); + reportTransferProgress(userConfigSize, hardwareConfigSize); return { userConfiguration: JSON.stringify(convertBufferToIntArray(userConfiguration)), @@ -327,7 +352,10 @@ export class UhkOperations { * Return with the actual user / hardware fonfiguration from UHK Device * @returns {Promise} */ - public async loadConfiguration(configBufferId: ConfigBufferId): Promise { + public async loadConfiguration( + configBufferId: ConfigBufferId, + onProgress?: (offset: number, configSize: number) => void + ): Promise { const configBufferIdToName = ['HardwareConfig', 'StagingUserConfig', 'ValidatedUserConfig']; const configName = configBufferIdToName[configBufferId]; @@ -360,6 +388,8 @@ export class UhkOperations { configSize = originalConfigSize; } } + + onProgress?.(offset, configSize); } return configBuffer; @@ -393,8 +423,15 @@ export class UhkOperations { }; } - public async saveUserConfiguration(buffer: Buffer): Promise { + public async saveUserConfiguration(buffer: Buffer, onProgress?: (percent: number) => void): Promise { + let lastProgress = 0; + const reportProgress = (percent: number) => { + lastProgress = Math.max(lastProgress, Math.min(100, Math.round(percent))); + onProgress?.(lastProgress); + }; + try { + reportProgress(0); this.logService.usbOps('[DeviceOperation] USB[T]: Write user configuration to keyboard'); let shouldRecalculateLength = false; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -423,11 +460,20 @@ export class UhkOperations { const resultBuffer = new UhkBuffer(UHK_EEPROM_SIZE) userConfiguration.toBinary(resultBuffer) - await this.sendConfigToKeyboard(resultBuffer.getBufferContent(), true); + const configBuffer = resultBuffer.getBufferContent(); + + reportProgress(2); + await this.sendConfigToKeyboard(configBuffer, true, (bytesSent, totalBytes) => { + reportProgress(2 + bytesSent / totalBytes * 83); + }); + reportProgress(86); await this.applyConfiguration(); + reportProgress(90); this.logService.usbOps('[DeviceOperation] USB[T]: Write user configuration to EEPROM'); await this.writeConfigToEeprom(ConfigBufferId.validatedUserConfig); + reportProgress(94); await this.waitUntilKeyboardBusy(); + reportProgress(96); } catch (error) { this.logService.error('[DeviceOperation] Transferring error', error); throw error; @@ -916,14 +962,23 @@ export class UhkOperations { * @returns {Promise} * @private */ - private async sendConfigToKeyboard(buffer: Buffer, isUserConfiguration): Promise { + private async sendConfigToKeyboard( + buffer: Buffer, + isUserConfiguration, + onProgress?: (bytesSent: number, totalBytes: number) => void + ): Promise { const command = isUserConfiguration ? UsbCommand.WriteStagingUserConfig : UsbCommand.WriteHardwareConfig; const fragments = getTransferBuffers(command, buffer); - for (const fragment of fragments) { - await this.device.write(fragment); + for (let i = 0; i < fragments.length; i++) { + await this.device.write(fragments[i]); + const bytesSent = Math.min( + buffer.length, + Math.round((i + 1) / fragments.length * buffer.length) + ); + onProgress?.(bytesSent, buffer.length); } } diff --git a/packages/uhk-web/src/app/components/progress-button/progress-button.component.html b/packages/uhk-web/src/app/components/progress-button/progress-button.component.html index c79c62dd2e5..101189405d0 100644 --- a/packages/uhk-web/src/app/components/progress-button/progress-button.component.html +++ b/packages/uhk-web/src/app/components/progress-button/progress-button.component.html @@ -1,8 +1,15 @@ - diff --git a/packages/uhk-web/src/app/components/progress-button/progress-button.component.scss b/packages/uhk-web/src/app/components/progress-button/progress-button.component.scss index d45efbef779..3d95f0dd4d0 100644 --- a/packages/uhk-web/src/app/components/progress-button/progress-button.component.scss +++ b/packages/uhk-web/src/app/components/progress-button/progress-button.component.scss @@ -1,3 +1,23 @@ button { min-width: 150px; } + +.progress-button { + position: relative; + overflow: hidden; +} + +.progress-button__fill { + position: absolute; + top: 0; + left: 0; + bottom: 0; + background-color: var(--color-loading-progress-bar-fill); + opacity: 0.35; + transition: width 200ms ease-out; +} + +.progress-button__content { + position: relative; + z-index: 1; +} diff --git a/packages/uhk-web/src/app/components/progress-button/progress-button.component.ts b/packages/uhk-web/src/app/components/progress-button/progress-button.component.ts index 2ce34995ec1..403268aa863 100644 --- a/packages/uhk-web/src/app/components/progress-button/progress-button.component.ts +++ b/packages/uhk-web/src/app/components/progress-button/progress-button.component.ts @@ -1,6 +1,5 @@ import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; import { Action } from '@ngrx/store'; -import { faSpinner } from '@fortawesome/free-solid-svg-icons'; import { ProgressButtonState, initProgressButtonState } from '../../store/reducers/progress-button-state'; @@ -15,8 +14,6 @@ export class ProgressButtonComponent { @Input() state: ProgressButtonState = initProgressButtonState; @Output() clicked: EventEmitter = new EventEmitter(); - faSpinner = faSpinner; - onClicked() { this.clicked.emit(this.state.action); } diff --git a/packages/uhk-web/src/app/components/uhk-message/uhk-message.component.html b/packages/uhk-web/src/app/components/uhk-message/uhk-message.component.html index 3fe18cb5694..f652e6f28ec 100644 --- a/packages/uhk-web/src/app/components/uhk-message/uhk-message.component.html +++ b/packages/uhk-web/src/app/components/uhk-message/uhk-message.component.html @@ -1,12 +1,27 @@
- -
-

{{ header }}

-

{{ subtitle }}

-
{{ subtitle }}
-

{{ description }}

+
+
+ +
+

{{ header }}

+

{{ subtitle }}

+
{{ subtitle }}
+

{{ description }}

+
+
+
+
+
+
+
+
diff --git a/packages/uhk-web/src/app/components/uhk-message/uhk-message.component.scss b/packages/uhk-web/src/app/components/uhk-message/uhk-message.component.scss index b503f68b0d6..7a489fad40b 100644 --- a/packages/uhk-web/src/app/components/uhk-message/uhk-message.component.scss +++ b/packages/uhk-web/src/app/components/uhk-message/uhk-message.component.scss @@ -5,12 +5,45 @@ justify-content: center; } +.uhk-message-content { + display: flex; + flex-direction: column; + align-items: stretch; +} + +.uhk-message-main { + display: flex; + align-items: center; +} + .agent-logo { margin: 1.25em; height: 8em; width: 8em; } +.with-progress-bar .agent-logo { + margin-bottom: 1.5em; +} + +.loading-progress-bar { + width: 100%; + padding: 0 1.25em; +} + +.loading-progress-bar__track { + background-color: var(--color-loading-progress-bar-bg); + border-radius: 0.375rem; + height: 1rem; + overflow: hidden; +} + +.loading-progress-bar__fill { + background-color: var(--color-loading-progress-bar-fill); + height: 100%; + transition: width 200ms ease-out; +} + .message { display: flex; flex-direction: column; diff --git a/packages/uhk-web/src/app/components/uhk-message/uhk-message.component.ts b/packages/uhk-web/src/app/components/uhk-message/uhk-message.component.ts index 4106699df65..d0d73d24459 100644 --- a/packages/uhk-web/src/app/components/uhk-message/uhk-message.component.ts +++ b/packages/uhk-web/src/app/components/uhk-message/uhk-message.component.ts @@ -13,5 +13,7 @@ export class UhkMessageComponent { @Input() subtitle: string; @Input() rotateLogo = false; @Input() showLogo = false; + @Input() showProgressBar = false; + @Input() progressPercent = 0; @Input() smallText = false; } diff --git a/packages/uhk-web/src/app/pages/loading-page/loading-device.page.ts b/packages/uhk-web/src/app/pages/loading-page/loading-device.page.ts index f114fad7727..fb73b6cea00 100644 --- a/packages/uhk-web/src/app/pages/loading-page/loading-device.page.ts +++ b/packages/uhk-web/src/app/pages/loading-page/loading-device.page.ts @@ -1,4 +1,8 @@ import { Component } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { Observable } from 'rxjs'; + +import { AppState, getConfigurationLoadingProgress } from '../../store'; @Component({ selector: 'loading-device', @@ -7,11 +11,15 @@ import { Component } from '@angular/core'; + [rotateLogo]="true" + [showProgressBar]="true" + [progressPercent]="(progressPercent$ | async) ?? 0"> `, }) export class LoadingDevicePageComponent { + progressPercent$: Observable; - constructor() { + constructor(store: Store) { + this.progressPercent$ = store.select(getConfigurationLoadingProgress); } } diff --git a/packages/uhk-web/src/app/services/device-renderer.service.ts b/packages/uhk-web/src/app/services/device-renderer.service.ts index f205e6d0fff..aff91883474 100644 --- a/packages/uhk-web/src/app/services/device-renderer.service.ts +++ b/packages/uhk-web/src/app/services/device-renderer.service.ts @@ -54,6 +54,7 @@ import { RecoveryDeviceReplyAction, RecoveryModuleReplyAction, SaveConfigurationReplyAction, + SaveConfigurationProgressChangedAction, SetPrivilegeOnLinuxReplyAction, StatusBufferChangedAction, UpdateFirmwareJsonAction, @@ -65,6 +66,7 @@ import { I2cWatchdogCounterChangedAction, } from '../store/actions/advance-settings.action'; import { LoadConfigFromDeviceReplyAction, LoadUserConfigurationFromFileAction } from '../store/actions/user-config'; +import { ConfigurationLoadingProgressChangedAction } from '../store/actions/app'; import { DeleteUserConfigHistoryReplyAction, LoadUserConfigurationHistorySuccessAction, @@ -264,6 +266,10 @@ export class DeviceRendererService { this.dispachStoreAction(new SetPrivilegeOnLinuxReplyAction(response)); }); + this.ipcRenderer.on(IpcEvents.device.saveUserConfigurationProgress, (event: string, progress: number) => { + this.dispachStoreAction(new SaveConfigurationProgressChangedAction(progress)); + }); + this.ipcRenderer.on(IpcEvents.device.saveUserConfigurationReply, (event: string, response: IpcResponse) => { this.dispachStoreAction(new SaveConfigurationReplyAction(response)); }); @@ -272,6 +278,10 @@ export class DeviceRendererService { this.dispachStoreAction(new StatusBufferChangedAction(response)); }); + this.ipcRenderer.on(IpcEvents.device.loadConfigurationProgress, (event: string, progress: number) => { + this.dispachStoreAction(new ConfigurationLoadingProgressChangedAction(progress)); + }); + this.ipcRenderer.on(IpcEvents.device.loadConfigurationReply, (event: string, response: string) => { this.dispachStoreAction(new LoadConfigFromDeviceReplyAction(JSON.parse(response))); }); diff --git a/packages/uhk-web/src/app/store/actions/app.ts b/packages/uhk-web/src/app/store/actions/app.ts index 63553d9aaba..555cf4eb3dd 100644 --- a/packages/uhk-web/src/app/store/actions/app.ts +++ b/packages/uhk-web/src/app/store/actions/app.ts @@ -17,6 +17,7 @@ export enum ActionTypes { UndoLastSuccess = '[app] undo last action success', DismissUndoNotification = '[app] dismiss notification action', LoadHardwareConfigurationSuccess = '[app] load hardware configuration success', + ConfigurationLoadingProgressChanged = '[app] configuration loading progress changed', LoadApplicationSettings = '[app] Load application settings', LoadApplicationSettingsSuccess = '[app] Load application settings success', SaveApplicationSettingsSuccess = '[app] Save application settings success', @@ -103,6 +104,13 @@ export class LoadHardwareConfigurationSuccessAction implements Action { } } +export class ConfigurationLoadingProgressChangedAction implements Action { + type = ActionTypes.ConfigurationLoadingProgressChanged; + + constructor(public payload: number) { + } +} + export class LoadApplicationSettingsAction implements Action { type = ActionTypes.LoadApplicationSettings; } @@ -215,6 +223,7 @@ export type Actions | UndoLastSuccessAction | DismissUndoNotificationAction | LoadHardwareConfigurationSuccessAction + | ConfigurationLoadingProgressChangedAction | LoadApplicationSettingsAction | LoadApplicationSettingsSuccessAction | SaveApplicationSettingsSuccessAction diff --git a/packages/uhk-web/src/app/store/actions/device.ts b/packages/uhk-web/src/app/store/actions/device.ts index df0d8035ec8..f2d2f4f21ca 100644 --- a/packages/uhk-web/src/app/store/actions/device.ts +++ b/packages/uhk-web/src/app/store/actions/device.ts @@ -37,6 +37,7 @@ export enum ActionTypes { SetPrivilegeOnLinuxReply = '[device] set privilege on linux reply', ConnectionStateChanged = '[device] connection state changed', SaveConfiguration = '[device] save configuration', + SaveConfigurationProgressChanged = '[device] save configuration progress changed', SaveConfigurationReply = '[device] save configuration reply', SavingConfiguration = '[device] saving configuration', // TODO: Delete looks like not used ShowSaveToKeyboardButton = '[device] show save to keyboard button', @@ -161,6 +162,13 @@ export class SaveConfigurationAction implements Action { } } +export class SaveConfigurationProgressChangedAction implements Action { + type = ActionTypes.SaveConfigurationProgressChanged; + + constructor(public payload: number) { + } +} + export class SaveConfigurationReplyAction implements Action { type = ActionTypes.SaveConfigurationReply; @@ -357,6 +365,7 @@ export type Actions | ConnectionStateChangedAction | ShowSaveToKeyboardButtonAction | SaveConfigurationAction + | SaveConfigurationProgressChangedAction | SaveConfigurationReplyAction | SaveToKeyboardSuccessAction | SaveToKeyboardSuccessFailed diff --git a/packages/uhk-web/src/app/store/index.ts b/packages/uhk-web/src/app/store/index.ts index 445b53252c2..3487e9ef605 100644 --- a/packages/uhk-web/src/app/store/index.ts +++ b/packages/uhk-web/src/app/store/index.ts @@ -191,6 +191,7 @@ export const getPrevUserConfiguration = createSelector(appState, fromApp.getPrev export const runningInElectron = createSelector(appState, fromApp.runningInElectron); export const getKeyboardLayout = createSelector(appState, fromApp.getKeyboardLayout); export const deviceConfigurationLoaded = createSelector(appState, fromApp.deviceConfigurationLoaded); +export const getConfigurationLoadingProgress = createSelector(appState, fromApp.getConfigurationLoadingProgress); export const getOperatingSystem = createSelector(appState, fromSelectors.getOperatingSystem); export const keypressCapturing = createSelector(appState, fromApp.keypressCapturing); export const runningOnNotSupportedWindows = createSelector(appState, fromApp.runningOnNotSupportedWindows); 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 63cc63500c9..48c952a339d 100644 --- a/packages/uhk-web/src/app/store/reducers/app.reducer.ts +++ b/packages/uhk-web/src/app/store/reducers/app.reducer.ts @@ -29,6 +29,7 @@ export interface State { prevUserConfig?: UserConfiguration; runningInElectron: boolean; configLoading: boolean; + configurationLoadingProgress: number; hardwareConfig?: HardwareConfiguration; privilegeWhatWillThisDoClicked: boolean; permissionError?: unknown; @@ -49,6 +50,7 @@ export const initialState: State = { navigationCountAfterNotification: 0, runningInElectron: runInElectron(), configLoading: true, + configurationLoadingProgress: 0, privilegeWhatWillThisDoClicked: false, keypressCapturing: false, everAttemptedSavingToKeyboard: false, @@ -133,7 +135,21 @@ export function reducer( case UserConfigActionTypes.LoadUserConfig: { return { ...state, - configLoading: true + configLoading: true, + configurationLoadingProgress: 0 + }; + } + + case App.ActionTypes.ConfigurationLoadingProgressChanged: { + const progress = (action as App.ConfigurationLoadingProgressChangedAction).payload; + + if (!state.configLoading) { + return state; + } + + return { + ...state, + configurationLoadingProgress: Math.max(state.configurationLoadingProgress, progress) }; } @@ -152,7 +168,8 @@ export function reducer( return { ...state, - hardwareConfig: null + hardwareConfig: null, + configurationLoadingProgress: 0 }; } @@ -235,6 +252,7 @@ export const getKeyboardLayout = (state: State): KeyboardLayout => { return KeyboardLayout.ANSI; }; export const deviceConfigurationLoaded = (state: State) => !state.runningInElectron ? true : !!state.hardwareConfig; +export const getConfigurationLoadingProgress = (state: State): number => state.configurationLoadingProgress; export const runningOnNotSupportedWindows = (state: State): boolean => { if (!state.osVersion || state.platform !== 'win32') { diff --git a/packages/uhk-web/src/app/store/reducers/device.ts b/packages/uhk-web/src/app/store/reducers/device.ts index 817cb075f50..5b6c8e1a08b 100644 --- a/packages/uhk-web/src/app/store/reducers/device.ts +++ b/packages/uhk-web/src/app/store/reducers/device.ts @@ -226,7 +226,24 @@ export function reducer(state = initialState, action: Action): State { saveToKeyboard: { showButton: true, text: 'Saving', - showProgress: true + showProgress: true, + progressPercent: 0 + } + }; + } + + case Device.ActionTypes.SaveConfigurationProgressChanged: { + if (state.saveToKeyboard.text !== 'Saving') { + return state; + } + + const progress = (action as Device.SaveConfigurationProgressChangedAction).payload; + + return { + ...state, + saveToKeyboard: { + ...state.saveToKeyboard, + progressPercent: Math.max(state.saveToKeyboard.progressPercent ?? 0, progress) } }; } @@ -237,7 +254,9 @@ export function reducer(state = initialState, action: Action): State { saveToKeyboard: { showButton: true, text: 'Saved!', - action: null + action: null, + showProgress: true, + progressPercent: 100 }, restoringUserConfiguration: false }; diff --git a/packages/uhk-web/src/app/store/reducers/progress-button-state.ts b/packages/uhk-web/src/app/store/reducers/progress-button-state.ts index 9fd5d564d4e..4a55a0e2d87 100644 --- a/packages/uhk-web/src/app/store/reducers/progress-button-state.ts +++ b/packages/uhk-web/src/app/store/reducers/progress-button-state.ts @@ -5,13 +5,15 @@ export interface ProgressButtonState { showButton: boolean; text: string; showProgress?: boolean; + progressPercent?: number; action?: Action; } export const initProgressButtonState = { showButton: false, text: null, - showProgress: false + showProgress: false, + progressPercent: 0 }; export function getSaveToKeyboardButtonState(): ProgressButtonState { diff --git a/packages/uhk-web/src/styles/themes/_dark.scss b/packages/uhk-web/src/styles/themes/_dark.scss index eb1a4ea822f..543233eb44f 100644 --- a/packages/uhk-web/src/styles/themes/_dark.scss +++ b/packages/uhk-web/src/styles/themes/_dark.scss @@ -118,6 +118,9 @@ $input-focus-border-color: tint-color($component-active-bg, 25%) !default; --color-progress-bar-progress-rgb-colors: #{tint-color($success, 10%)}; --color-progress-bar-text: #{$color-text}; + --color-loading-progress-bar-bg: #555; + --color-loading-progress-bar-fill: #fff; + --color-macro-bg: #333; --color-macro-border: #444; diff --git a/packages/uhk-web/src/styles/themes/_light.scss b/packages/uhk-web/src/styles/themes/_light.scss index cde125b9b1a..287671d09a9 100644 --- a/packages/uhk-web/src/styles/themes/_light.scss +++ b/packages/uhk-web/src/styles/themes/_light.scss @@ -91,6 +91,9 @@ $input-focus-border-color: lighten($component-active-bg, 25%) !default; --color-progress-bar-progress-rgb-colors: #{tint-color($success, 20%)}; --color-progress-bar-text: #{$color-text}; + --color-loading-progress-bar-bg: #e0e0e0; + --color-loading-progress-bar-fill: #000; + --color-macro-bg: #{$color-bg-light}; --color-macro-border: #ebebeb; From a501677fe062463957edcd10faf8b324f83aba7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A1szl=C3=B3=20Monda?= Date: Sun, 21 Jun 2026 18:14:44 +0200 Subject: [PATCH 2/4] Address PR feedback for loading progress bar. Use native progress element with WebKit styling fixes, restore smooth fill animation, and remove redundant loadConfigurationsInProgress guard. Co-authored-by: Cursor --- .../uhk-agent/src/services/device.service.ts | 8 ----- .../uhk-message/uhk-message.component.html | 15 +++----- .../uhk-message/uhk-message.component.scss | 36 ++++++++++++++----- 3 files changed, 32 insertions(+), 27 deletions(-) diff --git a/packages/uhk-agent/src/services/device.service.ts b/packages/uhk-agent/src/services/device.service.ts index d8c1be42d13..64f83503032 100644 --- a/packages/uhk-agent/src/services/device.service.ts +++ b/packages/uhk-agent/src/services/device.service.ts @@ -111,7 +111,6 @@ export class DeviceService { private leftHalfZephyrLogService: ZephyrLogService; private queueManager = new QueueManager(); private wasCalledSaveUserConfiguration = false; - private loadConfigurationsInProgress = false; private isI2cDebuggingEnabled = false; private i2cWatchdogRecoveryCounter = -1; private savedState: DeviceConnectionState; @@ -356,12 +355,6 @@ export class DeviceService { * @returns {Promise} */ public async loadConfigurations(event: Electron.IpcMainEvent, args): Promise { - if (this.loadConfigurationsInProgress) { - this.logService.misc('[DeviceService] load user configuration already in progress, skipping'); - return; - } - - this.loadConfigurationsInProgress = true; this.logService.misc('[DeviceService] load user configuration'); let response: ConfigurationReply; @@ -413,7 +406,6 @@ export class DeviceService { } finally { await this.device.close(); this.startPollUhkDevice(); - this.loadConfigurationsInProgress = false; } event.sender.send(IpcEvents.device.loadConfigurationReply, JSON.stringify(response)); diff --git a/packages/uhk-web/src/app/components/uhk-message/uhk-message.component.html b/packages/uhk-web/src/app/components/uhk-message/uhk-message.component.html index f652e6f28ec..1a2836e2325 100644 --- a/packages/uhk-web/src/app/components/uhk-message/uhk-message.component.html +++ b/packages/uhk-web/src/app/components/uhk-message/uhk-message.component.html @@ -12,16 +12,11 @@

{{ subtitle }}

{{ description }}

-
-
-
-
-
+
+ +
diff --git a/packages/uhk-web/src/app/components/uhk-message/uhk-message.component.scss b/packages/uhk-web/src/app/components/uhk-message/uhk-message.component.scss index 7a489fad40b..0e1fdbeb7be 100644 --- a/packages/uhk-web/src/app/components/uhk-message/uhk-message.component.scss +++ b/packages/uhk-web/src/app/components/uhk-message/uhk-message.component.scss @@ -26,22 +26,40 @@ margin-bottom: 1.5em; } -.loading-progress-bar { +.loading-progress-bar-wrapper { width: 100%; padding: 0 1.25em; } -.loading-progress-bar__track { - background-color: var(--color-loading-progress-bar-bg); - border-radius: 0.375rem; +.loading-progress-bar { + display: block; + width: 100%; height: 1rem; + appearance: none; + border: none; + border-radius: 0.375rem; overflow: hidden; -} + background-color: var(--color-loading-progress-bar-bg); -.loading-progress-bar__fill { - background-color: var(--color-loading-progress-bar-fill); - height: 100%; - transition: width 200ms ease-out; + &::-webkit-progress-inner-element { + border: none; + } + + &::-webkit-progress-bar { + background-color: var(--color-loading-progress-bar-bg); + } + + &::-webkit-progress-value { + background-color: var(--color-loading-progress-bar-fill); + border-radius: 0; + transition: inline-size 200ms ease-out; + } + + &::-moz-progress-bar { + background-color: var(--color-loading-progress-bar-fill); + border-radius: 0; + transition: inline-size 200ms ease-out; + } } .message { From bb96da0a5c672442aafce4c3a565e08bf526cbee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=C3=A1szl=C3=B3=20Monda?= Date: Tue, 7 Jul 2026 21:04:53 +0200 Subject: [PATCH 3/4] Address PR review feedback for progress bars. Set progress default values in ngrx state instead of using nullish coalescing, simplify progress reducers, encapsulate progress tracking variables inside their reporting closures, report fragment-based transfer percent, and drop the redundant progress-bar logo styling. Co-authored-by: Cursor --- .../uhk-agent/src/services/device.service.ts | 24 ++++++++------- packages/uhk-usb/src/uhk-operations.ts | 30 +++++++++---------- .../progress-button.component.html | 4 +-- .../uhk-message/uhk-message.component.html | 2 +- .../uhk-message/uhk-message.component.scss | 4 --- .../pages/loading-page/loading-device.page.ts | 5 ++-- .../src/app/store/reducers/app.reducer.ts | 6 +--- .../uhk-web/src/app/store/reducers/device.ts | 4 +-- .../store/reducers/progress-button-state.ts | 4 ++- 9 files changed, 41 insertions(+), 42 deletions(-) diff --git a/packages/uhk-agent/src/services/device.service.ts b/packages/uhk-agent/src/services/device.service.ts index 64f83503032..b108b9a6ae1 100644 --- a/packages/uhk-agent/src/services/device.service.ts +++ b/packages/uhk-agent/src/services/device.service.ts @@ -358,15 +358,17 @@ export class DeviceService { this.logService.misc('[DeviceService] load user configuration'); let response: ConfigurationReply; - let progress = 0; try { await this.stopPollUhkDevice(); - const sendProgress = (percent: number) => { - progress = Math.max(progress, Math.min(100, percent)); - event.sender.send(IpcEvents.device.loadConfigurationProgress, progress); - }; + const sendProgress = (() => { + let progress = 0; + return (percent: number) => { + progress = Math.max(progress, Math.min(100, percent)); + event.sender.send(IpcEvents.device.loadConfigurationProgress, progress); + }; + })(); sendProgress(0); await this.operations.waitUntilKeyboardBusy(); @@ -1312,11 +1314,13 @@ export class DeviceService { try { await this.stopPollUhkDevice(); - let progress = 0; - const sendProgress = (percent: number) => { - progress = Math.max(progress, Math.min(100, percent)); - event.sender.send(IpcEvents.device.saveUserConfigurationProgress, progress); - }; + const sendProgress = (() => { + let progress = 0; + return (percent: number) => { + progress = Math.max(progress, Math.min(100, percent)); + event.sender.send(IpcEvents.device.saveUserConfigurationProgress, progress); + }; + })(); sendProgress(0); await backupUserConfiguration(data); diff --git a/packages/uhk-usb/src/uhk-operations.ts b/packages/uhk-usb/src/uhk-operations.ts index 815726f7358..191d6f1426d 100644 --- a/packages/uhk-usb/src/uhk-operations.ts +++ b/packages/uhk-usb/src/uhk-operations.ts @@ -337,7 +337,6 @@ export class UhkOperations { reportTransferProgress(userConfigSize, offset); } ); - reportTransferProgress(userConfigSize, hardwareConfigSize); return { userConfiguration: JSON.stringify(convertBufferToIntArray(userConfiguration)), @@ -424,11 +423,13 @@ export class UhkOperations { } public async saveUserConfiguration(buffer: Buffer, onProgress?: (percent: number) => void): Promise { - let lastProgress = 0; - const reportProgress = (percent: number) => { - lastProgress = Math.max(lastProgress, Math.min(100, Math.round(percent))); - onProgress?.(lastProgress); - }; + const reportProgress = (() => { + let lastProgress = 0; + return (percent: number) => { + lastProgress = Math.max(lastProgress, Math.min(100, Math.round(percent))); + onProgress?.(lastProgress); + }; + })(); try { reportProgress(0); @@ -462,9 +463,12 @@ export class UhkOperations { userConfiguration.toBinary(resultBuffer) const configBuffer = resultBuffer.getBufferContent(); - reportProgress(2); - await this.sendConfigToKeyboard(configBuffer, true, (bytesSent, totalBytes) => { - reportProgress(2 + bytesSent / totalBytes * 83); + const preTransferPercent = 2; + const transferPercentRange = 83; + + reportProgress(preTransferPercent); + await this.sendConfigToKeyboard(configBuffer, true, (percent) => { + reportProgress(preTransferPercent + percent / 100 * transferPercentRange); }); reportProgress(86); await this.applyConfiguration(); @@ -965,7 +969,7 @@ export class UhkOperations { private async sendConfigToKeyboard( buffer: Buffer, isUserConfiguration, - onProgress?: (bytesSent: number, totalBytes: number) => void + onProgress?: (percent: number) => void ): Promise { const command = isUserConfiguration ? UsbCommand.WriteStagingUserConfig @@ -974,11 +978,7 @@ export class UhkOperations { const fragments = getTransferBuffers(command, buffer); for (let i = 0; i < fragments.length; i++) { await this.device.write(fragments[i]); - const bytesSent = Math.min( - buffer.length, - Math.round((i + 1) / fragments.length * buffer.length) - ); - onProgress?.(bytesSent, buffer.length); + onProgress?.(Math.round((i + 1) / fragments.length * 100)); } } diff --git a/packages/uhk-web/src/app/components/progress-button/progress-button.component.html b/packages/uhk-web/src/app/components/progress-button/progress-button.component.html index 101189405d0..1662b516b6e 100644 --- a/packages/uhk-web/src/app/components/progress-button/progress-button.component.html +++ b/packages/uhk-web/src/app/components/progress-button/progress-button.component.html @@ -4,10 +4,10 @@ [disabled]="state.showProgress"> + [style.width.%]="state.showProgress ? state.progressPercent : 0"> {{state.text}} diff --git a/packages/uhk-web/src/app/components/uhk-message/uhk-message.component.html b/packages/uhk-web/src/app/components/uhk-message/uhk-message.component.html index 1a2836e2325..742fd7ab893 100644 --- a/packages/uhk-web/src/app/components/uhk-message/uhk-message.component.html +++ b/packages/uhk-web/src/app/components/uhk-message/uhk-message.component.html @@ -1,5 +1,5 @@
-
+