Refactor the code reading and writing local storage

This commit is contained in:
Mathias Malmqvist
2025-11-23 22:11:42 +01:00
parent e63eb47374
commit b0fea23081
8 changed files with 317 additions and 140 deletions

View File

@@ -1,7 +1,8 @@
'use strict';
import { sleep, la } from './utils.js';
import { l } from './translations.js'
import { l } from './translations.js';
import { Storage } from './storage.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.";
@@ -46,50 +47,29 @@ class ControllerManager {
}
/**
* Generate a unique storage key for the device
* @param {string} serialNumber The device serial number
* @returns {string} Storage key based on serial number
*/
_getDeviceStorageKey(serialNumber) {
if (!serialNumber) return null;
return `changes_${serialNumber}`;
}
/**
* Save has_changes_to_write state to localStorage
* Save has_changes_to_write state to storage
*/
async _saveHasChangesState() {
if (!this.currentController) return;
try {
const serialNumber = await this.currentController.getSerialNumber();
const key = this._getDeviceStorageKey(serialNumber);
if (key) {
localStorage.setItem(key, JSON.stringify(this.has_changes_to_write));
}
Storage.hasChangesState.set(serialNumber, this.has_changes_to_write);
} catch (e) {
console.warn('Failed to save changes state:', e);
}
}
/**
* Restore has_changes_to_write state from localStorage
* Restore has_changes_to_write state from storage
*/
async _restoreHasChangesState() {
if (!this.currentController) return;
try {
const serialNumber = await this.currentController.getSerialNumber();
const key = this._getDeviceStorageKey(serialNumber);
if (key) {
const saved = localStorage.getItem(key);
if (saved !== null) {
try {
const restoredState = JSON.parse(saved);
this.has_changes_to_write = restoredState;
this._updateUI();
} catch (e) {
console.warn('Failed to parse changes state:', e);
}
}
const restoredState = Storage.hasChangesState.get(serialNumber);
if (restoredState !== null) {
this.has_changes_to_write = restoredState;
this._updateUI();
}
} catch (e) {
console.warn('Failed to restore changes state:', e);
@@ -108,19 +88,16 @@ class ControllerManager {
}
/**
* Clear controller state: remove localStorage entry and reset UI
* Clear controller state: remove storage entry and reset UI
* @private
*/
async _clearControllerState() {
if (this.currentController) {
try {
const serialNumber = await this.currentController.getSerialNumber();
const key = this._getDeviceStorageKey(serialNumber);
if (key) {
localStorage.removeItem(key);
}
Storage.hasChangesState.clear(serialNumber);
} catch (e) {
console.warn('Failed to clear localStorage:', e);
console.warn('Failed to clear storage:', e);
}
}
this.has_changes_to_write = false;

View File

@@ -1,6 +1,7 @@
'use strict';
import { sleep, float_to_str, dec2hex, dec2hex32, lerp_color, initAnalyticsApi, la, createCookie, readCookie } from './utils.js';
import { sleep, float_to_str, dec2hex, dec2hex32, lerp_color, initAnalyticsApi, la } from './utils.js';
import { Storage } from './storage.js';
import { initControllerManager } from './controller-manager.js';
import ControllerFactory from './controllers/controller-factory.js';
import { lang_init, l } from './translations.js';
@@ -118,9 +119,8 @@ function gboot() {
$("input[name='displayMode']").on('change', on_stick_mode_change);
// Setup edge modal "Don't show again" checkbox
$('#edgeModalDontShowAgain').on('change', function() {
localStorage.setItem('edgeModalDontShowAgain', this.checked.toString());
Storage.edgeModalDontShowAgain.set(this.checked);
});
}
@@ -310,7 +310,7 @@ async function continue_connection({data, device}) {
}
}
localStorage.setItem('lastConnectedController', JSON.stringify(lastConnectedInfo));
Storage.lastConnectedController.set(lastConnectedInfo);
updateLastConnectedInfo();
// Initialize SVG controller based on model
@@ -405,17 +405,15 @@ async function disconnect() {
function updateLastConnectedInfo() {
const $lastConnected = $("#lastConnected");
const $infoDiv = $("#lastConnectedInfo");
const lastConnectedInfo = localStorage.getItem('lastConnectedController');
const info = Storage.lastConnectedController.get();
if (!lastConnectedInfo) {
if (!info) {
console.log("No last connected info found.", $lastConnected);
$lastConnected.hide();
return;
}
try {
const info = JSON.parse(lastConnectedInfo);
const parts = [];
if (info.color) parts.push(info.color);
if (info.boardModel) parts.push(info.boardModel);
@@ -429,10 +427,7 @@ function updateLastConnectedInfo() {
$infoDiv.text(text);
if (info.serialNumber) {
const storageKey = `changes_${info.serialNumber}`;
const savedChangesState = localStorage.getItem(storageKey);
const hasChanges = savedChangesState ? JSON.parse(savedChangesState) : false;
const hasChanges = Storage.hasChangesState.get(info.serialNumber);
const $warning = $("#lastConnectedWarning");
$warning.toggle(hasChanges);
}
@@ -497,7 +492,7 @@ function set_edge_progress(score) {
}
function show_welcome_modal() {
const already_accepted = readCookie("welcome_accepted");
const already_accepted = Storage.getString("welcome_accepted");
if(already_accepted == "1")
return;
@@ -506,7 +501,7 @@ function show_welcome_modal() {
function welcome_accepted() {
la("welcome_accepted");
createCookie("welcome_accepted", "1");
Storage.setString("welcome_accepted", "1");
$("#welcomeModal").modal("hide");
}
@@ -861,7 +856,7 @@ function detectFailedRangeCalibration(changes) {
if (failedCalibration && !app.shownRangeCalibrationWarning && !hasOpenModals) {
app.failedCalibrationCount++;
localStorage.setItem('failedCalibrationCount', app.failedCalibrationCount.toString());
Storage.failedCalibrationCount.set(app.failedCalibrationCount);
app.shownRangeCalibrationWarning = true;
if (app.failedCalibrationCount <= 6) { // keep it from getting annoying
@@ -1088,8 +1083,7 @@ function show_donate_modal() {
function show_edge_modal() {
// Check if user has chosen not to show the modal again
const dontShowAgain = localStorage.getItem('edgeModalDontShowAgain');
if (dontShowAgain === 'true') {
if (Storage.edgeModalDontShowAgain.get()) {
return;
}
@@ -1268,7 +1262,7 @@ window.setCenterCalibrationMethod = (method, event) => {
event.stopPropagation();
}
app.centerCalibrationMethod = method;
localStorage.setItem('centerCalibrationMethod', method);
Storage.centerCalibrationMethod.set(method);
updateCalibrationMethodUI();
// Close the dropdown
const dropdownButton = event?.target?.closest('.dropdown-menu')?.previousElementSibling;
@@ -1310,7 +1304,7 @@ window.setRangeCalibrationMethod = (method, event) => {
event.stopPropagation();
}
app.rangeCalibrationMethod = method;
localStorage.setItem('rangeCalibrationMethod', method);
Storage.rangeCalibrationMethod.set(method);
updateCalibrationMethodUI();
// Close the dropdown
const dropdownButton = event?.target?.closest('.dropdown-menu')?.previousElementSibling;
@@ -1342,19 +1336,19 @@ function updateCalibrationMethodUI() {
}
function initCalibrationMethod() {
const savedCenterMethod = localStorage.getItem('centerCalibrationMethod');
const savedCenterMethod = Storage.centerCalibrationMethod.get();
if (savedCenterMethod && (savedCenterMethod === 'quick' || savedCenterMethod === 'four-step')) {
app.centerCalibrationMethod = savedCenterMethod;
}
const savedRangeMethod = localStorage.getItem('rangeCalibrationMethod');
const savedRangeMethod = Storage.rangeCalibrationMethod.get();
if (savedRangeMethod && (savedRangeMethod === 'normal' || savedRangeMethod === 'expert')) {
app.rangeCalibrationMethod = savedRangeMethod;
}
const savedFailedCalibrationCount = localStorage.getItem('failedCalibrationCount');
if (savedFailedCalibrationCount) {
app.failedCalibrationCount = parseInt(savedFailedCalibrationCount, 10);
const savedFailedCalibrationCount = Storage.failedCalibrationCount.get();
if (savedFailedCalibrationCount > 0) {
app.failedCalibrationCount = savedFailedCalibrationCount;
}
updateCalibrationMethodUI();

View File

@@ -1,6 +1,7 @@
'use strict';
const STORAGE_KEY = 'finetuneHistory';
import { Storage } from './storage.js';
const MAX_HISTORY_ENTRIES_PER_CONTROLLER = 10;
/**
@@ -134,13 +135,12 @@ export class FinetuneHistory {
// ==================== PRIVATE METHODS ====================
/**
* Get all history from localStorage (for all controllers)
* Get all history from storage (for all controllers)
* @private
*/
static _getAllHistory() {
try {
const stored = localStorage.getItem(STORAGE_KEY);
return stored ? JSON.parse(stored) : {};
return Storage.finetuneHistory.getAll();
} catch (e) {
console.error('Failed to parse finetune history:', e);
return {};
@@ -148,12 +148,12 @@ export class FinetuneHistory {
}
/**
* Save all history to localStorage
* Save all history to storage
* @private
*/
static _saveAllHistory(allHistory) {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(allHistory));
Storage.finetuneHistory.setAll(allHistory);
} catch (e) {
console.error('Failed to save finetune history:', e);
}

View File

@@ -2,6 +2,7 @@
import { draw_stick_dial } from '../stick-renderer.js';
import { dec2hex32, float_to_str, la } from '../utils.js';
import { Storage } from '../storage.js';
import { auto_calibrate_stick_centers } from './calib-center-modal.js';
import { calibrate_range } from './calib-range-modal.js';
@@ -496,13 +497,12 @@ export class Finetune {
// Private methods
/**
* Restore the show raw numbers checkbox state from localStorage
* Restore the show raw numbers checkbox state from storage
*/
_restoreShowRawNumbersCheckbox() {
const savedState = localStorage.getItem('showRawNumbersCheckbox');
if (savedState) {
const isChecked = savedState === 'true';
$("#showRawNumbersCheckbox").prop('checked', isChecked);
const isChecked = Storage.showRawNumbersCheckbox.get();
if (isChecked) {
$("#showRawNumbersCheckbox").prop('checked', true);
}
}
@@ -740,7 +740,7 @@ export class Finetune {
const showRawNumbers = $("#showRawNumbersCheckbox").is(":checked");
const modal = $("#finetuneModal");
modal.toggleClass("hide-raw-numbers", !showRawNumbers);
localStorage.setItem('showRawNumbersCheckbox', showRawNumbers);
Storage.showRawNumbersCheckbox.set(showRawNumbers);
this.refresh_finetune_sticks();
}
@@ -1156,25 +1156,23 @@ export class Finetune {
}
/**
* Save step size to localStorage
* Save step size to storage
*/
_saveStepSizeToLocalStorage() {
localStorage.setItem('finetuneCenterStepSize', this._centerStepSize.toString());
localStorage.setItem('finetuneCircularityStepSize', this._circularityStepSize.toString());
Storage.finetuneCenterStepSize.set(this._centerStepSize);
Storage.finetuneCircularityStepSize.set(this._circularityStepSize);
}
/**
* Restore step size from localStorage
* Restore step size from storage
*/
_restoreStepSizeFromLocalStorage() {
// Restore center step size
const savedCenterStepSize = localStorage.getItem('finetuneCenterStepSize');
const savedCenterStepSize = Storage.finetuneCenterStepSize.get();
if (savedCenterStepSize) {
this._centerStepSize = parseInt(savedCenterStepSize);
}
// Restore circularity step size
const savedCircularityStepSize = localStorage.getItem('finetuneCircularityStepSize');
const savedCircularityStepSize = Storage.finetuneCircularityStepSize.get();
if (savedCircularityStepSize) {
this._circularityStepSize = parseInt(savedCircularityStepSize);
}

View File

@@ -1,7 +1,8 @@
'use strict';
import { l } from '../translations.js';
import { la } from '../utils.js'
import { la } from '../utils.js';
import { Storage } from '../storage.js';
const TEST_SEQUENCE = ['usb', 'buttons', 'adaptive', 'haptic', 'lights', 'speaker', 'headphone', 'microphone'];
const TEST_NAMES = {
@@ -98,32 +99,28 @@ export class QuickTestModal {
}
/**
* Save skipped tests to localStorage
* Save skipped tests to storage
*/
_saveSkippedTestsToStorage() {
try {
localStorage.setItem('quickTestSkippedTests', JSON.stringify(this.state.skippedTests));
Storage.quickTestSkippedTests.set(this.state.skippedTests);
} catch (error) {
console.warn('Failed to save skipped tests to localStorage:', error);
console.warn('Failed to save skipped tests to storage:', error);
}
}
/**
* Load skipped tests from localStorage
* Load skipped tests from storage
*/
_loadSkippedTestsFromStorage() {
try {
const saved = localStorage.getItem('quickTestSkippedTests');
if (saved) {
const skippedTests = JSON.parse(saved);
if (Array.isArray(skippedTests)) {
this.state.skippedTests = skippedTests.filter(test => TEST_SEQUENCE.includes(test));
// Apply the skipped tests to the UI
this._applySkippedTestsToUI();
}
const skippedTests = Storage.quickTestSkippedTests.get();
if (Array.isArray(skippedTests) && skippedTests.length > 0) {
this.state.skippedTests = skippedTests.filter(test => TEST_SEQUENCE.includes(test));
this._applySkippedTestsToUI();
}
} catch (error) {
console.warn('Failed to load skipped tests from localStorage:', error);
console.warn('Failed to load skipped tests from storage:', error);
this.state.skippedTests = [];
}
}
@@ -381,13 +378,13 @@ export class QuickTestModal {
}
/**
* Clear saved skipped tests from localStorage
* Clear saved skipped tests from storage
*/
_clearSkippedTestsFromStorage() {
try {
localStorage.removeItem('quickTestSkippedTests');
Storage.quickTestSkippedTests.clear();
} catch (error) {
console.warn('Failed to clear skipped tests from localStorage:', error);
console.warn('Failed to clear skipped tests from storage:', error);
}
}
@@ -1104,7 +1101,7 @@ export class QuickTestModal {
this.state.skippedTests.push(testType);
}
// Save to localStorage
// Save to storage
this._saveSkippedTestsToStorage();
// Stop any ongoing test activities
@@ -1520,7 +1517,7 @@ export class QuickTestModal {
// Reset state
this._initializeState();
// Load saved skipped tests from localStorage
// Load saved skipped tests from storage
this._loadSkippedTestsFromStorage();
// Reset button colors to initial state

245
js/storage.js Normal file
View File

@@ -0,0 +1,245 @@
'use strict';
export const Storage = {
STORAGE_KEYS: {
LAST_CONNECTED_CONTROLLER: 'lastConnectedController',
EDGE_MODAL_DONT_SHOW_AGAIN: 'edgeModalDontShowAgain',
FAILED_CALIBRATION_COUNT: 'failedCalibrationCount',
CENTER_CALIBRATION_METHOD: 'centerCalibrationMethod',
RANGE_CALIBRATION_METHOD: 'rangeCalibrationMethod',
QUICK_TEST_SKIPPED_TESTS: 'quickTestSkippedTests',
SHOW_RAW_NUMBERS_CHECKBOX: 'showRawNumbersCheckbox',
FINETUNE_CENTER_STEP_SIZE: 'finetuneCenterStepSize',
FINETUNE_CIRCULARITY_STEP_SIZE: 'finetuneCircularityStepSize',
FINETUNE_HISTORY: 'finetuneHistory',
},
getChangesStorageKey(serialNumber) {
if (!serialNumber) return null;
return `changes_${serialNumber}`;
},
setString(key, value) {
try {
localStorage.setItem(key, value);
} catch (e) {
console.warn(`Failed to save to localStorage (${key}):`, e);
}
},
getString(key) {
try {
return localStorage.getItem(key);
} catch (e) {
console.warn(`Failed to read from localStorage (${key}):`, e);
return null;
}
},
setObject(key, value) {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch (e) {
console.warn(`Failed to save object to localStorage (${key}):`, e);
}
},
getObject(key) {
try {
const value = localStorage.getItem(key);
return value ? JSON.parse(value) : null;
} catch (e) {
console.warn(`Failed to read object from localStorage (${key}):`, e);
return null;
}
},
removeItem(key) {
try {
localStorage.removeItem(key);
} catch (e) {
console.warn(`Failed to remove from localStorage (${key}):`, e);
}
},
setBoolean(key, value) {
this.setString(key, value.toString());
},
getBoolean(key, defaultValue = false) {
const value = this.getString(key);
return value !== null ? value === 'true' : defaultValue;
},
setNumber(key, value) {
this.setString(key, value.toString());
},
getNumber(key, defaultValue = 0) {
const value = this.getString(key);
return value !== null ? parseInt(value, 10) : defaultValue;
},
lastConnectedController: {
set(info) {
Storage.setObject(Storage.STORAGE_KEYS.LAST_CONNECTED_CONTROLLER, info);
},
get() {
return Storage.getObject(Storage.STORAGE_KEYS.LAST_CONNECTED_CONTROLLER);
},
clear() {
Storage.removeItem(Storage.STORAGE_KEYS.LAST_CONNECTED_CONTROLLER);
},
},
edgeModalDontShowAgain: {
set(value) {
Storage.setBoolean(Storage.STORAGE_KEYS.EDGE_MODAL_DONT_SHOW_AGAIN, value);
},
get() {
return Storage.getBoolean(Storage.STORAGE_KEYS.EDGE_MODAL_DONT_SHOW_AGAIN);
},
clear() {
Storage.removeItem(Storage.STORAGE_KEYS.EDGE_MODAL_DONT_SHOW_AGAIN);
},
},
failedCalibrationCount: {
set(count) {
Storage.setNumber(Storage.STORAGE_KEYS.FAILED_CALIBRATION_COUNT, count);
},
get() {
return Storage.getNumber(Storage.STORAGE_KEYS.FAILED_CALIBRATION_COUNT, 0);
},
clear() {
Storage.removeItem(Storage.STORAGE_KEYS.FAILED_CALIBRATION_COUNT);
},
},
centerCalibrationMethod: {
set(method) {
Storage.setString(Storage.STORAGE_KEYS.CENTER_CALIBRATION_METHOD, method);
},
get(defaultValue = 'four-step') {
return Storage.getString(Storage.STORAGE_KEYS.CENTER_CALIBRATION_METHOD) || defaultValue;
},
clear() {
Storage.removeItem(Storage.STORAGE_KEYS.CENTER_CALIBRATION_METHOD);
},
},
rangeCalibrationMethod: {
set(method) {
Storage.setString(Storage.STORAGE_KEYS.RANGE_CALIBRATION_METHOD, method);
},
get(defaultValue = 'normal') {
return Storage.getString(Storage.STORAGE_KEYS.RANGE_CALIBRATION_METHOD) || defaultValue;
},
clear() {
Storage.removeItem(Storage.STORAGE_KEYS.RANGE_CALIBRATION_METHOD);
},
},
quickTestSkippedTests: {
set(tests) {
Storage.setObject(Storage.STORAGE_KEYS.QUICK_TEST_SKIPPED_TESTS, tests);
},
get() {
return Storage.getObject(Storage.STORAGE_KEYS.QUICK_TEST_SKIPPED_TESTS) || [];
},
clear() {
Storage.removeItem(Storage.STORAGE_KEYS.QUICK_TEST_SKIPPED_TESTS);
},
},
showRawNumbersCheckbox: {
set(value) {
Storage.setString(Storage.STORAGE_KEYS.SHOW_RAW_NUMBERS_CHECKBOX, value.toString());
},
get() {
const value = Storage.getString(Storage.STORAGE_KEYS.SHOW_RAW_NUMBERS_CHECKBOX);
return value === 'true';
},
clear() {
Storage.removeItem(Storage.STORAGE_KEYS.SHOW_RAW_NUMBERS_CHECKBOX);
},
},
finetuneCenterStepSize: {
set(value) {
Storage.setString(Storage.STORAGE_KEYS.FINETUNE_CENTER_STEP_SIZE, value.toString());
},
get() {
return Storage.getString(Storage.STORAGE_KEYS.FINETUNE_CENTER_STEP_SIZE);
},
clear() {
Storage.removeItem(Storage.STORAGE_KEYS.FINETUNE_CENTER_STEP_SIZE);
},
},
finetuneCircularityStepSize: {
set(value) {
Storage.setString(Storage.STORAGE_KEYS.FINETUNE_CIRCULARITY_STEP_SIZE, value.toString());
},
get() {
return Storage.getString(Storage.STORAGE_KEYS.FINETUNE_CIRCULARITY_STEP_SIZE);
},
clear() {
Storage.removeItem(Storage.STORAGE_KEYS.FINETUNE_CIRCULARITY_STEP_SIZE);
},
},
hasChangesState: {
set(serialNumber, hasChanges) {
const key = Storage.getChangesStorageKey(serialNumber);
if (key) {
Storage.setObject(key, hasChanges);
}
},
get(serialNumber) {
const key = Storage.getChangesStorageKey(serialNumber);
if (!key) return false;
return Storage.getObject(key) || false;
},
clear(serialNumber) {
const key = Storage.getChangesStorageKey(serialNumber);
if (key) {
Storage.removeItem(key);
}
},
},
finetuneHistory: {
getAll() {
return Storage.getObject(Storage.STORAGE_KEYS.FINETUNE_HISTORY) || {};
},
setAll(history) {
Storage.setObject(Storage.STORAGE_KEYS.FINETUNE_HISTORY, history);
},
clear() {
Storage.removeItem(Storage.STORAGE_KEYS.FINETUNE_HISTORY);
},
},
};

View File

@@ -1,6 +1,7 @@
'use strict';
import { la, createCookie, readCookie } from './utils.js';
import { la } from './utils.js';
import { Storage } from './storage.js';
// Alphabetical order
const available_langs = {
@@ -50,7 +51,7 @@ export function lang_init(appState, handleLanguageChangeCb, welcomeModalCb) {
}
translationState.lang_orig_text[".title"] = document.title;
const force_lang = readCookie("force_lang");
const force_lang = Storage.getString("force_lang");
if (force_lang != null) {
lang_set(force_lang, true).catch(error => {
console.error("Failed to set forced language:", error);
@@ -89,9 +90,9 @@ async function lang_set(lang, skip_modal=false) {
}
await handleLanguageChange(lang);
createCookie("force_lang", lang);
Storage.setString("force_lang", lang);
if(!skip_modal && welcomeModal) {
createCookie("welcome_accepted", "0");
Storage.setString("welcome_accepted", "0");
welcomeModal();
}
}

View File

@@ -1,5 +1,7 @@
'use strict';
import { Storage } from './storage.js';
/**
* Utility functions for DualShock controller operations
*/
@@ -117,49 +119,12 @@ export function lerp_color(a, b, t) {
return rgb2hex(c[0], c[1], c[2]);
}
/**
* Create a cookie with specified name, value, and expiration days
* @param {string} name Cookie name
* @param {string} value Cookie value
* @param {number} days Number of days until expiration
*/
export function createCookie(name, value, days) {
const expires = days ? "; expires=" + new Date(Date.now() + days * 24 * 60 * 60 * 1000).toGMTString() : "";
document.cookie = encodeURIComponent(name) + "=" + encodeURIComponent(value) + expires + "; path=/";
}
/**
* Read a cookie value by name
* @param {string} name Cookie name
* @returns {string|null} Cookie value or null if not found
*/
export function readCookie(name) {
const nameEQ = encodeURIComponent(name) + "=";
const ca = document.cookie.split(';');
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) === ' ')
c = c.substring(1, c.length);
if (c.indexOf(nameEQ) === 0)
return decodeURIComponent(c.substring(nameEQ.length, c.length));
}
return null;
}
/**
* Delete a cookie by setting its expiration to the past
* @param {string} name Cookie name to delete
*/
export function eraseCookie(name) {
createCookie(name, "", -1);
}
/**
* Get the appropriate locale for date formatting based on language and timezone
* @returns {string} Locale string for use with toLocaleString()
*/
export function getLocaleForDateFormatting() {
let lang = readCookie('force_lang') || navigator.language;
let lang = Storage.getString('force_lang') || navigator.language;
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
// Replace en_US with en_UK if timezone does not start with "America"