diff --git a/package.json b/package.json
index e2171b0..f00ca0b 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/App.js b/src/App.js
index 80455d7..d6bd778 100644
--- a/src/App.js
+++ b/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 (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
);
diff --git a/src/components/dice/DiceButtons.js b/src/components/dice/DiceButtons.js
index 2820aae..dc89e1c 100644
--- a/src/components/dice/DiceButtons.js
+++ b/src/components/dice/DiceButtons.js
@@ -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 = [
diff --git a/src/components/map/Map.js b/src/components/map/Map.js
index fa0c28a..ae09b02 100644
--- a/src/components/map/Map.js
+++ b/src/components/map/Map.js
@@ -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]}
/>
);
diff --git a/src/contexts/SettingsContext.js b/src/contexts/SettingsContext.js
new file mode 100644
index 0000000..b396577
--- /dev/null
+++ b/src/contexts/SettingsContext.js
@@ -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 (
+
+ {children}
+
+ );
+}
+
+export default SettingsContext;
diff --git a/src/helpers/Settings.js b/src/helpers/Settings.js
new file mode 100644
index 0000000..d9ac9da
--- /dev/null
+++ b/src/helpers/Settings.js
@@ -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;
diff --git a/src/helpers/useSetting.js b/src/helpers/useSetting.js
new file mode 100644
index 0000000..ca38152
--- /dev/null
+++ b/src/helpers/useSetting.js
@@ -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;
diff --git a/src/modals/StartTimerModal.js b/src/modals/StartTimerModal.js
index 230e7ad..b8481d6 100644
--- a/src/modals/StartTimerModal.js
+++ b/src/modals/StartTimerModal.js
@@ -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();
diff --git a/src/network/NetworkedParty.js b/src/network/NetworkedParty.js
index ad48594..3326b78 100644
--- a/src/network/NetworkedParty.js
+++ b/src/network/NetworkedParty.js
@@ -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 });
diff --git a/src/settings.js b/src/settings.js
new file mode 100644
index 0000000..214a00a
--- /dev/null
+++ b/src/settings.js
@@ -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;
+}
diff --git a/yarn.lock b/yarn.lock
index 3e2d932..5747944 100644
--- a/yarn.lock
+++ b/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"