This commit is contained in:
legop3
2025-11-18 03:15:30 -05:00
parent 5c66bbd809
commit 98b61a7845
13 changed files with 294 additions and 136 deletions
+94
View File
@@ -0,0 +1,94 @@
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { loadSettings, saveSettings } from './persistence.js';
const SettingsContext = createContext(null);
export function SettingsProvider({ children }) {
const [state, setState] = useState(() => ({ status: 'loading', data: {} }));
useEffect(() => {
const loaded = loadSettings();
setState({ status: 'ready', data: loaded ?? {} });
}, []);
const reload = useCallback(() => {
const loaded = loadSettings();
setState({ status: 'ready', data: loaded ?? {} });
}, []);
const saveAll = useCallback((nextData) => {
const success = saveSettings(nextData ?? {});
if (success) {
setState({ status: 'ready', data: nextData ?? {} });
} else {
setState((prev) => ({ ...prev, status: 'error' }));
}
return success;
}, []);
const setNamespace = useCallback((namespace, updater) => {
setState((prev) => {
const current = prev.data[namespace] || {};
const nextValue = typeof updater === 'function' ? updater(current) : { ...current, ...(updater ?? {}) };
const nextData = { ...prev.data, [namespace]: nextValue };
const success = saveSettings(nextData);
if (!success) {
return { ...prev, status: 'error' };
}
return { status: 'ready', data: nextData };
});
}, []);
const contextValue = useMemo(
() => ({
data: state.data,
status: state.status,
reload,
saveAll,
setNamespace,
}),
[state, reload, saveAll, setNamespace],
);
return <SettingsContext.Provider value={contextValue}>{children}</SettingsContext.Provider>;
}
export function useSettings() {
const ctx = useContext(SettingsContext);
if (!ctx) {
throw new Error('useSettings must be used within SettingsProvider');
}
return ctx;
}
export function useSettingsNamespace(namespace, defaults = {}) {
const { data, status, setNamespace } = useSettings();
const value = data[namespace] ?? defaults ?? {};
const save = useCallback(
(update) => {
setNamespace(namespace, (current) => {
const base = current ?? {};
if (typeof update === 'function') {
return update(base);
}
return { ...base, ...(update ?? {}) };
});
},
[namespace, setNamespace],
);
const replace = useCallback(
(nextValue) => {
setNamespace(namespace, () => nextValue ?? {});
},
[namespace, setNamespace],
);
const reset = useCallback(() => {
const base = typeof defaults === 'object' ? { ...defaults } : defaults;
setNamespace(namespace, () => base ?? {});
}, [defaults, namespace, setNamespace]);
return { value, status, save, replace, reset };
}
+2
View File
@@ -0,0 +1,2 @@
export const SETTINGS_COOKIE = 'roverSettings';
export const SETTINGS_MAX_AGE = 60 * 60 * 24 * 365; // 1 year
+1
View File
@@ -0,0 +1 @@
export { SettingsProvider, useSettings, useSettingsNamespace } from './SettingsProvider.jsx';
+12
View File
@@ -0,0 +1,12 @@
export const INPUT_SETTINGS_DEFAULTS = {
gamepad: {
driveDeadzone: 0.2,
cameraDeadzone: 0.25,
servoStep: 2,
auxReverseScale: 0.55,
},
mobile: {
joystickRadius: 80,
joystickSmoothing: 0.15,
},
};
+36
View File
@@ -0,0 +1,36 @@
import { SETTINGS_COOKIE, SETTINGS_MAX_AGE } from './constants.js';
function parseCookieValue(raw) {
try {
const decoded = decodeURIComponent(raw ?? '');
return JSON.parse(decoded);
} catch (error) {
console.warn('Failed to parse settings cookie', error); // eslint-disable-line no-console
return null;
}
}
export function loadSettings() {
if (typeof document === 'undefined') return {};
const cookiePrefix = `${SETTINGS_COOKIE}=`;
const entry = document.cookie
.split(';')
.map((part) => part.trim())
.find((part) => part.startsWith(cookiePrefix));
if (!entry) return {};
const raw = entry.substring(cookiePrefix.length);
return parseCookieValue(raw) ?? {};
}
export function saveSettings(settings) {
if (typeof document === 'undefined') return false;
try {
const serialized = encodeURIComponent(JSON.stringify(settings ?? {}));
const cookie = `${SETTINGS_COOKIE}=${serialized}; path=/; max-age=${SETTINGS_MAX_AGE}; samesite=strict`;
document.cookie = cookie;
return true;
} catch (error) {
console.warn('Failed to write settings cookie', error); // eslint-disable-line no-console
return false;
}
}