Added a saved settings system that uses localstorage and versioning

This commit is contained in:
Mitchell McCaffrey 2020-08-07 17:15:16 +10:00
parent e702936c44
commit 602631f9b1
11 changed files with 205 additions and 65 deletions

View File

@ -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",

View File

@ -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>
);

View File

@ -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 = [

View File

@ -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]}
/>
);

View 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
View 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
View 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;

View File

@ -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();

View File

@ -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
View 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;
}

View File

@ -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"