From 68bfe1388b2b99502dda1517a0cd79291c37fb1a Mon Sep 17 00:00:00 2001 From: Mathias Malmqvist Date: Wed, 10 Sep 2025 01:59:51 +0200 Subject: [PATCH] Show progress in range calibration modal and show warning when calibration failed --- css/main.css | 10 ++ js/core.js | 17 +++ js/modals/calib-range-modal.js | 207 +++++++++++++++++++++++++++++++-- templates/range-modal.html | 24 +++- 4 files changed, 248 insertions(+), 10 deletions(-) diff --git a/css/main.css b/css/main.css index c5e4d6f..9c840ba 100644 --- a/css/main.css +++ b/css/main.css @@ -92,3 +92,13 @@ dl.row dd { .accordion-header:hover .skip-btn { opacity: 1; } + +/* Blinking animation for range calibration alert */ +@keyframes blink { + 0%, 50% { opacity: 1; } + 51%, 100% { opacity: 0.3; } +} + +.blink-text { + animation: blink 1s infinite; +} diff --git a/js/core.js b/js/core.js index 8b9afb4..ccf3d43 100644 --- a/js/core.js +++ b/js/core.js @@ -21,6 +21,8 @@ const app = { disable_btn: 0, last_disable_btn: 0, + shownRangeCalibrationWarning: false, + // Language and UI state lang_orig_text: {}, lang_orig_text: {}, @@ -669,6 +671,19 @@ function get_current_test_tab() { return activeBtn?.id || 'haptic-test-tab'; } +function detectFailedRangeCalibration(changes) { + if (!changes.sticks || app.shownRangeCalibrationWarning) return; + + const { left, right } = changes.sticks; + const failedCalibration = [left, right].some(({x, y}) => Math.abs(x) + Math.abs(y) == 2); + const hasOpenModals = document.querySelectorAll('.modal.show').length > 0; + + if (failedCalibration && !app.shownRangeCalibrationWarning && !hasOpenModals) { + app.shownRangeCalibrationWarning = true; + show_popup(l("Range calibration appears to have failed. Please try again and make sure you rotate the sticks.")); + } +} + // Callback function to handle UI updates after controller input processing function handleControllerInput({ changes, inputConfig, touchPoints, batteryStatus }) { const { buttonMap } = inputConfig; @@ -689,6 +704,7 @@ function handleControllerInput({ changes, inputConfig, touchPoints, batteryStatu update_stick_graphics(changes); update_ds_button_svg(changes, buttonMap); update_touchpad_circles(touchPoints); + detectFailedRangeCalibration(changes); } break; @@ -984,6 +1000,7 @@ window.calibrate_range = () => calibrate_range( resetStickDiagrams(); successAlert(message); switchToRangeMode(); + app.shownRangeCalibrationWarning = false } } ); diff --git a/js/modals/calib-range-modal.js b/js/modals/calib-range-modal.js index 4a03c14..0e6b8fe 100644 --- a/js/modals/calib-range-modal.js +++ b/js/modals/calib-range-modal.js @@ -1,14 +1,42 @@ 'use strict'; import { sleep } from '../utils.js'; +import { l } from '../translations.js'; +import { CIRCULARITY_DATA_SIZE } from '../stick-renderer.js'; + +const SECONDS_UNTIL_UNLOCK = 15; /** * Calibrate Stick Range Modal Class * Handles stick range calibration */ export class CalibRangeModal { - constructor(controllerInstance, doneCallback = null) { + constructor(controllerInstance, { resetStickDiagrams, successAlert, ll_data, rr_data }, doneCallback = null) { + // Dependencies this.controller = controllerInstance; + this.resetStickDiagrams = resetStickDiagrams; + this.successAlert = successAlert; + this.ll_data = ll_data; + this.rr_data = rr_data; + + // Progress tracking + this.buttonText = l("Done"); + this.leftNonZeroCount = 0; + this.rightNonZeroCount = 0; + this.leftFullCycles = 0; + this.rightFullCycles = 0; + this.requiredFullCycles = 4; + this.progressUpdateInterval = null; + + // Countdown timer + this.countdownSeconds = 0; + this.countdownInterval = null; + + // Progress alert enhancement + this.leftCycleProgress = 0; + this.rightCycleProgress = 0; + + this.allDonePromiseResolve = undefined; this.doneCallback = doneCallback; } @@ -16,13 +44,30 @@ export class CalibRangeModal { if(!this.controller.isConnected()) return; + $('#range-calibration-alert').hide(); + $('#keep-rotating-alert').removeClass('blink-text'); + $('#range-done-btn') + .prop('disabled', true) + .toggleClass('btn-primary', false) + .toggleClass('btn-outline-primary', true); bootstrap.Modal.getOrCreateInstance('#rangeModal').show(); + this.resetStickDiagrams(); + + this.updateProgress(); // reset progress bar + this.startProgressMonitoring(); + + this.resetAlertEnhancement(); + this.startCountdown(); await sleep(1000); await this.controller.calibrateRangeBegin(); } async onClose() { + this.stopProgressMonitoring(); + this.stopCountdown(); + this.resetStickDiagrams(); + bootstrap.Modal.getOrCreateInstance('#rangeModal').hide(); const result = await this.controller.calibrateRangeOnClose(); @@ -31,31 +76,177 @@ export class CalibRangeModal { if (this.doneCallback && typeof this.doneCallback === 'function') { this.doneCallback(true, result?.message); } + this.allDonePromiseResolve(); + } + + /** + * Start monitoring progress by checking ll_data and rr_data arrays + */ + startProgressMonitoring() { + this.progressUpdateInterval = setInterval(() => { + this.checkDataProgress(); + }, 100); // Check every 100ms + } + + /** + * Stop progress monitoring + */ + stopProgressMonitoring() { + if (this.progressUpdateInterval) { + clearInterval(this.progressUpdateInterval); + this.progressUpdateInterval = null; + } + } + + /** + * Start countdown timer for Done button + */ + startCountdown() { + this.countdownSeconds = SECONDS_UNTIL_UNLOCK; + this.updateCountdownButton(); + + // Every second, update countdown + this.countdownInterval = setInterval(() => { + this.countdownSeconds--; + if (this.countdownSeconds <= 0 || this.leftCycleProgress + this.rightCycleProgress >= 100) { + this.stopCountdown(); + + $('#range-calibration-alert').hide(); + $('#range-done-btn') + .prop('disabled', false) + .toggleClass('btn-primary', true) + .toggleClass('btn-outline-primary', false); + + this.updateCountdownButton(); + } else { + this.checkAndEnhanceAlert(); + } + this.updateCountdownButton(); + }, 1000); + } + + /** + * Stop countdown timer + */ + stopCountdown() { + if (!this.countdownInterval) return; + + clearInterval(this.countdownInterval); + this.countdownInterval = null; + this.countdownSeconds = 0; + this.updateCountdownButton(); + } + + /** + * Update countdown button text and state + */ + updateCountdownButton() { + const seconds = this.countdownSeconds; + const text = this.buttonText + (seconds > 0 ? ` (${seconds})` : ""); + $('#range-done-btn').text(text); + } + + /** + * Check if ll_data and rr_data have received data + */ + checkDataProgress() { + const JOYSTICK_EXTREME_THRESHOLD = 0.95; + const CIRCLE_FILL_THRESHOLD = 0.95; + + // Count the number of times the joysticks have been rotated full circle + const leftNonZeroCount = this.ll_data.filter(v => v > JOYSTICK_EXTREME_THRESHOLD).length + const leftFillRatio = leftNonZeroCount / CIRCULARITY_DATA_SIZE; + if (leftFillRatio >= CIRCLE_FILL_THRESHOLD) { + this.leftFullCycles++; + this.ll_data.fill(0); + } + + const rightNonZeroCount = this.rr_data.filter(v => v > JOYSTICK_EXTREME_THRESHOLD).length; + const rightFillRatio = rightNonZeroCount / CIRCULARITY_DATA_SIZE; + if (rightFillRatio >= CIRCLE_FILL_THRESHOLD) { + this.rightFullCycles++; + this.rr_data.fill(0); + } + + // Update progress if counts changed + if (leftNonZeroCount !== this.leftNonZeroCount || rightNonZeroCount !== this.rightNonZeroCount) { + this.leftNonZeroCount = leftNonZeroCount; + this.rightNonZeroCount = rightNonZeroCount; + this.updateProgress(); + } + } + + /** + * Update the progress bar and enable/disable Done button + */ + updateProgress() { + // Calculate progress based on full cycles completed + // Each stick needs to complete 6 full cycles to contribute 50% to total progress + const leftCycleProgress = Math.min(this.leftFullCycles / this.requiredFullCycles, 1) * 50; + const rightCycleProgress = Math.min(this.rightFullCycles / this.requiredFullCycles, 1) * 50; + this.leftCycleProgress = leftCycleProgress; + this.rightCycleProgress = rightCycleProgress; + + // Add current partial progress for visual feedback + const leftCurrentProgress = (this.leftNonZeroCount / CIRCULARITY_DATA_SIZE) * (50 / this.requiredFullCycles); + const rightCurrentProgress = (this.rightNonZeroCount / CIRCULARITY_DATA_SIZE) * (50 / this.requiredFullCycles); + + const totalProgress = Math.round(leftCycleProgress + rightCycleProgress + leftCurrentProgress + rightCurrentProgress); + // this.totalProgress = totalProgress; + + const $progressBar = $('#range-progress-bar'); + const $progressText = $('#range-progress-text'); + + $progressBar + .css('width', `${totalProgress}%`) + .attr('aria-valuenow', totalProgress); + + $progressText.text(`${totalProgress}% (L:${this.leftFullCycles}/${this.requiredFullCycles}, R:${this.rightFullCycles}/${this.requiredFullCycles})`); + } + + checkAndEnhanceAlert() { + const secondsElapsed = SECONDS_UNTIL_UNLOCK - this.countdownSeconds; + + const alertIsVisible = $('#range-calibration-alert').is(":visible") + const progressBelowThreshold = this.leftCycleProgress < 10 || this.rightCycleProgress < 10; + if (secondsElapsed > 5 && progressBelowThreshold && !alertIsVisible) { + $('#range-calibration-alert').show(); + } + + const isBlinking = $('#keep-rotating-alert').hasClass('blink-text'); + if (secondsElapsed > 10 && progressBelowThreshold && !isBlinking) { + $('#keep-rotating-alert').addClass('blink-text'); + } + } + + resetAlertEnhancement() { + $('#keep-rotating-alert').removeClass('blink-text'); } } // Global reference to the current range calibration instance let currentCalibRangeInstance = null; -/** - * Helper function to safely clear the current calibration instance - */ function destroyCurrentInstance() { currentCalibRangeInstance = null; } -// Legacy function exports for backward compatibility -export async function calibrate_range(controller, doneCallback = null) { +export async function calibrate_range(controller, dependencies, doneCallback = null) { destroyCurrentInstance(); // Clean up any existing instance - currentCalibRangeInstance = new CalibRangeModal(controller, doneCallback); + currentCalibRangeInstance = new CalibRangeModal(controller, dependencies, doneCallback); + await currentCalibRangeInstance.open(); + return new Promise((resolve) => { + currentCalibRangeInstance.allDonePromiseResolve = resolve; + }); } async function calibrate_range_on_close() { if (currentCalibRangeInstance) { await currentCalibRangeInstance.onClose(); + destroyCurrentInstance(); } } -// Legacy compatibility - expose functions to window for HTML onclick handlers +// Expose functions to window for HTML onclick handlers window.calibrate_range_on_close = calibrate_range_on_close; \ No newline at end of file diff --git a/templates/range-modal.html b/templates/range-modal.html index 8a63aa9..e7cf6a1 100644 --- a/templates/range-modal.html +++ b/templates/range-modal.html @@ -7,10 +7,30 @@