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"