Added a saved settings system that uses localstorage and versioning
This commit is contained in:
parent
e702936c44
commit
602631f9b1
@ -16,6 +16,8 @@
|
||||
"fake-indexeddb": "^3.0.0",
|
||||
"interactjs": "^1.9.7",
|
||||
"konva": "^6.0.0",
|
||||
"lodash.get": "^4.4.2",
|
||||
"lodash.set": "^4.3.2",
|
||||
"polygon-clipping": "^0.14.3",
|
||||
"raw.macro": "^0.3.0",
|
||||
"react": "^16.13.0",
|
||||
|
63
src/App.js
63
src/App.js
@ -15,41 +15,44 @@ import { DatabaseProvider } from "./contexts/DatabaseContext";
|
||||
import { MapDataProvider } from "./contexts/MapDataContext";
|
||||
import { TokenDataProvider } from "./contexts/TokenDataContext";
|
||||
import { MapLoadingProvider } from "./contexts/MapLoadingContext";
|
||||
import { SettingsProvider } from "./contexts/SettingsContext.js";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<DatabaseProvider>
|
||||
<AuthProvider>
|
||||
<Router>
|
||||
<Switch>
|
||||
<Route path="/howTo">
|
||||
<HowTo />
|
||||
</Route>
|
||||
<Route path="/releaseNotes">
|
||||
<ReleaseNotes />
|
||||
</Route>
|
||||
<Route path="/about">
|
||||
<About />
|
||||
</Route>
|
||||
<Route path="/faq">
|
||||
<FAQ />
|
||||
</Route>
|
||||
<Route path="/game/:id">
|
||||
<MapLoadingProvider>
|
||||
<MapDataProvider>
|
||||
<TokenDataProvider>
|
||||
<Game />
|
||||
</TokenDataProvider>
|
||||
</MapDataProvider>
|
||||
</MapLoadingProvider>
|
||||
</Route>
|
||||
<Route path="/">
|
||||
<Home />
|
||||
</Route>
|
||||
</Switch>
|
||||
</Router>
|
||||
</AuthProvider>
|
||||
<SettingsProvider>
|
||||
<AuthProvider>
|
||||
<Router>
|
||||
<Switch>
|
||||
<Route path="/howTo">
|
||||
<HowTo />
|
||||
</Route>
|
||||
<Route path="/releaseNotes">
|
||||
<ReleaseNotes />
|
||||
</Route>
|
||||
<Route path="/about">
|
||||
<About />
|
||||
</Route>
|
||||
<Route path="/faq">
|
||||
<FAQ />
|
||||
</Route>
|
||||
<Route path="/game/:id">
|
||||
<MapLoadingProvider>
|
||||
<MapDataProvider>
|
||||
<TokenDataProvider>
|
||||
<Game />
|
||||
</TokenDataProvider>
|
||||
</MapDataProvider>
|
||||
</MapLoadingProvider>
|
||||
</Route>
|
||||
<Route path="/">
|
||||
<Home />
|
||||
</Route>
|
||||
</Switch>
|
||||
</Router>
|
||||
</AuthProvider>
|
||||
</SettingsProvider>
|
||||
</DatabaseProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
@ -19,6 +19,7 @@ import SelectDiceButton from "./SelectDiceButton";
|
||||
import Divider from "../Divider";
|
||||
|
||||
import { dice } from "../../dice";
|
||||
import useSetting from "../../helpers/useSetting";
|
||||
|
||||
function DiceButtons({
|
||||
diceRolls,
|
||||
@ -29,10 +30,13 @@ function DiceButtons({
|
||||
shareDice,
|
||||
onShareDiceChange,
|
||||
}) {
|
||||
const [currentDice, setCurrentDice] = useState(dice[0]);
|
||||
const [currentDiceStyle, setCurrentDiceStyle] = useSetting("dice.style");
|
||||
const [currentDice, setCurrentDice] = useState(
|
||||
dice.find((d) => d.key === currentDiceStyle)
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const initialDice = dice[0];
|
||||
const initialDice = dice.find((d) => d.key === currentDiceStyle);
|
||||
onDiceLoad(initialDice);
|
||||
setCurrentDice(initialDice);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@ -50,6 +54,7 @@ function DiceButtons({
|
||||
async function handleDiceChange(dice) {
|
||||
await onDiceLoad(dice);
|
||||
setCurrentDice(dice);
|
||||
setCurrentDiceStyle(dice.key);
|
||||
}
|
||||
|
||||
let buttons = [
|
||||
|
@ -12,6 +12,7 @@ import MapLoadingOverlay from "./MapLoadingOverlay";
|
||||
import NetworkedMapPointer from "../../network/NetworkedMapPointer";
|
||||
|
||||
import TokenDataContext from "../../contexts/TokenDataContext";
|
||||
import SettingsContext from "../../contexts/SettingsContext";
|
||||
|
||||
import TokenMenu from "../token/TokenMenu";
|
||||
import TokenDragOverlay from "../token/TokenDragOverlay";
|
||||
@ -48,31 +49,10 @@ function Map({
|
||||
const tokenSizePercent = gridSizeNormalized.x;
|
||||
|
||||
const [selectedToolId, setSelectedToolId] = useState("pan");
|
||||
const [toolSettings, setToolSettings] = useState({
|
||||
fog: {
|
||||
type: "polygon",
|
||||
useEdgeSnapping: false,
|
||||
useFogCut: false,
|
||||
preview: false,
|
||||
},
|
||||
drawing: {
|
||||
color: "red",
|
||||
type: "brush",
|
||||
useBlending: true,
|
||||
},
|
||||
measure: {
|
||||
type: "chebyshev",
|
||||
scale: "5ft",
|
||||
},
|
||||
timer: {
|
||||
hour: 0,
|
||||
minute: 0,
|
||||
second: 0,
|
||||
},
|
||||
});
|
||||
const { settings, setSettings } = useContext(SettingsContext);
|
||||
|
||||
function handleToolSettingChange(tool, change) {
|
||||
setToolSettings((prevSettings) => ({
|
||||
setSettings((prevSettings) => ({
|
||||
...prevSettings,
|
||||
[tool]: {
|
||||
...prevSettings[tool],
|
||||
@ -191,7 +171,7 @@ function Map({
|
||||
currentMapState={mapState}
|
||||
onSelectedToolChange={setSelectedToolId}
|
||||
selectedToolId={selectedToolId}
|
||||
toolSettings={toolSettings}
|
||||
toolSettings={settings}
|
||||
onToolSettingChange={handleToolSettingChange}
|
||||
onToolAction={handleToolAction}
|
||||
disabledControls={disabledControls}
|
||||
@ -291,7 +271,7 @@ function Map({
|
||||
onShapesRemove={handleMapShapesRemove}
|
||||
active={selectedToolId === "drawing"}
|
||||
toolId="drawing"
|
||||
toolSettings={toolSettings.drawing}
|
||||
toolSettings={settings.drawing}
|
||||
gridSize={gridSizeNormalized}
|
||||
/>
|
||||
);
|
||||
@ -306,9 +286,9 @@ function Map({
|
||||
onShapesEdit={handleFogShapesEdit}
|
||||
active={selectedToolId === "fog"}
|
||||
toolId="fog"
|
||||
toolSettings={toolSettings.fog}
|
||||
toolSettings={settings.fog}
|
||||
gridSize={gridSizeNormalized}
|
||||
transparent={allowFogDrawing && !toolSettings.fog.preview}
|
||||
transparent={allowFogDrawing && !settings.fog.preview}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -321,7 +301,7 @@ function Map({
|
||||
map={map}
|
||||
active={selectedToolId === "measure"}
|
||||
gridSize={gridSizeNormalized}
|
||||
selectedToolSettings={toolSettings[selectedToolId]}
|
||||
selectedToolSettings={settings[selectedToolId]}
|
||||
/>
|
||||
);
|
||||
|
||||
|
31
src/contexts/SettingsContext.js
Normal file
31
src/contexts/SettingsContext.js
Normal file
@ -0,0 +1,31 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
|
||||
import { getSettings } from "../settings";
|
||||
|
||||
const SettingsContext = React.createContext({
|
||||
settings: {},
|
||||
setSettings: () => {},
|
||||
});
|
||||
|
||||
const settingsProvider = getSettings();
|
||||
|
||||
export function SettingsProvider({ children }) {
|
||||
const [settings, setSettings] = useState(settingsProvider.getAll());
|
||||
|
||||
useEffect(() => {
|
||||
settingsProvider.setAll(settings);
|
||||
}, [settings]);
|
||||
|
||||
const value = {
|
||||
settings,
|
||||
setSettings,
|
||||
};
|
||||
|
||||
return (
|
||||
<SettingsContext.Provider value={value}>
|
||||
{children}
|
||||
</SettingsContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export default SettingsContext;
|
43
src/helpers/Settings.js
Normal file
43
src/helpers/Settings.js
Normal file
@ -0,0 +1,43 @@
|
||||
/**
|
||||
* An interface to a local storage back settings store with a versioning mechanism
|
||||
*/
|
||||
class Settings {
|
||||
name;
|
||||
currentVersion;
|
||||
|
||||
constructor(name) {
|
||||
this.name = name;
|
||||
this.currentVersion = this.get("__version");
|
||||
}
|
||||
|
||||
version(versionNumber, upgradeFunction) {
|
||||
if (versionNumber > this.currentVersion) {
|
||||
this.currentVersion = versionNumber;
|
||||
this.setAll(upgradeFunction(this.getAll()));
|
||||
}
|
||||
}
|
||||
|
||||
getAll() {
|
||||
return JSON.parse(localStorage.getItem(this.name));
|
||||
}
|
||||
|
||||
get(key) {
|
||||
const settings = this.getAll();
|
||||
return settings && settings[key];
|
||||
}
|
||||
|
||||
setAll(newSettings) {
|
||||
localStorage.setItem(
|
||||
this.name,
|
||||
JSON.stringify({ ...newSettings, __version: this.currentVersion })
|
||||
);
|
||||
}
|
||||
|
||||
set(key, value) {
|
||||
let settings = this.getAll();
|
||||
settings[key] = value;
|
||||
this.setAll(settings);
|
||||
}
|
||||
}
|
||||
|
||||
export default Settings;
|
26
src/helpers/useSetting.js
Normal file
26
src/helpers/useSetting.js
Normal file
@ -0,0 +1,26 @@
|
||||
import { useContext } from "react";
|
||||
|
||||
import get from "lodash.get";
|
||||
import set from "lodash.set";
|
||||
|
||||
import SettingsContext from "../contexts/SettingsContext";
|
||||
|
||||
/**
|
||||
* Helper to get and set nested settings that are saved in local storage
|
||||
* @param {String} path The path to the setting within the Settings object provided by the SettingsContext
|
||||
*/
|
||||
function useSetting(path) {
|
||||
const { settings, setSettings } = useContext(SettingsContext);
|
||||
|
||||
const setting = get(settings, path);
|
||||
|
||||
const setSetting = (value) =>
|
||||
setSettings((prev) => {
|
||||
const updated = set({ ...prev }, path, value);
|
||||
return updated;
|
||||
});
|
||||
|
||||
return [setting, setSetting];
|
||||
}
|
||||
|
||||
export default useSetting;
|
@ -1,10 +1,12 @@
|
||||
import React, { useState, useRef } from "react";
|
||||
import React, { useRef } from "react";
|
||||
import { Box, Label, Input, Button, Flex, Text } from "theme-ui";
|
||||
|
||||
import Modal from "../components/Modal";
|
||||
|
||||
import { getHMSDuration, getDurationHMS } from "../helpers/timer";
|
||||
|
||||
import useSetting from "../helpers/useSetting";
|
||||
|
||||
function StartTimerModal({
|
||||
isOpen,
|
||||
onRequestClose,
|
||||
@ -17,9 +19,9 @@ function StartTimerModal({
|
||||
inputRef.current && inputRef.current.focus();
|
||||
}
|
||||
|
||||
const [hour, setHour] = useState(0);
|
||||
const [minute, setMinute] = useState(0);
|
||||
const [second, setSecond] = useState(0);
|
||||
const [hour, setHour] = useSetting("timer.hour");
|
||||
const [minute, setMinute] = useSetting("timer.minute");
|
||||
const [second, setSecond] = useSetting("timer.second");
|
||||
|
||||
function handleSubmit(event) {
|
||||
event.preventDefault();
|
||||
|
@ -6,6 +6,7 @@ import Session from "../helpers/Session";
|
||||
import { isStreamStopped, omit, fromEntries } from "../helpers/shared";
|
||||
|
||||
import AuthContext from "../contexts/AuthContext";
|
||||
import useSetting from "../helpers/useSetting";
|
||||
|
||||
import Party from "../components/party/Party";
|
||||
|
||||
@ -26,9 +27,10 @@ function NetworkedParty({ gameId, session }) {
|
||||
const [timer, setTimer] = useState(null);
|
||||
const [partyTimers, setPartyTimers] = useState({});
|
||||
const [diceRolls, setDiceRolls] = useState([]);
|
||||
const [shareDice, setShareDice] = useState(false);
|
||||
const [partyDiceRolls, setPartyDiceRolls] = useState({});
|
||||
|
||||
const [shareDice, setShareDice] = useSetting("dice.shareDice");
|
||||
|
||||
function handleNicknameChange(newNickname) {
|
||||
setNickname(newNickname);
|
||||
session.send("nickname", { [session.id]: newNickname });
|
||||
|
36
src/settings.js
Normal file
36
src/settings.js
Normal file
@ -0,0 +1,36 @@
|
||||
import Settings from "./helpers/Settings";
|
||||
|
||||
function loadVersions(settings) {
|
||||
settings.version(1, () => ({
|
||||
fog: {
|
||||
type: "polygon",
|
||||
useEdgeSnapping: false,
|
||||
useFogCut: false,
|
||||
preview: false,
|
||||
},
|
||||
drawing: {
|
||||
color: "red",
|
||||
type: "brush",
|
||||
useBlending: true,
|
||||
},
|
||||
measure: {
|
||||
type: "chebyshev",
|
||||
scale: "5ft",
|
||||
},
|
||||
timer: {
|
||||
hour: 0,
|
||||
minute: 0,
|
||||
second: 0,
|
||||
},
|
||||
dice: {
|
||||
shareDice: false,
|
||||
style: "galaxy",
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
export function getSettings() {
|
||||
let settings = new Settings("OwlbearRodeoSettings");
|
||||
loadVersions(settings);
|
||||
return settings;
|
||||
}
|
10
yarn.lock
10
yarn.lock
@ -7061,11 +7061,21 @@ lodash.debounce@^4.0.8:
|
||||
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
|
||||
integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168=
|
||||
|
||||
lodash.get@^4.4.2:
|
||||
version "4.4.2"
|
||||
resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
|
||||
integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=
|
||||
|
||||
lodash.memoize@^4.1.2:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
|
||||
integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=
|
||||
|
||||
lodash.set@^4.3.2:
|
||||
version "4.3.2"
|
||||
resolved "https://registry.yarnpkg.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23"
|
||||
integrity sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=
|
||||
|
||||
lodash.sortby@^4.7.0:
|
||||
version "4.7.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
|
||||
|
Loading…
x
Reference in New Issue
Block a user