Bug fixes and tweaks

This commit is contained in:
Mathias Malmqvist
2025-10-02 22:17:58 +02:00
committed by dualshock-tools
parent 0e8e7fc281
commit fea0e9c3fa
14 changed files with 208 additions and 128 deletions

View File

@@ -83,6 +83,16 @@ dl.row dd {
animation: pulse 1s ease-in-out infinite !important;
}
/* Quick Test accordion button height reduction */
#quickTestAccordion .accordion-button {
padding: 0.5rem 1rem;
}
/* Quick Test accordion body tint */
#quickTestAccordion .accordion-collapse .accordion-body {
background-color: rgba(13, 125, 253, 0.05);
}
/* Skip button hover behavior */
.skip-btn {
opacity: 0;
@@ -102,3 +112,8 @@ dl.row dd {
.blink-text {
animation: blink 1s infinite;
}
/* Set text color to red for internationalized elements */
/* .ds-i18n {
color: red;
} */

View File

@@ -138,9 +138,9 @@
<br>
<div class="d-flex justify-content-end">
<button type="button" class="btn btn-outline-primary ds-btn ds-i18n mb-3" onclick="show_quick_test_modal()" id="quick-test-btn">
<button type="button" class="btn btn-outline-primary ds-btn mb-3" onclick="show_quick_test_modal()" id="quick-test-btn">
<i class="fas fa-vial" id="quick-test-icon"></i>&nbsp;&nbsp;
<span id="quick-test-text">Quick Test</span>
<span id="quick-test-text" class="ds-i18n">Quick Test</span>
</button>
</div>
@@ -153,13 +153,13 @@
<div class="col-md-6 col-sm-12" style="min-width: 330px;">
<div class="vstack gap-2 p-2">
<button type="button" class="btn btn-primary ds-btn" onclick="calibrate_stick_centers()" id="four-step-center-calib" style="display: none;">
1. <span ds-i18n>Calibrate stick center</span>
1. <span class="ds-i18n">Calibrate stick center</span>
</button>
<button type="button" class="btn btn-primary ds-btn" onclick="auto_calibrate_stick_centers()" id="quick-center-calib" style="display: none;">
1. <span ds-i18n>Calibrate stick center</span>
1. <span class="ds-i18n">Calibrate stick center</span>
</button>
<button type="button" class="btn btn-primary ds-btn" onclick="calibrate_range()">
2. <span ds-i18n>Calibrate stick range</span>
2. <span class="ds-i18n">Calibrate stick range</span>
</button>
<button type="button" class="btn btn-primary ds-btn" onclick="ds5_finetune()" id="ds5finetune">
3. <span class="ds-i18n">Finetune stick calibration</span>
@@ -318,7 +318,7 @@
<footer class="fixed-bottom bg-body-tertiary border-top">
<div class="container">
<div class="d-flex flex-column flex-sm-row justify-content-between py-3" id="footbody">
<p class="mb-0"><a target="_blank" href="https://github.com/dualshock-tools/dualshock-tools.github.io/commits/main/"><span class="ds-i18n">Version</span> 2.18 beta 1</a> (2025-10-02) - <a href="#" class="ds-i18n" onclick="show_donate_modal();">Support this project</a>&nbsp;<span id="authorMsg"></span></p>
<p class="mb-0"><a target="_blank" href="https://github.com/dualshock-tools/dualshock-tools.github.io/commits/main/"><span class="ds-i18n">Version</span> 2.18 beta 2</a> (2025-10-03) - <a href="#" class="ds-i18n" onclick="show_donate_modal();">Support this project</a>&nbsp;<span id="authorMsg"></span></p>
<ul class="list-unstyled d-flex mb-0">
<li class="ms-3"><a class="link-body-emphasis" href="mailto:ds4@the.al" target="_blank"><svg class="bi" width="24" height="24"><use xlink:href="#mail"/></svg></a></li>

View File

@@ -1,6 +1,7 @@
'use strict';
import { sleep, la } from './utils.js';
import { l } from './translations.js'
/**
* Controller Manager - Manages the current controller instance and provides unified interface
@@ -8,7 +9,6 @@ import { sleep, la } from './utils.js';
class ControllerManager {
constructor(uiDependencies = {}) {
this.currentController = null;
this.l = uiDependencies.l;
this.handleNvStatusUpdate = uiDependencies.handleNvStatusUpdate;
this.has_changes_to_write = null;
this.inputHandler = null; // Callback function for input processing
@@ -109,6 +109,17 @@ class ControllerManager {
return this.currentController.getModel();
}
/**
* Get the list of supported quick tests for the current controller
* @returns {Array<string>} Array of supported test types
*/
getSupportedQuickTests() {
if (!this.currentController) {
return [];
}
return this.currentController.getSupportedQuickTests();
}
/**
* Check if a controller is connected
* @returns {boolean} True if controller is connected
@@ -183,7 +194,7 @@ class ControllerManager {
async nvsLock() {
const res = await this.currentController.nvsLock();
if (!res.ok) {
throw new Error(this.l("NVS Lock failed"), { cause: res.error });
throw new Error(l("NVS Lock failed"), { cause: res.error });
}
await this.queryNvStatus(); // Refresh NVS status
@@ -196,7 +207,7 @@ class ControllerManager {
async calibrateSticksBegin() {
const res = await this.currentController.calibrateSticksBegin();
if (!res.ok) {
throw new Error(`${this.l("Stick calibration failed")}. ${res.error?.message}`, { cause: res.error });
throw new Error(`${l("Stick calibration failed")}. ${res.error?.message}`, { cause: res.error });
}
}
@@ -207,7 +218,7 @@ class ControllerManager {
const res = await this.currentController.calibrateSticksSample();
if (!res.ok) {
await sleep(500);
throw new Error(this.l("Stick calibration failed"), { cause: res.error });
throw new Error(l("Stick calibration failed"), { cause: res.error });
}
}
@@ -218,7 +229,7 @@ class ControllerManager {
const res = await this.currentController.calibrateSticksEnd();
if (!res.ok) {
await sleep(500);
throw new Error(this.l("Stick calibration failed"), { cause: res.error });
throw new Error(l("Stick calibration failed"), { cause: res.error });
}
this.setHasChangesToWrite(true);
@@ -230,7 +241,7 @@ class ControllerManager {
async calibrateRangeBegin() {
const res = await this.currentController.calibrateRangeBegin();
if (!res.ok) {
throw new Error(`${this.l("Stick calibration failed")}. ${res.error?.message}`, { cause: res.error });
throw new Error(`${l("Stick calibration failed")}. ${res.error?.message}`, { cause: res.error });
}
}
@@ -241,7 +252,7 @@ class ControllerManager {
const res = await this.currentController.calibrateRangeEnd();
if(res?.ok) {
this.setHasChangesToWrite(true);
return { success: true, message: this.l("Range calibration completed") };
return { success: true, message: l("Range calibration completed") };
} else {
// Check if the error is code 3 (DS4/DS5) or codes 4/5 (DS5 Edge), which typically means
// the calibration was already ended or the controller is not in range calibration mode
@@ -254,7 +265,7 @@ class ControllerManager {
console.log("Range calibration end failed with unexpected error:", res);
await sleep(500);
const msg = res?.code ? (`${this.l("Range calibration failed")}. ${this.l("Error")} ${res.code}`) : (`${this.l("Range calibration failed")}. ${res?.error || ""}`);
const msg = res?.code ? (`${l("Range calibration failed")}. ${l("Error")} ${res.code}`) : (`${l("Range calibration failed")}. ${res?.error || ""}`);
return { success: false, message: msg, error: res?.error };
}
}
@@ -286,7 +297,7 @@ class ControllerManager {
await this.calibrateSticksEnd();
progressCallback(100);
return { success: true, message: this.l("Stick calibration completed") };
return { success: true, message: l("Stick calibration completed") };
} catch (e) {
la("multi_calibrate_sticks_failed", {"r": e});
throw e;
@@ -299,24 +310,24 @@ class ControllerManager {
*/
async disableLeftAdaptiveTrigger() {
if (!this.currentController) {
throw new Error(this.l("No controller connected"));
throw new Error(l("No controller connected"));
}
// Check if the controller supports adaptive triggers (DS5 only)
if (this.getModel() !== "DS5") {
throw new Error(this.l("Adaptive triggers are only supported on DualSense controllers"));
throw new Error(l("Adaptive triggers are only supported on DualSense controllers"));
}
// Check if the controller has the disableLeftAdaptiveTrigger method
if (typeof this.currentController.disableLeftAdaptiveTrigger !== 'function') {
throw new Error(this.l("Controller does not support adaptive trigger control"));
throw new Error(l("Controller does not support adaptive trigger control"));
}
try {
const result = await this.currentController.disableLeftAdaptiveTrigger();
return result;
} catch (error) {
throw new Error(this.l("Failed to disable adaptive trigger"), { cause: error });
throw new Error(l("Failed to disable adaptive trigger"), { cause: error });
}
}
@@ -345,7 +356,7 @@ class ControllerManager {
// if (preset === 'custom') {
// // Validate custom parameters
// if (typeof start !== 'number' || typeof end !== 'number' || typeof force !== 'number') {
// throw new Error(this.l("Custom preset requires start, end, and force parameters"));
// throw new Error(l("Custom preset requires start, end, and force parameters"));
// }
// }
@@ -374,7 +385,7 @@ class ControllerManager {
}
} catch (error) {
if(duration) doneCb({ success: false});
throw new Error(this.l("Failed to set vibration"), { cause: error });
throw new Error(l("Failed to set vibration"), { cause: error });
}
}
@@ -387,7 +398,7 @@ class ControllerManager {
async setSpeakerTone(duration = 1000, doneCb = ({success}) => {}, output = "speaker") {
try {
if (!this.currentController.setSpeakerTone) {
throw new Error(this.l("Speaker tone not supported on this controller"));
throw new Error(l("Speaker tone not supported on this controller"));
}
await this.currentController.setSpeakerTone(output);
@@ -409,7 +420,7 @@ class ControllerManager {
}
} catch (error) {
if(duration) doneCb({ success: false});
throw new Error(this.l("Failed to set speaker tone"), { cause: error });
throw new Error(l("Failed to set speaker tone"), { cause: error });
}
}
@@ -564,7 +575,7 @@ class ControllerManager {
*/
_batteryPercentToText({bat_capacity, is_charging, is_error}) {
if (is_error) {
return '<font color="red">' + this.l("error") + '</font>';
return '<font color="red">' + l("error") + '</font>';
}
const batteryIcons = [

View File

@@ -4,13 +4,10 @@
* Base Controller class that provides common functionality for all controller types
*/
class BaseController {
constructor(device, uiDependencies = {}) {
constructor(device) {
this.device = device;
this.model = "undefined"; // to be set by subclasses
this.finetuneMaxValue; // to be set by subclasses
// UI dependencies injected from core
this.l = uiDependencies.l;
}
getModel() {
@@ -184,6 +181,15 @@ class BaseController {
// Default no-op implementation for controllers that don't support player indicators
return { success: true, message: "This controller does not support player indicators" };
}
/**
* Get the list of supported quick tests for this controller
* @returns {Array<string>} Array of supported test types
*/
getSupportedQuickTests() {
// Default implementation - supports all tests
return ['usb', 'buttons', 'adaptive', 'haptic', 'lights', 'speaker', 'headphone', 'microphone'];
}
}
export default BaseController;

View File

@@ -21,20 +21,19 @@ class ControllerFactory {
/**
* Create a controller instance based on the HID device product ID
* @param {HIDDevice} device The HID device
* @param {Object} uiDependencies Optional UI dependencies (l function, etc.)
* @returns {BaseController} The appropriate controller instance
*/
static createControllerInstance(device, uiDependencies = {}) {
static createControllerInstance(device) {
switch (device.productId) {
case 0x05c4: // DS4 v1
case 0x09cc: // DS4 v2
return new DS4Controller(device, uiDependencies);
return new DS4Controller(device);
case 0x0ce6: // DS5
return new DS5Controller(device, uiDependencies);
return new DS5Controller(device);
case 0x0df2: // DS5 Edge
return new DS5EdgeController(device, uiDependencies);
return new DS5EdgeController(device);
default:
throw new Error(`Unsupported device: ${dec2hex(device.vendorId)}:${dec2hex(device.productId)}`);

View File

@@ -10,6 +10,7 @@ import {
lf,
la
} from '../utils.js';
import { l } from '../translations.js';
const NOT_GENUINE_SONY_CONTROLLER_MSG = "Your device might not be a genuine Sony controller. If it is not a clone then please report this issue.";
@@ -111,8 +112,8 @@ class DS4OutputStruct {
* DualShock 4 Controller implementation
*/
class DS4Controller extends BaseController {
constructor(device, uiDependencies = {}) {
super(device, uiDependencies);
constructor(device) {
super(device);
this.model = "DS4";
// Initialize current output state to track controller settings
@@ -134,8 +135,6 @@ class DS4Controller extends BaseController {
}
async getInfo() {
const { l } = this;
// Device-only: collect info and return a common structure; do not touch the DOM
try {
let deviceTypeText = l("unknown");
@@ -204,9 +203,9 @@ class DS4Controller extends BaseController {
const lockRes = await this.nvsLock();
if(!lockRes.ok) throw (lockRes.error || new Error("NVS lock failed"));
return { success: true, message: this.l("Changes saved successfully") };
return { success: true, message: l("Changes saved successfully") };
} catch(error) {
throw new Error(this.l("Error while saving changes"), { cause: error });
throw new Error(l("Error while saving changes"), { cause: error });
}
}
@@ -258,7 +257,7 @@ class DS4Controller extends BaseController {
la("ds4_calibrate_range_begin_failed", {"d1": d1, "d2": d2});
return {
ok: false,
error: new Error(this.l(NOT_GENUINE_SONY_CONTROLLER_MSG)),
error: new Error(l(NOT_GENUINE_SONY_CONTROLLER_MSG)),
code: 1, d1, d2
};
}
@@ -306,7 +305,7 @@ class DS4Controller extends BaseController {
la("ds4_calibrate_sticks_begin_failed", {"d1": d1, "d2": d2});
return {
ok: false,
error: new Error(this.l(NOT_GENUINE_SONY_CONTROLLER_MSG)),
error: new Error(l(NOT_GENUINE_SONY_CONTROLLER_MSG)),
code: 1, d1, d2,
};
}
@@ -401,7 +400,7 @@ class DS4Controller extends BaseController {
} else {
if(this.isRare(hw_ver))
return "WOW!";
return this.l("Unknown");
return l("Unknown");
}
}
@@ -689,6 +688,15 @@ class DS4Controller extends BaseController {
throw new Error('WebRTC getUserMedia API or mediaDevices enumeration not available.');
}
}
/**
* Get the list of supported quick tests for DS4 controller
* DS4 does not support adaptive triggers, speaker, or microphone
* @returns {Array<string>} Array of supported test types
*/
getSupportedQuickTests() {
return ['usb', 'buttons', 'haptic', 'lights', 'headphone'];
}
}
export default DS4Controller;

View File

@@ -12,6 +12,7 @@ import {
la,
lf
} from '../utils.js';
import { l } from '../translations.js';
// DS5 Button mapping configuration
const DS5_BUTTON_MAP = [
@@ -225,8 +226,8 @@ function ds5_color(x) {
* DualSense (DS5) Controller implementation
*/
class DS5Controller extends BaseController {
constructor(device, uiDependencies = {}) {
super(device, uiDependencies);
constructor(device) {
super(device);
this.model = "DS5";
this.finetuneMaxValue = 65535; // 16-bit max value for DS5
@@ -271,7 +272,6 @@ class DS5Controller extends BaseController {
}
async _getInfo(is_edge) {
const { l } = this;
// Device-only: collect info and return a common structure; do not touch the DOM
try {
console.log("Fetching DS5 info...");
@@ -352,9 +352,9 @@ class DS5Controller extends BaseController {
const lockRes = await this.nvsLock();
if(!lockRes.ok) throw (lockRes.error || new Error("NVS lock failed"));
return { success: true, message: this.l("Changes saved successfully") };
return { success: true, message: l("Changes saved successfully") };
} catch(error) {
throw new Error(this.l("Error while saving changes"), { cause: error });
throw new Error(l("Error while saving changes"), { cause: error });
}
}
@@ -384,7 +384,7 @@ class DS5Controller extends BaseController {
const data = await this.receiveFeatureReport(0x81);
} catch(error) {
await sleep(500);
throw new Error(this.l("NVS Unlock failed"), { cause: error });
throw new Error(l("NVS Unlock failed"), { cause: error });
}
}
@@ -398,7 +398,7 @@ class DS5Controller extends BaseController {
await this.sendFeatureReport(128, [base,num])
const pcba_id = lf("ds5_pcba_id", await this.receiveFeatureReport(129));
if(pcba_id.getUint8(1) != base || pcba_id.getUint8(2) != num || pcba_id.getUint8(3) != 2) {
return this.l("error");
return l("error");
}
if(decode)
return new TextDecoder().decode(pcba_id.buffer.slice(4, 4+length));
@@ -539,7 +539,7 @@ class DS5Controller extends BaseController {
if(a == 0x05) return "BDM-030";
if(a == 0x06) return "BDM-040";
if(a == 0x07 || a == 0x08) return "BDM-050";
return this.l("Unknown");
return l("Unknown");
}
async getInMemoryModuleData() {

View File

@@ -2,20 +2,19 @@
import DS5Controller from './ds5-controller.js';
import { sleep, dec2hex32, la, lf } from '../utils.js';
import { l } from "../translations.js";
/**
* DualSense Edge (DS5 Edge) Controller implementation
*/
class DS5EdgeController extends DS5Controller {
constructor(device, uiDependencies = {}) {
super(device, uiDependencies);
constructor(device) {
super(device);
this.model = "DS5_Edge";
this.finetuneMaxValue = 4095; // 12-bit max value for DS5 Edge
}
async getInfo() {
const { l } = this;
// DS5 Edge uses the same info structure as DS5 but with is_edge=true
const result = await this._getInfo(true);
@@ -41,12 +40,12 @@ class DS5EdgeController extends DS5Controller {
if(ret) {
return {
success: true,
message: "<b>" + this.l("Changes saved successfully") + "</b>.<br><br>" + this.l("If the calibration is not stored permanently, please double-check the wirings of the hardware mod."),
message: "<b>" + l("Changes saved successfully") + "</b>.<br><br>" + l("If the calibration is not stored permanently, please double-check the wirings of the hardware mod."),
isHtml: true
};
}
} catch(error) {
throw new Error(this.l("Error while saving changes"), { cause: error });
throw new Error(l("Error while saving changes"), { cause: error });
}
}
@@ -68,7 +67,7 @@ class DS5EdgeController extends DS5Controller {
await sleep(200);
const ret = await this.waitUntilWritten([21, 6, 2]);
if(!ret) {
throw new Error(this.l("Cannot unlock") + " " + this.l(m_name));
throw new Error(l("Cannot unlock") + " " + l(m_name));
}
}
@@ -79,7 +78,7 @@ class DS5EdgeController extends DS5Controller {
await sleep(200);
const ret = await this.waitUntilWritten([21, 4, 2]);
if(!ret) {
throw new Error(this.l("Cannot lock") + " " + this.l(m_name));
throw new Error(l("Cannot lock") + " " + l(m_name));
}
}
@@ -90,7 +89,7 @@ class DS5EdgeController extends DS5Controller {
await sleep(200);
const ret = await this.waitUntilWritten([21, 3, 2]);
if(!ret) {
throw new Error(this.l("Cannot store data into") + " " + this.l(m_name));
throw new Error(l("Cannot store data into") + " " + l(m_name));
}
}

View File

@@ -141,7 +141,7 @@ async function connect() {
initAnalyticsApi(app); // init with gu and jg
// Initialize controller manager with translation function
controller = initControllerManager({ l, handleNvStatusUpdate });
controller = initControllerManager({ handleNvStatusUpdate });
controller.setInputHandler(handleControllerInput);
la("begin");
@@ -224,7 +224,7 @@ async function continue_connection({data, device}) {
try {
// Create controller instance using factory
controllerInstance = ControllerFactory.createControllerInstance(device, { l });
controllerInstance = ControllerFactory.createControllerInstance(device);
controller.setControllerInstance(controllerInstance);
info = await controllerInstance.getInfo();
@@ -1058,7 +1058,7 @@ window.show_donate_modal = show_donate_modal;
window.board_model_info = board_model_info;
window.edge_color_info = edge_color_info;
window.show_quick_test_modal = () => {
show_quick_test_modal(controller, { l }).catch(error => {
show_quick_test_modal(controller).catch(error => {
throw new Error("Failed to show quick test modal", { cause: error });
});
};

View File

@@ -1,5 +1,7 @@
'use strict';
import { l } from '../translations.js';
const TEST_SEQUENCE = ['usb', 'buttons', 'adaptive', 'haptic', 'lights', 'speaker', 'headphone', 'microphone'];
const TEST_NAMES = {
'usb': 'USB Connector',
@@ -39,9 +41,8 @@ const BUTTON_INFILL_MAPPING = {
* Handles controller feature testing including haptic feedback, adaptive triggers, speaker, and microphone functionality
*/
export class QuickTestModal {
constructor(controllerInstance, { l }) {
constructor(controllerInstance) {
this.controller = controllerInstance;
this.l = l;
this.resetAllTests();
@@ -119,8 +120,9 @@ export class QuickTestModal {
/**
* Apply skipped tests to the UI (rebuild accordion with non-skipped tests)
*/
_applySkippedTestsToUI() {
async _applySkippedTestsToUI() {
this._buildDynamicAccordion();
await this._initSvgController();
this._updateSkippedTestsDropdown();
}
@@ -131,14 +133,13 @@ export class QuickTestModal {
const $accordion = $('#quickTestAccordion');
$accordion.empty();
// Get non-skipped tests in order
let activeTests = TEST_SEQUENCE.filter(testType => !this.state.skippedTests.includes(testType));
// Get supported tests from the controller
const supportedTests = this.controller.getSupportedQuickTests();
// Filter out unsupported tests for DS4 controllers
if (this.controller.getModel() === 'DS4') {
const ds4UnsupportedTests = ['adaptive', 'speaker', 'microphone'];
activeTests = activeTests.filter(testType => !ds4UnsupportedTests.includes(testType));
}
// Get non-skipped tests in order, filtered by what the controller supports
let activeTests = TEST_SEQUENCE.filter(testType =>
!this.state.skippedTests.includes(testType) && supportedTests.includes(testType)
);
activeTests.forEach(testType => {
const accordionItem = this._createAccordionItem(testType);
@@ -223,7 +224,7 @@ export class QuickTestModal {
</div>
<div class="alert alert-info mb-3">
<i class="fas fa-info-circle me-2"></i>
<span class="ds-i18n">The test will automatically pass when all buttons have turned green.</span>
<span class="ds-i18n">Long-press <kbd>Circle</kbd> to skip ahead.</span>
</div>
<div class="d-flex gap-2 mt-3">
<button type="button" class="btn btn-success" id="buttons-pass-btn" onclick="markTestResult('buttons', true)">
@@ -239,7 +240,7 @@ export class QuickTestModal {
`;
case 'haptic':
return `
<p class="ds-i18n">This test will activate the controller's vibration motors for 3 seconds.</p>
<p class="ds-i18n">This test will activate the controller's vibration motors, first the heavy one, and then the light one.</p>
<p class="ds-i18n"><strong>Instructions:</strong> Feel for vibration in the controller.</p>
<div class="d-flex gap-2 mt-3">
<button type="button" class="btn btn-success" id="haptic-pass-btn" onclick="markTestResult('haptic', true)">
@@ -267,10 +268,6 @@ export class QuickTestModal {
return `
<p class="ds-i18n">This test will cycle through red, green, and blue colors on the controller lightbar, animate the player indicator lights, and flash the mute button.</p>
<p class="ds-i18n"><strong>Instructions:</strong> Watch the controller lights change colors, the player lights animate, and the mute button flash.</p>
<div class="alert alert-info mb-3">
<i class="fas fa-info-circle me-2"></i>
<span class="ds-i18n">The lights will automatically cycle through colors and patterns until you mark the test as passed or failed.</span>
</div>
<div class="d-flex gap-2 mt-3">
<button type="button" class="btn btn-success" id="lights-pass-btn" onclick="markTestResult('lights', true)">
<i class="fas fa-check me-1"></i><span class="ds-i18n">Pass</span>
@@ -375,13 +372,13 @@ export class QuickTestModal {
const allTestsCompleted = this._areAllTestsCompleted();
if (activeTest === 'buttons') {
$instructionsText.html(this.l('Test all buttons, or long-press <kbd>Square</kbd> to Pass and <kbd>Cross</kbd> to Fail, or <kbd>Circle</kbd> to skip.'));
$instructionsText.html(l('Test all buttons, or long-press <kbd>Square</kbd> to Pass and <kbd>Cross</kbd> to Fail, or <kbd>Circle</kbd> to skip.'));
} else if (activeTest) {
$instructionsText.html(this.l('Press <kbd>Square</kbd> to Pass, <kbd>Cross</kbd> to Fail, or <kbd>Circle</kbd> to skip.'));
$instructionsText.html(l('Press <kbd>Square</kbd> to Pass, <kbd>Cross</kbd> to Fail, or <kbd>Circle</kbd> to skip.'));
} else if (allTestsCompleted) {
$instructionsText.html(this.l('Press <kbd>Circle</kbd> to close, or <kbd>Square</kbd> to start over'));
$instructionsText.html(l('Press <kbd>Circle</kbd> to close, or <kbd>Square</kbd> to start over'));
} else {
$instructionsText.html(this.l('Press <kbd>Square</kbd> to begin or <kbd>Circle</kbd> to close'));
$instructionsText.html(l('Press <kbd>Square</kbd> to begin or <kbd>Circle</kbd> to close'));
}
}
@@ -417,6 +414,8 @@ export class QuickTestModal {
$modal.on('hidden.bs.modal', this._boundModalHidden);
$modal.on('shown.bs.modal', () => {
this._updateInstructions();
// Automatically start the test sequence when modal opens
this._startTestSequence();
});
}
@@ -518,6 +517,10 @@ export class QuickTestModal {
});
this._resetButtonColors();
// Hide mute button for DS4 controllers
const model = this.controller.getModel();
this._setMuteVisibility(model !== 'DS4');
}
/**
@@ -530,6 +533,28 @@ export class QuickTestModal {
return this.svgContainer.querySelector(`#${id}`);
}
/**
* Get the list of buttons to test based on controller model
* DS4 controllers don't have a mute button
*/
_getAvailableButtons() {
const model = this.controller.getModel();
if (model === 'DS4') {
return BUTTONS.filter(button => button !== 'mute');
}
return BUTTONS;
}
/**
* Set mute button visibility in the quick test SVG
*/
_setMuteVisibility(show) {
const muteOutline = this._getQuickTestElement('qt-Mute_outline');
const muteInfill = this._getQuickTestElement('qt-Mute_infill');
if (muteOutline) muteOutline.style.display = show ? '' : 'none';
if (muteInfill) muteInfill.style.display = show ? '' : 'none';
}
/**
* Set color for SVG group elements
*/
@@ -633,14 +658,14 @@ export class QuickTestModal {
// Initialize button press counts only if not already initialized
if (!this.state.buttonPressCount || Object.keys(this.state.buttonPressCount).length === 0) {
this.state.buttonPressCount = {};
BUTTONS.forEach(button => {
this._getAvailableButtons().forEach(button => {
this.state.buttonPressCount[button] = 0;
});
}
// Check for any buttons that are already stuck pressed when the test starts
// and draw them as pressed
BUTTONS.forEach(button => {
this._getAvailableButtons().forEach(button => {
if (this.controller.button_states[button] === true) {
this._setButtonPressed(button, true);
}
@@ -686,7 +711,7 @@ export class QuickTestModal {
* Check if all buttons have been pressed the required number of times
*/
_checkButtonsTestComplete() {
const allPressed = BUTTONS.every(button => {
const allPressed = this._getAvailableButtons().every(button => {
const count = this.state.buttonPressCount[button] || 0;
// Special buttons (create, options, mute, ps) only need 1 press
const checkOnce = ['create', 'touchpad', 'options', 'l3', 'ps', 'mute', 'r3'].includes(button);
@@ -706,7 +731,7 @@ export class QuickTestModal {
resetButtonsTest() {
// Reset button press counts
this.state.buttonPressCount = {};
BUTTONS.forEach(button => {
this._getAvailableButtons().forEach(button => {
this.state.buttonPressCount[button] = 0;
});
@@ -717,7 +742,7 @@ export class QuickTestModal {
this._resetButtonColors();
// Check for any buttons that are already stuck pressed and draw them as pressed
BUTTONS.forEach(button => {
this._getAvailableButtons().forEach(button => {
if (this.controller.button_states[button] === true) {
this._setButtonPressed(button, true);
}
@@ -730,9 +755,11 @@ export class QuickTestModal {
async _startHapticTest() {
this._startIconAnimation('haptic');
await this.controller.setVibration({ heavyLeft: 255, lightRight: 0, duration: 500 }, async () => {
await this.controller.setVibration({ heavyLeft: 0, lightRight: 255, duration: 500 });
await setTimeout(async () => {
await this.controller.setVibration({ heavyLeft: 0, lightRight: 255, duration: 500 });
}, 500);
});
setTimeout(() => { this._stopIconAnimation('haptic'); }, 1000); }
setTimeout(() => { this._stopIconAnimation('haptic'); }, 1500); }
/**
* Start adaptive trigger test
@@ -984,12 +1011,12 @@ export class QuickTestModal {
if (passed) {
$statusBadge.attr('class', 'badge bg-success me-2');
$statusBadge.text(this.l('Passed'));
$statusBadge.text(l('Passed'));
$accordionItem.addClass('border-success');
$accordionButton.css('backgroundColor', 'rgba(25, 135, 84, 0.1)'); // Light green background
} else {
$statusBadge.attr('class', 'badge bg-danger me-2');
$statusBadge.text(this.l('Failed'));
$statusBadge.text(l('Failed'));
$accordionItem.addClass('border-danger');
$accordionButton.css('backgroundColor', 'rgba(220, 53, 69, 0.1)'); // Light red background
}
@@ -1010,7 +1037,7 @@ export class QuickTestModal {
/**
* Skip a test and remove it from the accordion
*/
skipTest(testType) {
async skipTest(testType) {
// Add to skipped tests if not already there
if (!this.state.skippedTests.includes(testType)) {
this.state.skippedTests.push(testType);
@@ -1031,6 +1058,7 @@ export class QuickTestModal {
// Rebuild the accordion without the skipped test
this._buildDynamicAccordion();
await this._initSvgController();
this._updateSkippedTestsDropdown();
this._updateTestSummary();
@@ -1041,7 +1069,7 @@ export class QuickTestModal {
/**
* Add a test back from the skipped list
*/
addTestBack(testType) {
async addTestBack(testType) {
// Remove from skipped tests
const index = this.state.skippedTests.indexOf(testType);
if (index > -1) {
@@ -1055,6 +1083,7 @@ export class QuickTestModal {
// Rebuild the accordion with the restored test
this._buildDynamicAccordion();
await this._initSvgController();
this._updateSkippedTestsDropdown();
this._updateTestSummary();
@@ -1077,7 +1106,7 @@ export class QuickTestModal {
$list.empty();
this.state.skippedTests.forEach(testType => {
const testName = this.l(TEST_NAMES[testType]);
const testName = l(TEST_NAMES[testType]);
const $item = $(`
<li>
<a class="dropdown-item" href="#" onclick="addTestBack('${testType}'); return false;">
@@ -1099,14 +1128,13 @@ export class QuickTestModal {
let passed = 0;
let skipped = this.state.skippedTests.length;
// Get active tests for this controller model
let activeTests = TEST_SEQUENCE.filter(testType => !this.state.skippedTests.includes(testType));
// Get supported tests from the controller
const supportedTests = this.controller.getSupportedQuickTests();
// Filter out unsupported tests for DS4 controllers
if (this.controller.getModel() === 'DS4') {
const ds4UnsupportedTests = ['adaptive', 'speaker', 'microphone'];
activeTests = activeTests.filter(testType => !ds4UnsupportedTests.includes(testType));
}
// Get active tests for this controller model (non-skipped and supported)
let activeTests = TEST_SEQUENCE.filter(testType =>
!this.state.skippedTests.includes(testType) && supportedTests.includes(testType)
);
activeTests.forEach(test => {
if (this.state[test] !== null) {
@@ -1119,12 +1147,12 @@ export class QuickTestModal {
const totalProcessed = completed + skipped;
if (totalProcessed === 0) {
$summary.text(this.l('No tests completed yet.'));
$summary.text(l('No tests completed yet.'));
$summary.attr('class', 'text-muted ds-i18n');
} else {
let summaryText = this.l(`${completed}/${numTests} tests completed. ${passed} passed, ${completed - passed} failed.`);
let summaryText = `${completed}/${numTests} ${l("tests completed")}. ${passed} ${"passed"}, ${completed - passed} ${"failed"}.`;
if (skipped > 0) {
summaryText += this.l(` ${skipped} skipped.`);
summaryText += ` ${skipped} l({"skipped"}).`;
}
$summary.text(summaryText);
$summary.attr('class', totalProcessed === numTests ? 'text-success' : 'text-info');
@@ -1239,7 +1267,7 @@ export class QuickTestModal {
* Track button presses for the buttons test
*/
_trackButtonPresses(changes) {
BUTTONS.forEach(button => {
this._getAvailableButtons().forEach(button => {
const handleLongpress = ['cross', 'square', 'triangle', 'circle'].includes(button);
if (changes[button] === true) {
// Button pressed - increment count and show dark blue infill
@@ -1343,9 +1371,9 @@ export class QuickTestModal {
/**
* Start the test sequence from the beginning
*/
_startTestSequence() {
async _startTestSequence() {
// First, reset all tests to ensure clean state
this.resetAllTests();
await this.resetAllTests();
// After a short delay, start with the first non-skipped test
setTimeout(() => {
@@ -1354,7 +1382,10 @@ export class QuickTestModal {
if (firstAvailableTest) {
const $firstCollapse = $(`#${firstAvailableTest}-test-collapse`);
bootstrap.Collapse.getOrCreateInstance($firstCollapse[0]).show();
// Check if the element exists in the DOM before trying to create a Collapse instance
if ($firstCollapse.length > 0 && $firstCollapse[0]) {
bootstrap.Collapse.getOrCreateInstance($firstCollapse[0]).show();
}
}
}, 300);
}
@@ -1367,8 +1398,18 @@ export class QuickTestModal {
if (!activeTest) return;
const currentIndex = TEST_SEQUENCE.indexOf(activeTest);
const previousIndex = currentIndex > 0 ? currentIndex - 1 : 0;
if(previousIndex == currentIndex) return;
// Find the previous non-skipped test
let previousIndex = -1;
for (let i = currentIndex - 1; i >= 0; i--) {
if (!this.state.skippedTests.includes(TEST_SEQUENCE[i])) {
previousIndex = i;
break;
}
}
// If no previous test found, stay on current
if (previousIndex === -1) return;
const previousTest = TEST_SEQUENCE[previousIndex];
@@ -1380,14 +1421,17 @@ export class QuickTestModal {
// Expand previous test after a short delay
setTimeout(() => {
const $previousCollapse = $(`#${previousTest}-test-collapse`);
bootstrap.Collapse.getOrCreateInstance($previousCollapse[0]).show();
// Check if the element exists in the DOM before trying to create a Collapse instance
if ($previousCollapse.length > 0 && $previousCollapse[0]) {
bootstrap.Collapse.getOrCreateInstance($previousCollapse[0]).show();
}
}, 300);
}
/**
* Reset all tests to initial state
*/
resetAllTests() {
async resetAllTests() {
// Clear any active long-press timers before resetting state
this._clearAllLongPressTimers();
@@ -1409,7 +1453,7 @@ export class QuickTestModal {
const $accordionButton = $accordionItem.find('.accordion-button');
$statusBadge.attr('class', 'badge bg-secondary me-2');
$statusBadge.text(this.l('Not tested'));
$statusBadge.text(l('Not tested'));
$accordionItem.removeClass('border-success border-danger');
$accordionButton.css('backgroundColor', ''); // Clear background color
@@ -1423,7 +1467,7 @@ export class QuickTestModal {
});
// Apply skipped tests to UI (hide skipped items)
this._applySkippedTestsToUI();
await this._applySkippedTestsToUI();
this._updateTestSummary();
@@ -1472,12 +1516,12 @@ export function quicktest_handle_controller_input(changes) {
/**
* Show the Quick Test modal (legacy function for backward compatibility)
*/
export async function show_quick_test_modal(controller, { l } = {}) {
export async function show_quick_test_modal(controller) {
// Destroy any existing instance
destroyCurrentInstance();
// Create new instance
currentQuickTestInstance = new QuickTestModal(controller, { l });
currentQuickTestInstance = new QuickTestModal(controller);
await currentQuickTestInstance.open();
}

View File

@@ -137,7 +137,7 @@ export function l(text) {
const [out] = translationState.lang_cur[text] || [];
if(out) return out;
console.log("Missing translation for: '" + text + "'");
console.log(`Missing translation for "${text}"`);
return text;
}
@@ -167,7 +167,7 @@ function lang_translate(target_file, target_lang, target_direction) {
if (translatedText) {
$(item).html(translatedText);
} else {
console.log("Cannot find mapping for " + originalText);
console.log(`Cannot find mapping for "${originalText}"`);
$(item).html(originalText);
}
}

View File

@@ -31,12 +31,14 @@
<div class="alert alert-info finetune-circularity-mode" role="alert">
<p>
<i class="fas fa-info-circle"></i>&nbsp;&nbsp;
<span class="ds-i18n">While holding the stick to be adjusted straight up/down/left/right, make adjustments until you see lightblue sectors in all four directions after circling the stick both left and right. Then use the<svg width="24" height="24" style="vertical-align: -6px;"><use xlink:href="#arrows-alt"/></svg>button to increase the non-circularity.</span>
<span class="ds-i18n">While holding the stick to be adjusted straight up/down/left/right, make adjustments until you see lightblue sectors in all four directions after circling the stick both left and right. Then use the</span>
<svg width="24" height="24" style="vertical-align: -6px;"><use xlink:href="#arrows-alt"/></svg>
<span class="ds-i18n">to increase the non-circularity.</span>
</p>
<p class="mb-0">
<i class="fas fa-hand-point-up"></i>&nbsp;&nbsp;
<span class="ds-i18n"><strong>Average circularity error:</strong> smaller is not always better! Aim for 7-9 %.</span>
<a class="btn-link text-decoration-none" href="#" id="learn-more-link">Learn more...</a>
<a class="btn-link text-decoration-none ds-i18n" href="#" id="learn-more-link">Learn more...</a>
</p>
<p class="mb-0" id="learn-more-text" style="display: none;">
<span class="ds-i18n">Sony controllers come from the factory calibrated to have an average circularity error of nearly 10 %, and this is now what games expect. Too perfect circularity can make movements and aim feel stiff and unresponsive in some games.</span>

View File

@@ -18,7 +18,7 @@
</div>
<div class="mt-4">
<h6 class="ds-i18n">Test Summary:</h6>
<h6><span class="ds-i18n">Test Summary</span>:</h6>
<div id="test-summary" class="text-muted ds-i18n">No tests completed yet.</div>
</div>
</div>

View File

@@ -13,20 +13,16 @@
</div>
<div class="mt-2">
<small class="text-muted">
<span class="ds-i18n">Progress:</span> <span id="range-progress-text">0%</span>
<span class="ds-i18n">Progress</span>: <span id="range-progress-text">0%</span>
</small>
</div>
<div class="alert alert-info mt-3" id="range-calibration-alert" role="alert">
<span id="keep-rotating-alert" ><i class="fas fa-info-circle"></i>
<small class="ds-i18n">
Keep rotating the sticks even if you see no progress!
</small>
<small class="ds-i18n">Keep rotating the sticks even if you see no progress!</small>
</span>
<br><br>
<small class="ds-i18n">
The <b>Done</b> button will unlock after at most 15 seconds. If you press <b>Done</b> without rotating the sticks, the calibration will be incomplete and you will need to repeat it.
</small>
<small class="ds-i18n">The <b>Done</b> button will unlock after at most 15 seconds. If you press <b>Done</b> without rotating the sticks, the calibration will be incomplete and you will need to repeat it.</small>
</div>
</div>
<div class="modal-footer">