mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
guh
This commit is contained in:
@@ -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 };
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export const SETTINGS_COOKIE = 'roverSettings';
|
||||
export const SETTINGS_MAX_AGE = 60 * 60 * 24 * 365; // 1 year
|
||||
@@ -0,0 +1 @@
|
||||
export { SettingsProvider, useSettings, useSettingsNamespace } from './SettingsProvider.jsx';
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user