diff --git a/.env.production b/.env.production index ce84652..4d18c3b 100644 --- a/.env.production +++ b/.env.production @@ -1,8 +1,9 @@ -REACT_APP_BROKER_URL=https://rocket.owlbear.rodeo -REACT_APP_ICE_SERVERS_URL=https://rocket.owlbear.rodeo/iceservers +REACT_APP_BROKER_URL=https://connect.owlbear.rodeo +REACT_APP_ICE_SERVERS_URL=https://connect.owlbear.rodeo/iceservers REACT_APP_STRIPE_API_KEY=pk_live_MJjzi5djj524Y7h3fL5PNh4e00a852XD51 REACT_APP_STRIPE_URL=https://payment.owlbear.rodeo REACT_APP_VERSION=$npm_package_version REACT_APP_PREVIEW=false REACT_APP_LOGGING=true -REACT_APP_FATHOM_SITE_ID=VMSHBPKD \ No newline at end of file +REACT_APP_FATHOM_SITE_ID=VMSHBPKD +REACT_APP_SENTRY_DSN=https://5257021c3a114649baa5e3b8ba775bfe@o467475.ingest.sentry.io/5493956 diff --git a/package.json b/package.json index 38ea871..5fa1a20 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,18 @@ { "name": "owlbear-rodeo", - "version": "1.8.0", + "version": "1.8.1", "private": true, "dependencies": { "@babylonjs/core": "^4.2.0", "@babylonjs/loaders": "^4.2.0", "@mitchemmc/dexie-export-import": "^1.0.1", - "@msgpack/msgpack": "^2.3.0", - "@sentry/react": "^5.27.1", - "@stripe/stripe-js": "^1.3.2", - "@tensorflow/tfjs": "^2.6.0", - "@testing-library/jest-dom": "^5.11.6", - "@testing-library/react": "^11.2.0", - "@testing-library/user-event": "^12.2.2", + "@msgpack/msgpack": "^2.4.1", + "@sentry/react": "^6.2.2", + "@stripe/stripe-js": "^1.13.1", + "@tensorflow/tfjs": "^3.3.0", + "@testing-library/jest-dom": "^5.11.9", + "@testing-library/react": "^11.2.5", + "@testing-library/user-event": "^13.0.2", "ammo.js": "kripken/ammo.js#aab297a4164779c3a9d8dc8d9da26958de3cb778", "case": "^1.6.3", "color": "^3.1.3", @@ -20,42 +20,44 @@ "deep-diff": "^1.0.2", "dexie": "^3.0.3", "dexie-observable": "^3.0.0-beta.10", - "err-code": "^2.0.3", + "err-code": "^3.0.1", "fake-indexeddb": "^3.1.2", "file-saver": "^2.0.5", - "fuse.js": "^6.4.1", - "interactjs": "^1.9.7", - "konva": "^7.1.8", + "fuse.js": "^6.4.6", + "interactjs": "^1.10.8", + "konva": "^7.2.5", "lodash.clonedeep": "^4.5.0", "lodash.get": "^4.4.2", "lodash.set": "^4.3.2", "normalize-wheel": "^1.0.1", - "polygon-clipping": "^0.15.1", - "pretty-bytes": "^5.4.1", + "pepjs": "^0.5.3", + "polygon-clipping": "^0.15.2", + "pretty-bytes": "^5.6.0", "raw.macro": "^0.4.2", "react": "^17.0.1", "react-dom": "^17.0.1", - "react-konva": "^17.0.0-0", + "react-konva": "^17.0.1-3", "react-markdown": "4", "react-media": "^2.0.0-rc.1", - "react-modal": "^3.11.2", + "react-modal": "^3.12.1", "react-resize-detector": "4.2.3", "react-router-dom": "^5.1.2", "react-router-hash-link": "^2.2.2", - "react-scripts": "4.0.0", - "react-select": "^3.1.0", + "react-scripts": "^4.0.3", + "react-select": "^4.2.1", "react-spring": "^8.0.27", - "react-toast-notifications": "^2.4.0", - "react-use-gesture": "^8.0.1", + "react-toast-notifications": "^2.4.3", + "react-use-gesture": "^9.1.3", "shortid": "^2.2.15", "simple-peer": "feross/simple-peer#694/head", "simplebar-react": "^2.1.0", "simplify-js": "^1.2.4", - "socket.io-client": "^3.0.3", - "source-map-explorer": "^2.4.2", + "socket.io-client": "^4.0.0", + "socket.io-msgpack-parser": "^3.0.1", + "source-map-explorer": "^2.5.2", "theme-ui": "^0.3.1", - "use-image": "^1.0.5", - "webrtc-adapter": "^7.5.1" + "use-image": "^1.0.7", + "webrtc-adapter": "^7.7.1" }, "resolutions": { "simple-peer/get-browser-rtc": "substack/get-browser-rtc#4/head" @@ -83,6 +85,6 @@ ] }, "devDependencies": { - "worker-loader": "^3.0.5" + "worker-loader": "^3.0.8" } } diff --git a/public/logo192.png b/public/logo192.png index fdd052d..41438c0 100755 Binary files a/public/logo192.png and b/public/logo192.png differ diff --git a/public/logo512.png b/public/logo512.png index 78c5dcb..450a04a 100755 Binary files a/public/logo512.png and b/public/logo512.png differ diff --git a/public/thumbnail.jpg b/public/thumbnail.jpg index e2f4282..df71469 100644 Binary files a/public/thumbnail.jpg and b/public/thumbnail.jpg differ diff --git a/src/App.js b/src/App.js index 7e1409d..a77ae2b 100644 --- a/src/App.js +++ b/src/App.js @@ -18,6 +18,7 @@ import { TokenDataProvider } from "./contexts/TokenDataContext"; import { MapLoadingProvider } from "./contexts/MapLoadingContext"; import { SettingsProvider } from "./contexts/SettingsContext"; import { KeyboardProvider } from "./contexts/KeyboardContext"; +import { ImageSourcesProvider } from "./contexts/ImageSourceContext"; import { ToastProvider } from "./components/Toast"; @@ -29,38 +30,40 @@ function App() { - - - - - - {/* Legacy support camel case routes */} - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + {/* Legacy support camel case routes */} + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/Grid.js b/src/components/Grid.js index c1a1230..8896cbb 100644 --- a/src/components/Grid.js +++ b/src/components/Grid.js @@ -4,7 +4,12 @@ import useImage from "use-image"; import Vector2 from "../helpers/Vector2"; -import { useGrid } from "../contexts/GridContext"; +import { + useGrid, + useGridPixelSize, + useGridOffset, + useGridCellPixelSize, +} from "../contexts/GridContext"; import squarePatternDark from "../images/SquarePatternDark.png"; import squarePatternLight from "../images/SquarePatternLight.png"; @@ -12,7 +17,11 @@ import hexPatternDark from "../images/HexPatternDark.png"; import hexPatternLight from "../images/HexPatternLight.png"; function Grid({ stroke }) { - const { grid, gridPixelSize, gridOffset, gridCellPixelSize } = useGrid(); + const grid = useGrid(); + const gridPixelSize = useGridPixelSize(); + const gridOffset = useGridOffset(); + const gridCellPixelSize = useGridCellPixelSize(); + let imageSource; if (grid.type === "square") { if (stroke === "black") { diff --git a/src/components/Tile.js b/src/components/Tile.js index 5e7714d..3640c37 100644 --- a/src/components/Tile.js +++ b/src/components/Tile.js @@ -55,11 +55,7 @@ function Tile({ e.stopPropagation(); onSelect(); }} - onDoubleClick={(e) => { - if (canEdit) { - onDoubleClick(e); - } - }} + onDoubleClick={onDoubleClick} > + + + Error: {error && error.message} + + + + ); +} + +export default ErrorBanner; diff --git a/src/components/banner/OfflineBanner.js b/src/components/banner/OfflineBanner.js new file mode 100644 index 0000000..d5d90ad --- /dev/null +++ b/src/components/banner/OfflineBanner.js @@ -0,0 +1,33 @@ +import React from "react"; +import { Flex } from "theme-ui"; + +import Banner from "./Banner"; +import OfflineIcon from "../../icons/OfflineIcon"; + +function OfflineBanner({ isOpen }) { + return ( + {}} + allowClose={false} + backgroundColor="transparent" + > + + + + + ); +} + +export default OfflineBanner; diff --git a/src/components/banner/ReconnectBanner.js b/src/components/banner/ReconnectBanner.js new file mode 100644 index 0000000..7fa917a --- /dev/null +++ b/src/components/banner/ReconnectBanner.js @@ -0,0 +1,33 @@ +import React from "react"; +import { Flex } from "theme-ui"; + +import Banner from "./Banner"; +import ReconnectingIcon from "../../icons/ReconnectingIcon"; + +function ReconnectBanner({ isOpen }) { + return ( + {}} + allowClose={false} + backgroundColor="transparent" + > + + + + + ); +} + +export default ReconnectBanner; diff --git a/src/components/dice/DiceInteraction.js b/src/components/dice/DiceInteraction.js index 19f709c..f5d225b 100644 --- a/src/components/dice/DiceInteraction.js +++ b/src/components/dice/DiceInteraction.js @@ -1,5 +1,4 @@ import React, { useRef, useEffect, useState } from "react"; -import { Box, Text } from "theme-ui"; import { Engine } from "@babylonjs/core/Engines/engine"; import { Scene } from "@babylonjs/core/scene"; import { Vector3, Color4, Matrix } from "@babylonjs/core/Maths/math"; @@ -19,7 +18,7 @@ import ReactResizeDetector from "react-resize-detector"; import usePreventTouch from "../../hooks/usePreventTouch"; -import Banner from "../Banner"; +import ErrorBanner from "../banner/ErrorBanner"; const diceThrowSpeed = 2; @@ -166,13 +165,7 @@ function DiceInteraction({ onSceneMount, onPointerDown, onPointerUp }) { style={{ outline: "none" }} /> - setError()}> - - - Error: {error && error.message} - - - + setError()} /> ); } diff --git a/src/components/dice/DiceTile.js b/src/components/dice/DiceTile.js index 8491c3b..101e390 100644 --- a/src/components/dice/DiceTile.js +++ b/src/components/dice/DiceTile.js @@ -2,7 +2,7 @@ import React from "react"; import Tile from "../Tile"; -function DiceTile({ dice, isSelected, onDiceSelect, onDone, large }) { +function DiceTile({ dice, isSelected, onDiceSelect, onDone, size }) { return ( onDiceSelect(dice)} onDoubleClick={() => onDone(dice)} - large={large} + size={size} /> ); } diff --git a/src/components/dice/DiceTiles.js b/src/components/dice/DiceTiles.js index 92cd598..86ef58a 100644 --- a/src/components/dice/DiceTiles.js +++ b/src/components/dice/DiceTiles.js @@ -1,21 +1,27 @@ import React from "react"; import { Flex } from "theme-ui"; import SimpleBar from "simplebar-react"; -import { useMedia } from "react-media"; import DiceTile from "./DiceTile"; +import useResponsiveLayout from "../../hooks/useResponsiveLayout"; + function DiceTiles({ dice, onDiceSelect, selectedDice, onDone }) { - const isSmallScreen = useMedia({ query: "(max-width: 500px)" }); + const layout = useResponsiveLayout(); return ( - + {dice.map((dice) => ( @@ -25,7 +31,7 @@ function DiceTiles({ dice, onDiceSelect, selectedDice, onDone }) { isSelected={selectedDice && dice.key === selectedDice.key} onDiceSelect={onDiceSelect} onDone={onDone} - large={isSmallScreen} + size={layout.tileSize} /> ))} diff --git a/src/components/map/Map.js b/src/components/map/Map.js index 19ad2be..340749e 100644 --- a/src/components/map/Map.js +++ b/src/components/map/Map.js @@ -209,7 +209,6 @@ function Map({ tokenGroup={tokenDraggingOptions && tokenDraggingOptions.tokenGroup} dragging={!!(tokenDraggingOptions && tokenDraggingOptions.dragging)} token={tokensById[tokenDraggingOptions.tokenState.tokenId]} - mapState={mapState} /> ); diff --git a/src/components/map/MapDrawing.js b/src/components/map/MapDrawing.js index 868ac7b..54e1234 100644 --- a/src/components/map/MapDrawing.js +++ b/src/components/map/MapDrawing.js @@ -2,9 +2,17 @@ import React, { useState, useEffect } from "react"; import shortid from "shortid"; import { Group, Line, Rect, Circle } from "react-konva"; -import { useMapInteraction } from "../../contexts/MapInteractionContext"; +import { + useDebouncedStageScale, + useMapWidth, + useMapHeight, + useInteractionEmitter, +} from "../../contexts/MapInteractionContext"; import { useMapStage } from "../../contexts/MapStageContext"; -import { useGrid } from "../../contexts/GridContext"; +import { + useGridCellNormalizedSize, + useGridStrokeWidth, +} from "../../contexts/GridContext"; import Vector2 from "../../helpers/Vector2"; import { @@ -25,13 +33,14 @@ function MapDrawing({ active, toolSettings, }) { - const { - stageScale, - mapWidth, - mapHeight, - interactionEmitter, - } = useMapInteraction(); - const { gridCellNormalizedSize, gridStrokeWidth } = useGrid(); + const stageScale = useDebouncedStageScale(); + const mapWidth = useMapWidth(); + const mapHeight = useMapHeight(); + const interactionEmitter = useInteractionEmitter(); + + const gridCellNormalizedSize = useGridCellNormalizedSize(); + const gridStrokeWidth = useGridStrokeWidth(); + const mapStageRef = useMapStage(); const [drawingShape, setDrawingShape] = useState(null); const [isBrushDown, setIsBrushDown] = useState(false); diff --git a/src/components/map/MapEditor.js b/src/components/map/MapEditor.js index 57ff6b8..f8dbb8a 100644 --- a/src/components/map/MapEditor.js +++ b/src/components/map/MapEditor.js @@ -1,4 +1,4 @@ -import React, { useState, useRef, useContext } from "react"; +import React, { useState, useRef } from "react"; import { Box, IconButton } from "theme-ui"; import { Stage, Layer, Image } from "react-konva"; import ReactResizeDetector from "react-resize-detector"; @@ -10,9 +10,9 @@ import useImageCenter from "../../hooks/useImageCenter"; import useResponsiveLayout from "../../hooks/useResponsiveLayout"; import { getGridDefaultInset, getGridMaxZoom } from "../../helpers/grid"; +import KonvaBridge from "../../helpers/KonvaBridge"; import { MapInteractionProvider } from "../../contexts/MapInteractionContext"; -import KeyboardContext from "../../contexts/KeyboardContext"; import { GridProvider } from "../../contexts/GridContext"; import ResetMapIcon from "../../icons/ResetMapIcon"; @@ -90,11 +90,9 @@ function MapEditor({ map, onSettingsChange }) { setPreventMapInteraction, mapWidth, mapHeight, + interactionEmitter: null, }; - // Get keyboard context to pass to Konva - const keyboardValue = useContext(KeyboardContext); - const canEditGrid = map.type !== "default"; const gridChanged = @@ -106,77 +104,90 @@ function MapEditor({ map, onSettingsChange }) { const layout = useResponsiveLayout(); return ( - - - + + - - - - + + ( + + {children} + + )} + > + + {showGridControls && canEditGrid && ( - + <> - + )} - - - - - - {gridChanged && ( - - - - )} - {canEditGrid && ( - setShowGridControls(!showGridControls)} - bg="overlay" - sx={{ - borderRadius: "50%", - position: "absolute", - bottom: 0, - right: 0, - }} - m={2} - p="6px" - > - {showGridControls ? : } - - )} - + + + + {gridChanged && ( + + + + )} + {canEditGrid && ( + setShowGridControls(!showGridControls)} + bg="overlay" + sx={{ + borderRadius: "50%", + position: "absolute", + bottom: 0, + right: 0, + }} + m={2} + p="6px" + > + {showGridControls ? : } + + )} + + + ); } diff --git a/src/components/map/MapFog.js b/src/components/map/MapFog.js index 41230fd..95527c2 100644 --- a/src/components/map/MapFog.js +++ b/src/components/map/MapFog.js @@ -5,9 +5,21 @@ import useImage from "use-image"; import diagonalPattern from "../../images/DiagonalPattern.png"; -import { useMapInteraction } from "../../contexts/MapInteractionContext"; +import { + useDebouncedStageScale, + useMapWidth, + useMapHeight, + useInteractionEmitter, +} from "../../contexts/MapInteractionContext"; import { useMapStage } from "../../contexts/MapStageContext"; -import { useGrid } from "../../contexts/GridContext"; +import { + useGrid, + useGridCellPixelSize, + useGridCellNormalizedSize, + useGridStrokeWidth, + useGridCellPixelOffset, + useGridOffset, +} from "../../contexts/GridContext"; import { useKeyboard } from "../../contexts/KeyboardContext"; import Vector2 from "../../helpers/Vector2"; @@ -30,6 +42,8 @@ import SubtractShapeAction from "../../actions/SubtractShapeAction"; import useSetting from "../../hooks/useSetting"; +import shortcuts from "../../shortcuts"; + function MapFog({ map, shapes, @@ -41,21 +55,21 @@ function MapFog({ toolSettings, editable, }) { - const { - stageScale, - mapWidth, - mapHeight, - interactionEmitter, - } = useMapInteraction(); - const { - grid, - gridCellNormalizedSize, - gridCellPixelSize, - gridStrokeWidth, - gridCellPixelOffset, - gridOffset, - } = useGrid(); + const stageScale = useDebouncedStageScale(); + const mapWidth = useMapWidth(); + const mapHeight = useMapHeight(); + const interactionEmitter = useInteractionEmitter(); + + const grid = useGrid(); + const gridCellNormalizedSize = useGridCellNormalizedSize(); + const gridCellPixelSize = useGridCellPixelSize(); + const gridStrokeWidth = useGridStrokeWidth(); + const gridCellPixelOffset = useGridCellPixelOffset(); + const gridOffset = useGridOffset(); + const [gridSnappingSensitivity] = useSetting("map.gridSnappingSensitivity"); + const [showFogGuides] = useSetting("fog.showGuides"); + const [editOpacity] = useSetting("fog.editOpacity"); const mapStageRef = useMapStage(); const [drawingShape, setDrawingShape] = useState(null); @@ -73,11 +87,13 @@ function MapFog({ editable && (toolSettings.type === "toggle" || toolSettings.type === "remove"); - const shouldRenderGuides = + const shouldUseGuides = active && editable && (toolSettings.type === "rectangle" || toolSettings.type === "polygon"); + const shouldRenderGuides = shouldUseGuides && showFogGuides; + const [patternImage] = useImage(diagonalPattern); useEffect(() => { @@ -90,7 +106,7 @@ function MapFog({ function getBrushPosition(snapping = true) { const mapImage = mapStage.findOne("#mapImage"); let position = getRelativePointerPosition(mapImage); - if (snapping && shouldRenderGuides) { + if (shouldUseGuides && snapping) { for (let guide of guides) { if (guide.orientation === "vertical") { position.x = guide.start.x * mapWidth; @@ -157,11 +173,16 @@ function MapFog({ ) { return prevShape; } + const simplified = simplifyPoints( + [...prevPoints, brushPosition], + gridCellNormalizedSize, + stageScale / 4 + ); return { ...prevShape, data: { ...prevShape.data, - points: [...prevPoints, brushPosition], + points: simplified, }, }; }); @@ -192,15 +213,12 @@ function MapFog({ drawingShape ) { const cut = toolSettings.useFogCut; - let drawingShapes = [drawingShape]; if (!toolSettings.multilayer) { const shapesToSubtract = shapes.filter((shape) => cut ? !shape.visible : shape.visible ); - const subtractAction = new SubtractShapeAction( - mergeFogShapes(shapesToSubtract, !cut) - ); + const subtractAction = new SubtractShapeAction(shapesToSubtract); const state = subtractAction.execute({ [drawingShape.id]: drawingShape, }); @@ -211,24 +229,15 @@ function MapFog({ if (drawingShapes.length > 0) { drawingShapes = drawingShapes.map((shape) => { - let shapeData = {}; if (cut) { - shapeData = { id: shape.id, type: shape.type }; + return { + id: shape.id, + type: shape.type, + data: shape.data, + }; } else { - shapeData = { ...shape, color: "black" }; + return { ...shape, color: "black" }; } - return { - ...shapeData, - data: { - ...shape.data, - points: simplifyPoints( - shape.data.points, - gridCellNormalizedSize, - // Downscale fog as smoothing doesn't currently work with edge snapping - Math.max(stageScale, 1) / 2 - ), - }, - }; }); if (cut) { @@ -275,10 +284,7 @@ function MapFog({ } function handlePointerMove() { - if ( - active && - (toolSettings.type === "polygon" || toolSettings.type === "rectangle") - ) { + if (shouldUseGuides) { let guides = []; const brushPosition = getBrushPosition(false); const absoluteBrushPosition = Vector2.multiply(brushPosition, { @@ -371,9 +377,7 @@ function MapFog({ const shapesToSubtract = shapes.filter((shape) => cut ? !shape.visible : shape.visible ); - const subtractAction = new SubtractShapeAction( - mergeFogShapes(shapesToSubtract, !cut) - ); + const subtractAction = new SubtractShapeAction(shapesToSubtract); const state = subtractAction.execute({ [polygonShape.id]: polygonShape, }); @@ -401,16 +405,20 @@ function MapFog({ }, [toolSettings, drawingShape, onShapesCut, onShapesAdd, shapes]); // Add keyboard shortcuts - function handleKeyDown({ key }) { - if (key === "Enter" && toolSettings.type === "polygon" && drawingShape) { + function handleKeyDown(event) { + if ( + shortcuts.fogFinishPolygon(event) && + toolSettings.type === "polygon" && + drawingShape + ) { finishDrawingPolygon(); } - if (key === "Escape" && drawingShape) { + if (shortcuts.fogCancelPolygon(event) && drawingShape) { setDrawingShape(null); } // Remove last point from polygon shape if delete pressed if ( - (key === "Backspace" || key === "Delete") && + shortcuts.delete(event) && drawingShape && toolSettings.type === "polygon" ) { @@ -492,14 +500,18 @@ function MapFog({ onTouchEnd={eraseHoveredShapes} points={points} stroke={ - editable ? colors.lightGray : colors[shape.color] || shape.color + editable && active + ? colors.lightGray + : colors[shape.color] || shape.color } fill={colors[shape.color] || shape.color} closed lineCap="round" lineJoin="round" strokeWidth={gridStrokeWidth * shape.strokeWidth} - opacity={editable ? (!shape.visible ? 0.2 : 0.5) : 1} + opacity={ + editable ? (!shape.visible ? editOpacity / 2 : editOpacity) : 1 + } fillPatternImage={patternImage} fillPriority={editable && !shape.visible ? "pattern" : "color"} holes={holes} @@ -566,12 +578,17 @@ function MapFog({ if (editable) { const visibleShapes = shapes.filter(shapeVisible); - setFogShapeBoundingBoxes(getFogShapesBoundingBoxes(visibleShapes)); + // Only use bounding box guides when rendering them + if (shouldRenderGuides) { + setFogShapeBoundingBoxes(getFogShapesBoundingBoxes(visibleShapes, 5)); + } else { + setFogShapeBoundingBoxes([]); + } setFogShapes(visibleShapes); } else { setFogShapes(mergeFogShapes(shapes)); } - }, [shapes, editable, active, toolSettings]); + }, [shapes, editable, active, toolSettings, shouldRenderGuides]); const fogGroupRef = useRef(); diff --git a/src/components/map/MapGrid.js b/src/components/map/MapGrid.js index 1fe603b..5cfbf94 100644 --- a/src/components/map/MapGrid.js +++ b/src/components/map/MapGrid.js @@ -1,7 +1,8 @@ import React, { useEffect, useState } from "react"; import useImage from "use-image"; -import useDataSource from "../../hooks/useDataSource"; +import { useImageSource } from "../../contexts/ImageSourceContext"; + import { mapSources as defaultMapSources } from "../../maps"; import { getImageLightness } from "../../helpers/image"; @@ -17,7 +18,7 @@ function MapGrid({ map }) { mapSourceMap = map.resolutions[resolutionArray[0]]; } } - const mapSource = useDataSource(mapSourceMap, defaultMapSources); + const mapSource = useImageSource(mapSourceMap, defaultMapSources); const [mapImage, mapLoadingStatus] = useImage(mapSource); const [isImageLight, setIsImageLight] = useState(true); diff --git a/src/components/map/MapGridEditor.js b/src/components/map/MapGridEditor.js index ccc316d..8962654 100644 --- a/src/components/map/MapGridEditor.js +++ b/src/components/map/MapGridEditor.js @@ -1,18 +1,23 @@ import React, { useRef } from "react"; import { Group, Circle, Rect } from "react-konva"; -import { useMapInteraction } from "../../contexts/MapInteractionContext"; +import { + useDebouncedStageScale, + useMapWidth, + useMapHeight, + useSetPreventMapInteraction, +} from "../../contexts/MapInteractionContext"; import { useKeyboard } from "../../contexts/KeyboardContext"; import Vector2 from "../../helpers/Vector2"; +import shortcuts from "../../shortcuts"; + function MapGridEditor({ map, onGridChange }) { - const { - mapWidth, - mapHeight, - stageScale, - setPreventMapInteraction, - } = useMapInteraction(); + const stageScale = useDebouncedStageScale(); + const mapWidth = useMapWidth(); + const mapHeight = useMapHeight(); + const setPreventMapInteraction = useSetPreventMapInteraction(); const mapSize = { x: mapWidth, y: mapHeight }; @@ -163,20 +168,19 @@ function MapGridEditor({ map, onGridChange }) { } function handleKeyDown(event) { - const { key, shiftKey } = event; - const nudgeAmount = shiftKey ? 2 : 0.5; - if (key === "ArrowUp") { + const nudgeAmount = event.shiftKey ? 2 : 0.5; + if (shortcuts.gridNudgeUp(event)) { // Stop arrow up/down scrolling if overflowing event.preventDefault(); nudgeGrid({ x: 0, y: -1 }, nudgeAmount); } - if (key === "ArrowLeft") { + if (shortcuts.gridNudgeLeft(event)) { nudgeGrid({ x: -1, y: 0 }, nudgeAmount); } - if (key === "ArrowRight") { + if (shortcuts.gridNudgeRight(event)) { nudgeGrid({ x: 1, y: 0 }, nudgeAmount); } - if (key === "ArrowDown") { + if (shortcuts.gridNudgeDown(event)) { event.preventDefault(); nudgeGrid({ x: 0, y: 1 }, nudgeAmount); } diff --git a/src/components/map/MapInteraction.js b/src/components/map/MapInteraction.js index 404b52d..e1bb369 100644 --- a/src/components/map/MapInteraction.js +++ b/src/components/map/MapInteraction.js @@ -1,4 +1,4 @@ -import React, { useRef, useEffect, useState, useContext } from "react"; +import React, { useRef, useEffect, useState } from "react"; import { Box } from "theme-ui"; import ReactResizeDetector from "react-resize-detector"; import { Stage, Layer, Image } from "react-konva"; @@ -10,18 +10,15 @@ import useStageInteraction from "../../hooks/useStageInteraction"; import useImageCenter from "../../hooks/useImageCenter"; import { getGridMaxZoom } from "../../helpers/grid"; +import KonvaBridge from "../../helpers/KonvaBridge"; import { MapInteractionProvider } from "../../contexts/MapInteractionContext"; -import { MapStageProvider, useMapStage } from "../../contexts/MapStageContext"; -import AuthContext, { useAuth } from "../../contexts/AuthContext"; -import SettingsContext, { useSettings } from "../../contexts/SettingsContext"; -import KeyboardContext from "../../contexts/KeyboardContext"; -import TokenDataContext, { - useTokenData, -} from "../../contexts/TokenDataContext"; +import { useMapStage } from "../../contexts/MapStageContext"; import { GridProvider } from "../../contexts/GridContext"; import { useKeyboard } from "../../contexts/KeyboardContext"; +import shortcuts from "../../shortcuts"; + function MapInteraction({ map, mapState, @@ -116,12 +113,12 @@ function MapInteraction({ function handleKeyDown(event) { // Change to move tool when pressing space - if (event.key === " " && selectedToolId === "move") { + if (shortcuts.move(event) && selectedToolId === "move") { // Stop active state on move icon from being selected event.preventDefault(); } if ( - event.key === " " && + shortcuts.move(event) && selectedToolId !== "move" && !disabledControls.includes("move") ) { @@ -131,35 +128,33 @@ function MapInteraction({ } // Basic keyboard shortcuts - if (event.key === "w" && !disabledControls.includes("move")) { + if (shortcuts.moveTool(event) && !disabledControls.includes("move")) { onSelectedToolChange("move"); } - if (event.key === "d" && !disabledControls.includes("drawing")) { + if (shortcuts.drawingTool(event) && !disabledControls.includes("drawing")) { onSelectedToolChange("drawing"); } - if (event.key === "f" && !disabledControls.includes("fog")) { + if (shortcuts.fogTool(event) && !disabledControls.includes("fog")) { onSelectedToolChange("fog"); } - if (event.key === "m" && !disabledControls.includes("measure")) { + if (shortcuts.measureTool(event) && !disabledControls.includes("measure")) { onSelectedToolChange("measure"); } - if (event.key === "q" && !disabledControls.includes("pointer")) { + if (shortcuts.pointerTool(event) && !disabledControls.includes("pointer")) { onSelectedToolChange("pointer"); } - if (event.key === "n" && !disabledControls.includes("note")) { + if (shortcuts.noteTool(event) && !disabledControls.includes("note")) { onSelectedToolChange("note"); } } function handleKeyUp(event) { - if (event.key === " " && selectedToolId === "move") { + if (shortcuts.move(event) && selectedToolId === "move") { onSelectedToolChange(previousSelectedToolRef.current); } } useKeyboard(handleKeyDown, handleKeyUp); - // Get keyboard context to pass to Konva - const keyboardValue = useContext(KeyboardContext); function getCursorForTool(tool) { switch (tool) { @@ -167,9 +162,7 @@ function MapInteraction({ return "move"; case "fog": case "drawing": - return settings.settings[tool].type === "move" - ? "pointer" - : "crosshair"; + return "crosshair"; case "measure": case "pointer": case "note": @@ -179,10 +172,6 @@ function MapInteraction({ } } - const auth = useAuth(); - const settings = useSettings(); - const tokenData = useTokenData(); - const mapInteraction = { stageScale, stageWidth, @@ -194,61 +183,49 @@ function MapInteraction({ }; return ( - - - + + - - - {/* Forward auth context to konva elements */} - - - - - - - - {mapLoaded && children} - - - - - - - - - - - - + + ( + + {children} + + )} + > + + + + {mapLoaded && children} + + + {controls} - - - + + + ); } diff --git a/src/components/map/MapMeasure.js b/src/components/map/MapMeasure.js index 120bfef..f9318c9 100644 --- a/src/components/map/MapMeasure.js +++ b/src/components/map/MapMeasure.js @@ -1,9 +1,20 @@ import React, { useState, useEffect } from "react"; import { Group, Line, Text, Label, Tag } from "react-konva"; -import { useMapInteraction } from "../../contexts/MapInteractionContext"; +import { + useDebouncedStageScale, + useMapWidth, + useMapHeight, + useInteractionEmitter, +} from "../../contexts/MapInteractionContext"; import { useMapStage } from "../../contexts/MapStageContext"; -import { useGrid } from "../../contexts/GridContext"; +import { + useGrid, + useGridCellPixelSize, + useGridCellNormalizedSize, + useGridStrokeWidth, + useGridOffset, +} from "../../contexts/GridContext"; import { getDefaultShapeData, @@ -16,19 +27,17 @@ import { parseGridScale, gridDistance } from "../../helpers/grid"; import useGridSnapping from "../../hooks/useGridSnapping"; function MapMeasure({ map, active }) { - const { - stageScale, - mapWidth, - mapHeight, - interactionEmitter, - } = useMapInteraction(); - const { - grid, - gridCellNormalizedSize, - gridStrokeWidth, - gridCellPixelSize, - gridOffset, - } = useGrid(); + const stageScale = useDebouncedStageScale(); + const mapWidth = useMapWidth(); + const mapHeight = useMapHeight(); + const interactionEmitter = useInteractionEmitter(); + + const grid = useGrid(); + const gridCellNormalizedSize = useGridCellNormalizedSize(); + const gridCellPixelSize = useGridCellPixelSize(); + const gridStrokeWidth = useGridStrokeWidth(); + const gridOffset = useGridOffset(); + const mapStageRef = useMapStage(); const [drawingShapeData, setDrawingShapeData] = useState(null); const [isBrushDown, setIsBrushDown] = useState(false); diff --git a/src/components/map/MapNotes.js b/src/components/map/MapNotes.js index aff5ec1..c2706c8 100644 --- a/src/components/map/MapNotes.js +++ b/src/components/map/MapNotes.js @@ -2,7 +2,7 @@ import React, { useState, useEffect, useRef } from "react"; import shortid from "shortid"; import { Group } from "react-konva"; -import { useMapInteraction } from "../../contexts/MapInteractionContext"; +import { useInteractionEmitter } from "../../contexts/MapInteractionContext"; import { useMapStage } from "../../contexts/MapStageContext"; import { useAuth } from "../../contexts/AuthContext"; @@ -27,7 +27,7 @@ function MapNotes({ onNoteDragEnd, fadeOnHover, }) { - const { interactionEmitter } = useMapInteraction(); + const interactionEmitter = useInteractionEmitter(); const { userId } = useAuth(); const mapStageRef = useMapStage(); const [isBrushDown, setIsBrushDown] = useState(false); diff --git a/src/components/map/MapPointer.js b/src/components/map/MapPointer.js index 952ff11..e78ac33 100644 --- a/src/components/map/MapPointer.js +++ b/src/components/map/MapPointer.js @@ -1,9 +1,13 @@ import React, { useEffect } from "react"; import { Group } from "react-konva"; -import { useMapInteraction } from "../../contexts/MapInteractionContext"; +import { + useMapWidth, + useMapHeight, + useInteractionEmitter, +} from "../../contexts/MapInteractionContext"; import { useMapStage } from "../../contexts/MapStageContext"; -import { useGrid } from "../../contexts/GridContext"; +import { useGridStrokeWidth } from "../../contexts/GridContext"; import { getRelativePointerPositionNormalized, @@ -22,8 +26,10 @@ function MapPointer({ visible, color, }) { - const { mapWidth, mapHeight, interactionEmitter } = useMapInteraction(); - const { gridStrokeWidth } = useGrid(); + const mapWidth = useMapWidth(); + const mapHeight = useMapHeight(); + const interactionEmitter = useInteractionEmitter(); + const gridStrokeWidth = useGridStrokeWidth(); const mapStageRef = useMapStage(); useEffect(() => { diff --git a/src/components/map/MapTest.js b/src/components/map/MapTest.js new file mode 100644 index 0000000..25a914b --- /dev/null +++ b/src/components/map/MapTest.js @@ -0,0 +1,5 @@ +import React from "react"; + +function MapTest() {} + +export default MapTest; diff --git a/src/components/map/MapTile.js b/src/components/map/MapTile.js index e8d6358..f09a620 100644 --- a/src/components/map/MapTile.js +++ b/src/components/map/MapTile.js @@ -2,7 +2,7 @@ import React from "react"; import Tile from "../Tile"; -import useDataSource from "../../hooks/useDataSource"; +import { useImageSource } from "../../contexts/ImageSourceContext"; import { mapSources as defaultMapSources, unknownSource } from "../../maps"; function MapTile({ @@ -15,11 +15,11 @@ function MapTile({ canEdit, badges, }) { - const isDefault = map.type === "default"; - const mapSource = useDataSource( - isDefault ? map : map.thumbnail, + const mapSource = useImageSource( + map, defaultMapSources, - unknownSource + unknownSource, + map.type === "file" ); return ( @@ -29,7 +29,7 @@ function MapTile({ isSelected={isSelected} onSelect={() => onMapSelect(map)} onEdit={() => onMapEdit(map.id)} - onDoubleClick={onDone} + onDoubleClick={() => canEdit && onDone()} size={size} canEdit={canEdit} badges={badges} diff --git a/src/components/map/MapToken.js b/src/components/map/MapToken.js index e340ed1..1f7258f 100644 --- a/src/components/map/MapToken.js +++ b/src/components/map/MapToken.js @@ -4,14 +4,19 @@ import { useSpring, animated } from "react-spring/konva"; import useImage from "use-image"; import Konva from "konva"; -import useDataSource from "../../hooks/useDataSource"; import useDebounce from "../../hooks/useDebounce"; import usePrevious from "../../hooks/usePrevious"; import useGridSnapping from "../../hooks/useGridSnapping"; import { useAuth } from "../../contexts/AuthContext"; -import { useMapInteraction } from "../../contexts/MapInteractionContext"; -import { useGrid } from "../../contexts/GridContext"; +import { + useSetPreventMapInteraction, + useMapWidth, + useMapHeight, + useDebouncedStageScale, +} from "../../contexts/MapInteractionContext"; +import { useGridCellPixelSize } from "../../contexts/GridContext"; +import { useImageSource } from "../../contexts/ImageSourceContext"; import TokenStatus from "../token/TokenStatus"; import TokenLabel from "../token/TokenLabel"; @@ -26,20 +31,19 @@ function MapToken({ onTokenDragStart, onTokenDragEnd, draggable, - mapState, fadeOnHover, map, }) { const { userId } = useAuth(); - const { - setPreventMapInteraction, - mapWidth, - mapHeight, - stageScale, - } = useMapInteraction(); - const { gridCellPixelSize } = useGrid(); - const tokenSource = useDataSource(token, tokenSources, unknownSource); + const stageScale = useDebouncedStageScale(); + const mapWidth = useMapWidth(); + const mapHeight = useMapHeight(); + const setPreventMapInteraction = useSetPreventMapInteraction(); + + const gridCellPixelSize = useGridCellPixelSize(); + + const tokenSource = useImageSource(token, tokenSources, unknownSource); const [tokenSourceImage, tokenSourceStatus] = useImage(tokenSource); const [tokenAspectRatio, setTokenAspectRatio] = useState(1); @@ -106,7 +110,6 @@ function MapToken({ mountedToken.moveTo(parent); mountedToken.absolutePosition(position); mountChanges[mountedToken.id()] = { - ...mapState.tokens[mountedToken.id()], x: mountedToken.x() / mapWidth, y: mountedToken.y() / mapHeight, lastModifiedBy: userId, @@ -119,7 +122,6 @@ function MapToken({ onTokenStateChange({ ...mountChanges, [tokenState.id]: { - ...tokenState, x: tokenGroup.x() / mapWidth, y: tokenGroup.y() / mapHeight, lastModifiedBy: userId, @@ -207,8 +209,6 @@ function MapToken({ ), }); image.drawHitFromCache(); - // Force redraw - image.getLayer().draw(); } }, [debouncedStageScale, tokenWidth, tokenHeight, tokenSourceStatus, token]); diff --git a/src/components/map/MapTokens.js b/src/components/map/MapTokens.js index 3949c30..426dcfe 100644 --- a/src/components/map/MapTokens.js +++ b/src/components/map/MapTokens.js @@ -119,7 +119,6 @@ function MapTokens({ !(tokenState.id in disabledTokens) && !tokenState.locked } - mapState={mapState} fadeOnHover={selectedToolId === "drawing"} map={map} /> diff --git a/src/components/map/controls/DrawingToolSettings.js b/src/components/map/controls/DrawingToolSettings.js index 429766b..2537e3f 100644 --- a/src/components/map/controls/DrawingToolSettings.js +++ b/src/components/map/controls/DrawingToolSettings.js @@ -24,6 +24,8 @@ import Divider from "../../Divider"; import { useKeyboard } from "../../../contexts/KeyboardContext"; +import shortcuts from "../../../shortcuts"; + function DrawingToolSettings({ settings, onSettingChange, @@ -31,36 +33,26 @@ function DrawingToolSettings({ disabledActions, }) { // Keyboard shotcuts - function handleKeyDown({ key, ctrlKey, metaKey, shiftKey }) { - if (key === "b") { + function handleKeyDown(event) { + if (shortcuts.drawBrush(event)) { onSettingChange({ type: "brush" }); - } else if (key === "p") { + } else if (shortcuts.drawPaint(event)) { onSettingChange({ type: "paint" }); - } else if (key === "l") { + } else if (shortcuts.drawLine(event)) { onSettingChange({ type: "line" }); - } else if (key === "r") { + } else if (shortcuts.drawRect(event)) { onSettingChange({ type: "rectangle" }); - } else if (key === "c") { + } else if (shortcuts.drawCircle(event)) { onSettingChange({ type: "circle" }); - } else if (key === "t") { + } else if (shortcuts.drawTriangle(event)) { onSettingChange({ type: "triangle" }); - } else if (key === "e") { + } else if (shortcuts.drawErase(event)) { onSettingChange({ type: "erase" }); - } else if (key === "o") { + } else if (shortcuts.drawBlend(event)) { onSettingChange({ useBlending: !settings.useBlending }); - } else if ( - (key === "z" || key === "Z") && - (ctrlKey || metaKey) && - shiftKey && - !disabledActions.includes("redo") - ) { + } else if (shortcuts.redo(event) && !disabledActions.includes("redo")) { onToolAction("mapRedo"); - } else if ( - key === "z" && - (ctrlKey || metaKey) && - !shiftKey && - !disabledActions.includes("undo") - ) { + } else if (shortcuts.undo(event) && !disabledActions.includes("undo")) { onToolAction("mapUndo"); } } diff --git a/src/components/map/controls/FogToolSettings.js b/src/components/map/controls/FogToolSettings.js index 7342a1b..5eb14d7 100644 --- a/src/components/map/controls/FogToolSettings.js +++ b/src/components/map/controls/FogToolSettings.js @@ -22,6 +22,8 @@ import Divider from "../../Divider"; import { useKeyboard } from "../../../contexts/KeyboardContext"; +import shortcuts from "../../../shortcuts"; + function BrushToolSettings({ settings, onSettingChange, @@ -29,36 +31,26 @@ function BrushToolSettings({ disabledActions, }) { // Keyboard shortcuts - function handleKeyDown({ key, ctrlKey, metaKey, shiftKey }) { - if (key === "p") { + function handleKeyDown(event) { + if (shortcuts.fogPolygon(event)) { onSettingChange({ type: "polygon" }); - } else if (key === "b") { + } else if (shortcuts.fogBrush(event)) { onSettingChange({ type: "brush" }); - } else if (key === "t") { + } else if (shortcuts.fogToggle(event)) { onSettingChange({ type: "toggle" }); - } else if (key === "e") { + } else if (shortcuts.fogErase(event)) { onSettingChange({ type: "remove" }); - } else if (key === "l") { + } else if (shortcuts.fogLayer(event)) { onSettingChange({ multilayer: !settings.multilayer }); - } else if (key === "f") { + } else if (shortcuts.fogPreview(event)) { onSettingChange({ preview: !settings.preview }); - } else if (key === "c") { + } else if (shortcuts.fogCut(event)) { onSettingChange({ useFogCut: !settings.useFogCut }); - } else if (key === "r") { + } else if (shortcuts.fogRectangle(event)) { onSettingChange({ type: "rectangle" }); - } else if ( - (key === "z" || key === "Z") && - (ctrlKey || metaKey) && - shiftKey && - !disabledActions.includes("redo") - ) { + } else if (shortcuts.redo(event) && !disabledActions.includes("redo")) { onToolAction("fogRedo"); - } else if ( - key === "z" && - (ctrlKey || metaKey) && - !shiftKey && - !disabledActions.includes("undo") - ) { + } else if (shortcuts.undo(event) && !disabledActions.includes("undo")) { onToolAction("fogUndo"); } } diff --git a/src/components/note/Note.js b/src/components/note/Note.js index 1aacc4d..461d8b1 100644 --- a/src/components/note/Note.js +++ b/src/components/note/Note.js @@ -3,8 +3,12 @@ import { Rect, Text } from "react-konva"; import { useSpring, animated } from "react-spring/konva"; import { useAuth } from "../../contexts/AuthContext"; -import { useMapInteraction } from "../../contexts/MapInteractionContext"; -import { useGrid } from "../../contexts/GridContext"; +import { + useSetPreventMapInteraction, + useMapWidth, + useMapHeight, +} from "../../contexts/MapInteractionContext"; +import { useGridCellPixelSize } from "../../contexts/GridContext"; import colors from "../../helpers/colors"; @@ -24,8 +28,12 @@ function Note({ fadeOnHover, }) { const { userId } = useAuth(); - const { mapWidth, mapHeight, setPreventMapInteraction } = useMapInteraction(); - const { gridCellPixelSize } = useGrid(); + + const mapWidth = useMapWidth(); + const mapHeight = useMapHeight(); + const setPreventMapInteraction = useSetPreventMapInteraction(); + + const gridCellPixelSize = useGridCellPixelSize(); const minCellSize = Math.min( gridCellPixelSize.width, @@ -142,7 +150,7 @@ function Note({ } findFontSize(); - }, [note, note.text, noteWidth, noteHeight, notePadding]); + }, [note, note.text, note.visible, noteWidth, noteHeight, notePadding]); const textRef = useRef(); diff --git a/src/components/party/DiceRolls.js b/src/components/party/DiceRolls.js index bc05f1e..b20b3bd 100644 --- a/src/components/party/DiceRolls.js +++ b/src/components/party/DiceRolls.js @@ -14,11 +14,32 @@ import DiceRoll from "./DiceRoll"; import { getDiceRollTotal } from "../../helpers/dice"; +const diceIcons = [ + { type: "d20", Icon: D20Icon }, + { type: "d12", Icon: D12Icon }, + { type: "d10", Icon: D10Icon }, + { type: "d8", Icon: D8Icon }, + { type: "d6", Icon: D6Icon }, + { type: "d4", Icon: D4Icon }, + { type: "d100", Icon: D100Icon }, +]; + function DiceRolls({ rolls }) { const total = getDiceRollTotal(rolls); const [expanded, setExpanded] = useState(false); + let expandedRolls = []; + for (let icon of diceIcons) { + if (rolls.some((roll) => roll.type === icon.type)) { + expandedRolls.push( + + + + ); + } + } + return ( total > 0 && ( @@ -40,27 +61,7 @@ function DiceRolls({ rolls }) { flexDirection: "column", }} > - - - - - - - - - - - - - - - - - - - - - + {expandedRolls} )} diff --git a/src/components/party/Party.js b/src/components/party/Party.js index 8e58feb..1158602 100644 --- a/src/components/party/Party.js +++ b/src/components/party/Party.js @@ -90,8 +90,6 @@ function Party({ gameId, stream, partyStreams, onStreamStart, onStreamEnd }) { bg="background" sx={{ position: "relative", - // width: fullScreen ? "0" : "112px", - // minWidth: fullScreen ? "0" : "112px", }} > ); diff --git a/src/components/token/TokenDragOverlay.js b/src/components/token/TokenDragOverlay.js index 467967f..d5d9348 100644 --- a/src/components/token/TokenDragOverlay.js +++ b/src/components/token/TokenDragOverlay.js @@ -1,7 +1,10 @@ import React from "react"; import { useAuth } from "../../contexts/AuthContext"; -import { useMapInteraction } from "../../contexts/MapInteractionContext"; +import { + useMapWidth, + useMapHeight, +} from "../../contexts/MapInteractionContext"; import DragOverlay from "../DragOverlay"; @@ -12,10 +15,11 @@ function TokenDragOverlay({ tokenState, tokenGroup, dragging, - mapState, }) { const { userId } = useAuth(); - const { mapWidth, mapHeight } = useMapInteraction(); + + const mapWidth = useMapWidth(); + const mapHeight = useMapHeight(); function handleTokenRemove() { // Handle other tokens when a vehicle gets deleted @@ -29,7 +33,6 @@ function TokenDragOverlay({ mountedToken.absolutePosition(position); onTokenStateChange({ [mountedToken.id()]: { - ...mapState.tokens[mountedToken.id()], x: mountedToken.x() / mapWidth, y: mountedToken.y() / mapHeight, lastModifiedBy: userId, diff --git a/src/components/token/TokenLabel.js b/src/components/token/TokenLabel.js index 00aea92..e11918e 100644 --- a/src/components/token/TokenLabel.js +++ b/src/components/token/TokenLabel.js @@ -45,7 +45,15 @@ function TokenLabel({ tokenState, width, height }) { } findFontSize(); - }, [tokenState.label, width, height, tokenState, labelSize, paddingX]); + }, [ + tokenState.label, + tokenState.visible, + width, + height, + tokenState, + labelSize, + paddingX, + ]); const [rectWidth, setRectWidth] = useState(0); useEffect(() => { diff --git a/src/components/token/TokenMenu.js b/src/components/token/TokenMenu.js index d319082..9f4b024 100644 --- a/src/components/token/TokenMenu.js +++ b/src/components/token/TokenMenu.js @@ -51,8 +51,7 @@ function TokenMenu({ function handleLabelChange(event) { const label = event.target.value.substring(0, 144); - tokenState && - onTokenStateChange({ [tokenState.id]: { ...tokenState, label: label } }); + tokenState && onTokenStateChange({ [tokenState.id]: { label: label } }); } function handleStatusChange(status) { @@ -66,35 +65,34 @@ function TokenMenu({ statuses.add(status); } onTokenStateChange({ - [tokenState.id]: { ...tokenState, statuses: [...statuses] }, + [tokenState.id]: { statuses: [...statuses] }, }); } function handleSizeChange(event) { const newSize = parseFloat(event.target.value); - tokenState && - onTokenStateChange({ [tokenState.id]: { ...tokenState, size: newSize } }); + tokenState && onTokenStateChange({ [tokenState.id]: { size: newSize } }); } function handleRotationChange(event) { const newRotation = parseInt(event.target.value); tokenState && onTokenStateChange({ - [tokenState.id]: { ...tokenState, rotation: newRotation }, + [tokenState.id]: { rotation: newRotation }, }); } function handleVisibleChange() { tokenState && onTokenStateChange({ - [tokenState.id]: { ...tokenState, visible: !tokenState.visible }, + [tokenState.id]: { visible: !tokenState.visible }, }); } function handleLockChange() { tokenState && onTokenStateChange({ - [tokenState.id]: { ...tokenState, locked: !tokenState.locked }, + [tokenState.id]: { locked: !tokenState.locked }, }); } diff --git a/src/components/token/TokenPreview.js b/src/components/token/TokenPreview.js index f731773..cb23670 100644 --- a/src/components/token/TokenPreview.js +++ b/src/components/token/TokenPreview.js @@ -6,11 +6,11 @@ import useImage from "use-image"; import usePreventOverscroll from "../../hooks/usePreventOverscroll"; import useStageInteraction from "../../hooks/useStageInteraction"; -import useDataSource from "../../hooks/useDataSource"; import useImageCenter from "../../hooks/useImageCenter"; import useResponsiveLayout from "../../hooks/useResponsiveLayout"; import { GridProvider } from "../../contexts/GridContext"; +import { useImageSource } from "../../contexts/ImageSourceContext"; import GridOnIcon from "../../icons/GridOnIcon"; import GridOffIcon from "../../icons/GridOffIcon"; @@ -27,7 +27,7 @@ function TokenPreview({ token }) { } }, [token, tokenSourceData]); - const tokenSource = useDataSource( + const tokenSource = useImageSource( tokenSourceData, tokenSources, unknownSource diff --git a/src/components/token/TokenTile.js b/src/components/token/TokenTile.js index 0f05157..29be8b2 100644 --- a/src/components/token/TokenTile.js +++ b/src/components/token/TokenTile.js @@ -2,7 +2,8 @@ import React from "react"; import Tile from "../Tile"; -import useDataSource from "../../hooks/useDataSource"; +import { useImageSource } from "../../contexts/ImageSourceContext"; + import { tokenSources as defaultTokenSources, unknownSource, @@ -17,11 +18,11 @@ function TokenTile({ canEdit, badges, }) { - const isDefault = token.type === "default"; - const tokenSource = useDataSource( - isDefault ? token : token.thumbnail, + const tokenSource = useImageSource( + token, defaultTokenSources, - unknownSource + unknownSource, + token.type === "file" ); return ( diff --git a/src/components/token/Tokens.js b/src/components/token/Tokens.js index c107ee7..c56feec 100644 --- a/src/components/token/Tokens.js +++ b/src/components/token/Tokens.js @@ -41,7 +41,11 @@ function Tokens({ onMapTokenStateCreate }) { visible: true, }); // Update last used for cache invalidation - updateToken(token.id, { lastUsed: Date.now() }); + // Keep last modified the same + updateToken(token.id, { + lastUsed: Date.now(), + lastModified: token.lastModified, + }); } } diff --git a/src/contexts/DatabaseContext.js b/src/contexts/DatabaseContext.js index d0bc2ea..5e1e3b5 100644 --- a/src/contexts/DatabaseContext.js +++ b/src/contexts/DatabaseContext.js @@ -1,12 +1,16 @@ import React, { useState, useEffect, useContext } from "react"; -import { Box, Text } from "theme-ui"; +import * as Comlink from "comlink"; -import Banner from "../components/Banner"; +import ErrorBanner from "../components/banner/ErrorBanner"; import { getDatabase } from "../database"; +import DatabaseWorker from "worker-loader!../workers/DatabaseWorker"; // eslint-disable-line import/no-webpack-loader-syntax + const DatabaseContext = React.createContext(); +const worker = Comlink.wrap(new DatabaseWorker()); + export function DatabaseProvider({ children }) { const [database, setDatabase] = useState(); const [databaseStatus, setDatabaseStatus] = useState("loading"); @@ -40,13 +44,19 @@ export function DatabaseProvider({ children }) { }; function handleDatabaseError(event) { - if (event.reason?.name === "QuotaExceededError") { - event.preventDefault(); + event.preventDefault(); + if (event.reason?.message.startsWith("QuotaExceededError")) { setDatabaseError({ name: event.reason.name, message: "Storage Quota Exceeded Please Clear Space and Try Again.", }); + } else { + setDatabaseError({ + name: event.reason.name, + message: "Something went wrong, please refresh your browser.", + }); } + console.error(event.reason); } window.addEventListener("unhandledrejection", handleDatabaseError); @@ -59,21 +69,16 @@ export function DatabaseProvider({ children }) { database, databaseStatus, databaseError, + worker, }; return ( <> {children} - setDatabaseError()} - > - - - {databaseError && databaseError.message} - - - + /> ); diff --git a/src/contexts/GridContext.js b/src/contexts/GridContext.js index 6598000..a4b173f 100644 --- a/src/contexts/GridContext.js +++ b/src/contexts/GridContext.js @@ -1,4 +1,4 @@ -import React, { useContext } from "react"; +import React, { useContext, useState, useEffect } from "react"; import Vector2 from "../helpers/Vector2"; import Size from "../helpers/Size"; @@ -37,55 +37,105 @@ const defaultValue = { gridCellPixelOffset: new Vector2(0, 0), }; -const GridContext = React.createContext(defaultValue); +export const GridContext = React.createContext(defaultValue.grid); +export const GridPixelSizeContext = React.createContext( + defaultValue.gridPixelSize +); +export const GridCellPixelSizeContext = React.createContext( + defaultValue.gridCellPixelSize +); +export const GridCellNormalizedSizeContext = React.createContext( + defaultValue.gridCellNormalizedSize +); +export const GridOffsetContext = React.createContext(defaultValue.gridOffset); +export const GridStrokeWidthContext = React.createContext( + defaultValue.gridStrokeWidth +); +export const GridCellPixelOffsetContext = React.createContext( + defaultValue.gridCellPixelOffset +); const defaultStrokeWidth = 1 / 10; -export function GridProvider({ grid, width, height, children }) { +export function GridProvider({ grid: inputGrid, width, height, children }) { + let grid = inputGrid; + if (!grid?.size.x || !grid?.size.y) { - return ( - - {children} - + grid = defaultValue.grid; + } + + const [gridPixelSize, setGridPixelSize] = useState( + defaultValue.gridCellPixelSize + ); + const [gridCellPixelSize, setGridCellPixelSize] = useState( + defaultValue.gridCellPixelSize + ); + const [gridCellNormalizedSize, setGridCellNormalizedSize] = useState( + defaultValue.gridCellNormalizedSize + ); + const [gridOffset, setGridOffset] = useState(defaultValue.gridOffset); + const [gridStrokeWidth, setGridStrokeWidth] = useState( + defaultValue.gridStrokeWidth + ); + const [gridCellPixelOffset, setGridCellPixelOffset] = useState( + defaultValue.gridCellPixelOffset + ); + + useEffect(() => { + const _gridPixelSize = getGridPixelSize(grid, width, height); + const _gridCellPixelSize = getCellPixelSize( + grid, + _gridPixelSize.width, + _gridPixelSize.height ); - } + const _gridCellNormalizedSize = new Size( + _gridCellPixelSize.width / width, + _gridCellPixelSize.height / height + ); + const _gridOffset = Vector2.multiply(grid.inset.topLeft, { + x: width, + y: height, + }); + const _gridStrokeWidth = + (_gridCellPixelSize.width < _gridCellPixelSize.height + ? _gridCellPixelSize.width + : _gridCellPixelSize.height) * defaultStrokeWidth; - const gridPixelSize = getGridPixelSize(grid, width, height); - const gridCellPixelSize = getCellPixelSize( - grid, - gridPixelSize.width, - gridPixelSize.height + let _gridCellPixelOffset = { x: 0, y: 0 }; + // Move hex tiles to top left + if (grid.type === "hexVertical" || grid.type === "hexHorizontal") { + _gridCellPixelOffset = Vector2.multiply(_gridCellPixelSize, 0.5); + } + + setGridPixelSize(_gridPixelSize); + setGridCellPixelSize(_gridCellPixelSize); + setGridCellNormalizedSize(_gridCellNormalizedSize); + setGridOffset(_gridOffset); + setGridStrokeWidth(_gridStrokeWidth); + setGridCellPixelOffset(_gridCellPixelOffset); + }, [grid, width, height]); + + return ( + + + + + + + + {children} + + + + + + + ); - const gridCellNormalizedSize = new Size( - gridCellPixelSize.width / width, - gridCellPixelSize.height / height - ); - const gridOffset = Vector2.multiply(grid.inset.topLeft, { - x: width, - y: height, - }); - const gridStrokeWidth = - (gridCellPixelSize.width < gridCellPixelSize.height - ? gridCellPixelSize.width - : gridCellPixelSize.height) * defaultStrokeWidth; - - let gridCellPixelOffset = { x: 0, y: 0 }; - // Move hex tiles to top left - if (grid.type === "hexVertical" || grid.type === "hexHorizontal") { - gridCellPixelOffset = Vector2.multiply(gridCellPixelSize, 0.5); - } - - const value = { - grid, - gridPixelSize, - gridCellPixelSize, - gridCellNormalizedSize, - gridOffset, - gridStrokeWidth, - gridCellPixelOffset, - }; - - return {children}; } export function useGrid() { @@ -96,4 +146,54 @@ export function useGrid() { return context; } -export default GridContext; +export function useGridPixelSize() { + const context = useContext(GridPixelSizeContext); + if (context === undefined) { + throw new Error("useGridPixelSize must be used within a GridProvider"); + } + return context; +} + +export function useGridCellPixelSize() { + const context = useContext(GridCellPixelSizeContext); + if (context === undefined) { + throw new Error("useGridCellPixelSize must be used within a GridProvider"); + } + return context; +} + +export function useGridCellNormalizedSize() { + const context = useContext(GridCellNormalizedSizeContext); + if (context === undefined) { + throw new Error( + "useGridCellNormalizedSize must be used within a GridProvider" + ); + } + return context; +} + +export function useGridOffset() { + const context = useContext(GridOffsetContext); + if (context === undefined) { + throw new Error("useGridOffset must be used within a GridProvider"); + } + return context; +} + +export function useGridStrokeWidth() { + const context = useContext(GridStrokeWidthContext); + if (context === undefined) { + throw new Error("useGridStrokeWidth must be used within a GridProvider"); + } + return context; +} + +export function useGridCellPixelOffset() { + const context = useContext(GridCellPixelOffsetContext); + if (context === undefined) { + throw new Error( + "useGridCellPixelOffset must be used within a GridProvider" + ); + } + return context; +} diff --git a/src/contexts/ImageSourceContext.js b/src/contexts/ImageSourceContext.js new file mode 100644 index 0000000..b558383 --- /dev/null +++ b/src/contexts/ImageSourceContext.js @@ -0,0 +1,157 @@ +import React, { useContext, useState, useEffect } from "react"; + +import { omit } from "../helpers/shared"; + +export const ImageSourcesStateContext = React.createContext(); +export const ImageSourcesUpdaterContext = React.createContext(() => {}); + +/** + * Helper to manage sharing of custom image sources between uses of useImageSource + */ +export function ImageSourcesProvider({ children }) { + const [imageSources, setImageSources] = useState({}); + + // Revoke url when no more references + useEffect(() => { + let sourcesToCleanup = []; + for (let source of Object.values(imageSources)) { + if (source.references <= 0) { + URL.revokeObjectURL(source.url); + sourcesToCleanup.push(source.id); + } + } + if (sourcesToCleanup.length > 0) { + setImageSources((prevSources) => omit(prevSources, sourcesToCleanup)); + } + }, [imageSources]); + + return ( + + + {children} + + + ); +} + +/** + * Get id from image data + */ +function getImageFileId(data, thumbnail) { + if (thumbnail) { + return `${data.id}-thumbnail`; + } + if (data.resolutions) { + // Check is a resolution is specified + if (data.quality && data.resolutions[data.quality]) { + return `${data.id}-${data.quality}`; + } else if (!data.file) { + // Fallback to the highest resolution + const resolutionArray = Object.keys(data.resolutions); + const resolution = resolutionArray[resolutionArray.length - 1]; + return `${data.id}-${resolution.id}`; + } + } + return data.id; +} + +/** + * Helper function to load either file or default image into a URL + */ +export function useImageSource(data, defaultSources, unknownSource, thumbnail) { + const imageSources = useContext(ImageSourcesStateContext); + if (imageSources === undefined) { + throw new Error( + "useImageSource must be used within a ImageSourcesProvider" + ); + } + const setImageSources = useContext(ImageSourcesUpdaterContext); + if (setImageSources === undefined) { + throw new Error( + "useImageSource must be used within a ImageSourcesProvider" + ); + } + + useEffect(() => { + if (!data || data.type !== "file") { + return; + } + const id = getImageFileId(data, thumbnail); + + function updateImageSource(file) { + if (file) { + setImageSources((prevSources) => { + if (id in prevSources) { + // Check if the image source is already added + return { + ...prevSources, + [id]: { + ...prevSources[id], + // Increase references + references: prevSources[id].references + 1, + }, + }; + } else { + const url = URL.createObjectURL(new Blob([file])); + return { + ...prevSources, + [id]: { url, id, references: 1 }, + }; + } + }); + } + } + + if (thumbnail) { + updateImageSource(data.thumbnail.file); + } else if (data.resolutions) { + // Check is a resolution is specified + if (data.quality && data.resolutions[data.quality]) { + updateImageSource(data.resolutions[data.quality].file); + } + // If no file available fallback to the highest resolution + else if (!data.file) { + const resolutionArray = Object.keys(data.resolutions); + updateImageSource( + data.resolutions[resolutionArray[resolutionArray.length - 1]].file + ); + } else { + updateImageSource(data.file); + } + } else { + updateImageSource(data.file); + } + + return () => { + // Decrease references + setImageSources((prevSources) => { + if (id in prevSources) { + return { + ...prevSources, + [id]: { + ...prevSources[id], + references: prevSources[id].references - 1, + }, + }; + } else { + return prevSources; + } + }); + }; + }, [data, unknownSource, thumbnail, setImageSources]); + + if (!data) { + return unknownSource; + } + + if (data.type === "default") { + return defaultSources[data.key]; + } + + if (data.type === "file") { + const id = getImageFileId(data, thumbnail); + return imageSources[id]?.url; + } + + return unknownSource; +} diff --git a/src/contexts/KeyboardContext.js b/src/contexts/KeyboardContext.js index ec47dd9..592e60f 100644 --- a/src/contexts/KeyboardContext.js +++ b/src/contexts/KeyboardContext.js @@ -74,4 +74,21 @@ export function useKeyboard(onKeyDown, onKeyUp) { }); } +/** + * Handler to handle a blur event. Useful when using a shortcut that uses the Alt or Cmd + * @param {FocusEvent} onBlur + */ +export function useBlur(onBlur) { + useEffect(() => { + if (onBlur) { + window.addEventListener("blur", onBlur); + } + return () => { + if (onBlur) { + window.removeEventListener("blur", onBlur); + } + }; + }); +} + export default KeyboardContext; diff --git a/src/contexts/MapDataContext.js b/src/contexts/MapDataContext.js index a714be3..ddd8dce 100644 --- a/src/contexts/MapDataContext.js +++ b/src/contexts/MapDataContext.js @@ -6,13 +6,11 @@ import React, { useRef, } from "react"; import * as Comlink from "comlink"; -import { decode } from "@msgpack/msgpack"; +import { decode, encode } from "@msgpack/msgpack"; import { useAuth } from "./AuthContext"; import { useDatabase } from "./DatabaseContext"; -import DatabaseWorker from "worker-loader!../workers/DatabaseWorker"; // eslint-disable-line import/no-webpack-loader-syntax - import { maps as defaultMaps } from "../maps"; const MapDataContext = React.createContext(); @@ -29,10 +27,8 @@ const defaultMapState = { notes: {}, }; -const worker = Comlink.wrap(new DatabaseWorker()); - export function MapDataProvider({ children }) { - const { database, databaseStatus } = useDatabase(); + const { database, databaseStatus, worker } = useDatabase(); const { userId } = useAuth(); const [maps, setMaps] = useState([]); @@ -74,6 +70,7 @@ export function MapDataProvider({ children }) { let storedMaps = []; // Try to load maps with worker, fallback to database if failed const packedMaps = await worker.loadData("maps"); + // let packedMaps; if (packedMaps) { storedMaps = decode(packedMaps); } else { @@ -93,7 +90,7 @@ export function MapDataProvider({ children }) { } loadMaps(); - }, [userId, database, databaseStatus]); + }, [userId, database, databaseStatus, worker]); const mapsRef = useRef(maps); useEffect(() => { @@ -218,12 +215,21 @@ export function MapDataProvider({ children }) { */ const putMap = useCallback( async (map) => { - await database.table("maps").put(map); + // Attempt to use worker to put map to avoid UI lockup + const packedMap = encode(map); + const success = await worker.putData( + Comlink.transfer(packedMap, [packedMap.buffer]), + "maps", + false + ); + if (!success) { + await database.table("maps").put(map); + } if (map.owner !== userId) { await updateCache(); } }, - [database, updateCache, userId] + [database, updateCache, userId, worker] ); // Create DB observable to sync creating and deleting diff --git a/src/contexts/MapInteractionContext.js b/src/contexts/MapInteractionContext.js index 905b1fe..1132999 100644 --- a/src/contexts/MapInteractionContext.js +++ b/src/contexts/MapInteractionContext.js @@ -1,24 +1,125 @@ import React, { useContext } from "react"; +import useDebounce from "../hooks/useDebounce"; -const MapInteractionContext = React.createContext({ - stageScale: 1, - stageWidth: 1, - stageHeight: 1, - setPreventMapInteraction: () => {}, - mapWidth: 1, - mapHeight: 1, - interactionEmitter: null, -}); -export const MapInteractionProvider = MapInteractionContext.Provider; +export const StageScaleContext = React.createContext(); +export const DebouncedStageScaleContext = React.createContext(); +export const StageWidthContext = React.createContext(); +export const StageHeightContext = React.createContext(); +export const SetPreventMapInteractionContext = React.createContext(); +export const MapWidthContext = React.createContext(); +export const MapHeightContext = React.createContext(); +export const InteractionEmitterContext = React.createContext(); -export function useMapInteraction() { - const context = useContext(MapInteractionContext); +export function MapInteractionProvider({ value, children }) { + const { + stageScale, + stageWidth, + stageHeight, + setPreventMapInteraction, + mapWidth, + mapHeight, + interactionEmitter, + } = value; + const debouncedStageScale = useDebounce(stageScale, 200); + return ( + + + + + + + + + {children} + + + + + + + + + ); +} + +export function useInteractionEmitter() { + const context = useContext(InteractionEmitterContext); if (context === undefined) { throw new Error( - "useMapInteraction must be used within a MapInteractionProvider" + "useInteractionEmitter must be used within a MapInteractionProvider" ); } return context; } -export default MapInteractionContext; +export function useSetPreventMapInteraction() { + const context = useContext(SetPreventMapInteractionContext); + if (context === undefined) { + throw new Error( + "useSetPreventMapInteraction must be used within a MapInteractionProvider" + ); + } + return context; +} + +export function useStageWidth() { + const context = useContext(StageWidthContext); + if (context === undefined) { + throw new Error( + "useStageWidth must be used within a MapInteractionProvider" + ); + } + return context; +} + +export function useStageHeight() { + const context = useContext(StageHeightContext); + if (context === undefined) { + throw new Error( + "useStageHeight must be used within a MapInteractionProvider" + ); + } + return context; +} + +export function useMapWidth() { + const context = useContext(MapWidthContext); + if (context === undefined) { + throw new Error("useMapWidth must be used within a MapInteractionProvider"); + } + return context; +} + +export function useMapHeight() { + const context = useContext(MapHeightContext); + if (context === undefined) { + throw new Error( + "useMapHeight must be used within a MapInteractionProvider" + ); + } + return context; +} + +export function useStageScale() { + const context = useContext(StageScaleContext); + if (context === undefined) { + throw new Error( + "useStageScale must be used within a MapInteractionProvider" + ); + } + return context; +} + +export function useDebouncedStageScale() { + const context = useContext(DebouncedStageScaleContext); + if (context === undefined) { + throw new Error( + "useDebouncedStageScale must be used within a MapInteractionProvider" + ); + } + return context; +} diff --git a/src/contexts/PlayerContext.js b/src/contexts/PlayerContext.js index 6ae1d1c..26cd95c 100644 --- a/src/contexts/PlayerContext.js +++ b/src/contexts/PlayerContext.js @@ -62,32 +62,50 @@ export function PlayerProvider({ session, children }) { }, [playerState, database, databaseStatus]); useEffect(() => { - setPlayerState((prevState) => ({ - ...prevState, - userId, - })); + if (userId) { + setPlayerState((prevState) => { + if (prevState) { + return { + ...prevState, + userId, + }; + } + return prevState; + }); + } }, [userId, setPlayerState]); useEffect(() => { + function updateSessionId() { + setPlayerState((prevState) => { + if (prevState) { + return { + ...prevState, + sessionId: session.id, + }; + } + return prevState; + }); + } function handleSocketConnect() { // Set the player state to trigger a sync - setPlayerState({ ...playerState, sessionId: session.id }); + updateSessionId(); } function handleSocketStatus(status) { if (status === "joined") { - setPlayerState({ ...playerState, sessionId: session.id }); + updateSessionId(); } } session.on("status", handleSocketStatus); session.socket?.on("connect", handleSocketConnect); - session.socket?.on("reconnect", handleSocketConnect); + session.socket?.io.on("reconnect", handleSocketConnect); return () => { session.off("status", handleSocketStatus); session.socket?.off("connect", handleSocketConnect); - session.socket?.off("reconnect", handleSocketConnect); + session.socket?.io.off("reconnect", handleSocketConnect); }; }); diff --git a/src/contexts/TokenDataContext.js b/src/contexts/TokenDataContext.js index 97ca575..329a509 100644 --- a/src/contexts/TokenDataContext.js +++ b/src/contexts/TokenDataContext.js @@ -5,24 +5,19 @@ import React, { useCallback, useRef, } from "react"; -import * as Comlink from "comlink"; import { decode } from "@msgpack/msgpack"; import { useAuth } from "./AuthContext"; import { useDatabase } from "./DatabaseContext"; -import DatabaseWorker from "worker-loader!../workers/DatabaseWorker"; // eslint-disable-line import/no-webpack-loader-syntax - import { tokens as defaultTokens } from "../tokens"; const TokenDataContext = React.createContext(); const cachedTokenMax = 100; -const worker = Comlink.wrap(new DatabaseWorker()); - export function TokenDataProvider({ children }) { - const { database, databaseStatus } = useDatabase(); + const { database, databaseStatus, worker } = useDatabase(); const { userId } = useAuth(); /** @@ -71,7 +66,7 @@ export function TokenDataProvider({ children }) { } loadTokens(); - }, [userId, database, databaseStatus]); + }, [userId, database, databaseStatus, worker]); const tokensRef = useRef(tokens); useEffect(() => { @@ -135,7 +130,7 @@ export function TokenDataProvider({ children }) { const updateToken = useCallback( async (id, update) => { - const change = { ...update, lastModified: Date.now() }; + const change = { lastModified: Date.now(), ...update }; await database.table("tokens").update(id, change); }, [database] @@ -143,7 +138,7 @@ export function TokenDataProvider({ children }) { const updateTokens = useCallback( async (ids, update) => { - const change = { ...update, lastModified: Date.now() }; + const change = { lastModified: Date.now(), ...update }; await Promise.all( ids.map((id) => database.table("tokens").update(id, change)) ); diff --git a/src/database.js b/src/database.js index 31c4d69..5ed9ed2 100644 --- a/src/database.js +++ b/src/database.js @@ -1,6 +1,7 @@ // eslint-disable-next-line no-unused-vars import Dexie, { Version, DexieOptions } from "dexie"; import "dexie-observable"; +import shortid from "shortid"; import blobToBuffer from "./helpers/blobToBuffer"; import { getGridDefaultInset } from "./helpers/grid"; @@ -414,9 +415,25 @@ const versions = { 21(v) { v.stores({}); }, + // v1.8.1 - Shorten fog shape ids + 22(v) { + v.stores({}).upgrade((tx) => { + return tx + .table("states") + .toCollection() + .modify((state) => { + for (let id of Object.keys(state.fogShapes)) { + const newId = shortid.generate(); + state.fogShapes[newId] = state.fogShapes[id]; + state.fogShapes[newId].id = newId; + delete state.fogShapes[id]; + } + }); + }); + }, }; -const latestVersion = 21; +const latestVersion = 22; /** * Load versions onto a database up to a specific version number diff --git a/src/dice/Dice.js b/src/dice/Dice.js index 21dcbfa..23dee18 100644 --- a/src/dice/Dice.js +++ b/src/dice/Dice.js @@ -27,13 +27,15 @@ class Dice { const mesh = await this.loadMesh(source, material, scene); meshes[type] = mesh; }; - await addToMeshes("d4", d4Source); - await addToMeshes("d6", d6Source); - await addToMeshes("d8", d8Source); - await addToMeshes("d10", d10Source); - await addToMeshes("d12", d12Source); - await addToMeshes("d20", d20Source); - await addToMeshes("d100", d100Source); + await Promise.all([ + addToMeshes("d4", d4Source), + addToMeshes("d6", d6Source), + addToMeshes("d8", d8Source), + addToMeshes("d10", d10Source), + addToMeshes("d12", d12Source), + addToMeshes("d20", d20Source), + addToMeshes("d100", d100Source), + ]); return meshes; } @@ -51,9 +53,14 @@ class Dice { static async loadMaterial(materialName, textures, scene) { let pbr = new PBRMaterial(materialName, scene); - pbr.albedoTexture = await importTextureAsync(textures.albedo); - pbr.normalTexture = await importTextureAsync(textures.normal); - pbr.metallicTexture = await importTextureAsync(textures.metalRoughness); + let [albedo, normal, metalRoughness] = await Promise.all([ + importTextureAsync(textures.albedo), + importTextureAsync(textures.normal), + importTextureAsync(textures.metalRoughness), + ]); + pbr.albedoTexture = albedo; + pbr.normalTexture = normal; + pbr.metallicTexture = metalRoughness; pbr.useRoughnessFromMetallicTextureAlpha = false; pbr.useRoughnessFromMetallicTextureGreen = true; pbr.useMetallnessFromMetallicTextureBlue = true; diff --git a/src/dice/diceTray/DiceTray.js b/src/dice/diceTray/DiceTray.js index 72bd936..1e9ec71 100644 --- a/src/dice/diceTray/DiceTray.js +++ b/src/dice/diceTray/DiceTray.js @@ -117,17 +117,33 @@ class DiceTray { } async loadMeshes() { - this.singleMesh = ( - await SceneLoader.ImportMeshAsync("", singleMeshSource, "", this.scene) - ).meshes[1]; + let [ + singleMeshes, + doubleMeshes, + singleAlbedoTexture, + singleNormalTexture, + singleMetalRoughnessTexture, + doubleAlbedoTexture, + doubleNormalTexture, + doubleMetalRoughnessTexture, + ] = await Promise.all([ + SceneLoader.ImportMeshAsync("", singleMeshSource, "", this.scene), + SceneLoader.ImportMeshAsync("", doubleMeshSource, "", this.scene), + importTextureAsync(singleAlbedo), + importTextureAsync(singleNormal), + importTextureAsync(singleMetalRoughness), + importTextureAsync(doubleAlbedo), + importTextureAsync(doubleNormal), + importTextureAsync(doubleMetalRoughness), + ]); + + this.singleMesh = singleMeshes.meshes[1]; this.singleMesh.id = "dice_tray_single"; this.singleMesh.name = "dice_tray"; let singleMaterial = new PBRMaterial("dice_tray_mat_single", this.scene); - singleMaterial.albedoTexture = await importTextureAsync(singleAlbedo); - singleMaterial.normalTexture = await importTextureAsync(singleNormal); - singleMaterial.metallicTexture = await importTextureAsync( - singleMetalRoughness - ); + singleMaterial.albedoTexture = singleAlbedoTexture; + singleMaterial.normalTexture = singleNormalTexture; + singleMaterial.metallicTexture = singleMetalRoughnessTexture; singleMaterial.useRoughnessFromMetallicTextureAlpha = false; singleMaterial.useRoughnessFromMetallicTextureGreen = true; singleMaterial.useMetallnessFromMetallicTextureBlue = true; @@ -137,17 +153,13 @@ class DiceTray { this.shadowGenerator.addShadowCaster(this.singleMesh); this.singleMesh.isVisible = this.size === "single"; - this.doubleMesh = ( - await SceneLoader.ImportMeshAsync("", doubleMeshSource, "", this.scene) - ).meshes[1]; + this.doubleMesh = doubleMeshes.meshes[1]; this.doubleMesh.id = "dice_tray_double"; this.doubleMesh.name = "dice_tray"; let doubleMaterial = new PBRMaterial("dice_tray_mat_double", this.scene); - doubleMaterial.albedoTexture = await importTextureAsync(doubleAlbedo); - doubleMaterial.normalTexture = await importTextureAsync(doubleNormal); - doubleMaterial.metallicTexture = await importTextureAsync( - doubleMetalRoughness - ); + doubleMaterial.albedoTexture = doubleAlbedoTexture; + doubleMaterial.normalTexture = doubleNormalTexture; + doubleMaterial.metallicTexture = doubleMetalRoughnessTexture; doubleMaterial.useRoughnessFromMetallicTextureAlpha = false; doubleMaterial.useRoughnessFromMetallicTextureGreen = true; doubleMaterial.useMetallnessFromMetallicTextureBlue = true; diff --git a/src/dice/diceTray/doubleAlbedo.jpg b/src/dice/diceTray/doubleAlbedo.jpg index c38282b..aa6c65d 100644 Binary files a/src/dice/diceTray/doubleAlbedo.jpg and b/src/dice/diceTray/doubleAlbedo.jpg differ diff --git a/src/dice/diceTray/doubleMetalRoughness.jpg b/src/dice/diceTray/doubleMetalRoughness.jpg index 94eb5d2..53c6a74 100644 Binary files a/src/dice/diceTray/doubleMetalRoughness.jpg and b/src/dice/diceTray/doubleMetalRoughness.jpg differ diff --git a/src/dice/diceTray/doubleNormal.jpg b/src/dice/diceTray/doubleNormal.jpg index 3f2a266..44c683d 100644 Binary files a/src/dice/diceTray/doubleNormal.jpg and b/src/dice/diceTray/doubleNormal.jpg differ diff --git a/src/dice/diceTray/singleAlbedo.jpg b/src/dice/diceTray/singleAlbedo.jpg index 0e3bffe..5f8b8bd 100644 Binary files a/src/dice/diceTray/singleAlbedo.jpg and b/src/dice/diceTray/singleAlbedo.jpg differ diff --git a/src/dice/diceTray/singleMetalRoughness.jpg b/src/dice/diceTray/singleMetalRoughness.jpg index 20246f2..7555003 100644 Binary files a/src/dice/diceTray/singleMetalRoughness.jpg and b/src/dice/diceTray/singleMetalRoughness.jpg differ diff --git a/src/dice/diceTray/singleNormal.jpg b/src/dice/diceTray/singleNormal.jpg index 4d469f9..2ac8a54 100644 Binary files a/src/dice/diceTray/singleNormal.jpg and b/src/dice/diceTray/singleNormal.jpg differ diff --git a/src/dice/galaxy/albedo.jpg b/src/dice/galaxy/albedo.jpg index 4f5cd33..0556f13 100755 Binary files a/src/dice/galaxy/albedo.jpg and b/src/dice/galaxy/albedo.jpg differ diff --git a/src/dice/galaxy/metalRoughness.jpg b/src/dice/galaxy/metalRoughness.jpg index 2883f3e..97cce28 100755 Binary files a/src/dice/galaxy/metalRoughness.jpg and b/src/dice/galaxy/metalRoughness.jpg differ diff --git a/src/dice/galaxy/normal.jpg b/src/dice/galaxy/normal.jpg index ca1c7f2..93f7914 100755 Binary files a/src/dice/galaxy/normal.jpg and b/src/dice/galaxy/normal.jpg differ diff --git a/src/dice/galaxy/preview.png b/src/dice/galaxy/preview.png index d03da88..60898e0 100644 Binary files a/src/dice/galaxy/preview.png and b/src/dice/galaxy/preview.png differ diff --git a/src/dice/gemstone/GemstoneDice.js b/src/dice/gemstone/GemstoneDice.js index 1d2b0b3..4545912 100644 --- a/src/dice/gemstone/GemstoneDice.js +++ b/src/dice/gemstone/GemstoneDice.js @@ -20,9 +20,14 @@ class GemstoneDice extends Dice { static async loadMaterial(materialName, textures, scene) { let pbr = new PBRMaterial(materialName, scene); - pbr.albedoTexture = await importTextureAsync(textures.albedo); - pbr.normalTexture = await importTextureAsync(textures.normal); - pbr.metallicTexture = await importTextureAsync(textures.metalRoughness); + let [albedo, normal, metalRoughness] = await Promise.all([ + importTextureAsync(textures.albedo), + importTextureAsync(textures.normal), + importTextureAsync(textures.metalRoughness), + ]); + pbr.albedoTexture = albedo; + pbr.normalTexture = normal; + pbr.metallicTexture = metalRoughness; pbr.useRoughnessFromMetallicTextureAlpha = false; pbr.useRoughnessFromMetallicTextureGreen = true; pbr.useMetallnessFromMetallicTextureBlue = true; diff --git a/src/dice/gemstone/albedo.jpg b/src/dice/gemstone/albedo.jpg index afcc39d..c3d34fa 100755 Binary files a/src/dice/gemstone/albedo.jpg and b/src/dice/gemstone/albedo.jpg differ diff --git a/src/dice/gemstone/metalRoughness.jpg b/src/dice/gemstone/metalRoughness.jpg index ba996da..7983fc8 100755 Binary files a/src/dice/gemstone/metalRoughness.jpg and b/src/dice/gemstone/metalRoughness.jpg differ diff --git a/src/dice/gemstone/normal.jpg b/src/dice/gemstone/normal.jpg index a764626..f771dc0 100755 Binary files a/src/dice/gemstone/normal.jpg and b/src/dice/gemstone/normal.jpg differ diff --git a/src/dice/gemstone/preview.png b/src/dice/gemstone/preview.png index b5f0502..56e08ac 100644 Binary files a/src/dice/gemstone/preview.png and b/src/dice/gemstone/preview.png differ diff --git a/src/dice/glass/GlassDice.js b/src/dice/glass/GlassDice.js index 62a5693..ec063dc 100644 --- a/src/dice/glass/GlassDice.js +++ b/src/dice/glass/GlassDice.js @@ -20,8 +20,13 @@ class GlassDice extends Dice { static async loadMaterial(materialName, textures, scene) { let pbr = new PBRMaterial(materialName, scene); - pbr.albedoTexture = await importTextureAsync(textures.albedo); - pbr.normalTexture = await importTextureAsync(textures.normal); + let [albedo, normal, mask] = await Promise.all([ + importTextureAsync(textures.albedo), + importTextureAsync(textures.normal), + importTextureAsync(textures.mask), + ]); + pbr.albedoTexture = albedo; + pbr.normalTexture = normal; pbr.roughness = 0.25; pbr.metallic = 0; pbr.subSurface.isRefractionEnabled = true; @@ -32,7 +37,7 @@ class GlassDice extends Dice { pbr.subSurface.minimumThickness = 10; pbr.subSurface.maximumThickness = 10; pbr.subSurface.tintColor = new Color3(43 / 255, 1, 115 / 255); - pbr.subSurface.thicknessTexture = await importTextureAsync(textures.mask); + pbr.subSurface.thicknessTexture = mask; pbr.subSurface.useMaskFromThicknessTexture = true; return pbr; diff --git a/src/dice/glass/albedo.jpg b/src/dice/glass/albedo.jpg index 34dee44..3eb7f10 100755 Binary files a/src/dice/glass/albedo.jpg and b/src/dice/glass/albedo.jpg differ diff --git a/src/dice/glass/mask.png b/src/dice/glass/mask.png index cbd2286..f8a8e57 100644 Binary files a/src/dice/glass/mask.png and b/src/dice/glass/mask.png differ diff --git a/src/dice/glass/normal.jpg b/src/dice/glass/normal.jpg index c7bdb61..6f4e4ac 100755 Binary files a/src/dice/glass/normal.jpg and b/src/dice/glass/normal.jpg differ diff --git a/src/dice/glass/preview.png b/src/dice/glass/preview.png index f612ec2..26b9e88 100644 Binary files a/src/dice/glass/preview.png and b/src/dice/glass/preview.png differ diff --git a/src/dice/iron/albedo.jpg b/src/dice/iron/albedo.jpg index c3da36e..9840f1f 100755 Binary files a/src/dice/iron/albedo.jpg and b/src/dice/iron/albedo.jpg differ diff --git a/src/dice/iron/metalRoughness.jpg b/src/dice/iron/metalRoughness.jpg index b68015a..f48ec42 100755 Binary files a/src/dice/iron/metalRoughness.jpg and b/src/dice/iron/metalRoughness.jpg differ diff --git a/src/dice/iron/normal.jpg b/src/dice/iron/normal.jpg index 1770d3f..8a39d17 100755 Binary files a/src/dice/iron/normal.jpg and b/src/dice/iron/normal.jpg differ diff --git a/src/dice/iron/preview.png b/src/dice/iron/preview.png index ddc5885..94a0122 100644 Binary files a/src/dice/iron/preview.png and b/src/dice/iron/preview.png differ diff --git a/src/dice/nebula/albedo.jpg b/src/dice/nebula/albedo.jpg index b767726..d81d562 100755 Binary files a/src/dice/nebula/albedo.jpg and b/src/dice/nebula/albedo.jpg differ diff --git a/src/dice/nebula/metalRoughness.jpg b/src/dice/nebula/metalRoughness.jpg index 0843c12..378002b 100755 Binary files a/src/dice/nebula/metalRoughness.jpg and b/src/dice/nebula/metalRoughness.jpg differ diff --git a/src/dice/nebula/normal.jpg b/src/dice/nebula/normal.jpg index c86a993..ad0e857 100755 Binary files a/src/dice/nebula/normal.jpg and b/src/dice/nebula/normal.jpg differ diff --git a/src/dice/nebula/preview.png b/src/dice/nebula/preview.png index 992a1a4..fc887a2 100644 Binary files a/src/dice/nebula/preview.png and b/src/dice/nebula/preview.png differ diff --git a/src/dice/sunrise/albedo.jpg b/src/dice/sunrise/albedo.jpg index 3010bb5..40273d2 100755 Binary files a/src/dice/sunrise/albedo.jpg and b/src/dice/sunrise/albedo.jpg differ diff --git a/src/dice/sunrise/metalRoughness.jpg b/src/dice/sunrise/metalRoughness.jpg index face52a..d8a820f 100755 Binary files a/src/dice/sunrise/metalRoughness.jpg and b/src/dice/sunrise/metalRoughness.jpg differ diff --git a/src/dice/sunrise/normal.jpg b/src/dice/sunrise/normal.jpg index c6cd269..8dcc7b1 100755 Binary files a/src/dice/sunrise/normal.jpg and b/src/dice/sunrise/normal.jpg differ diff --git a/src/dice/sunrise/preview.png b/src/dice/sunrise/preview.png index afe95bb..859d20d 100644 Binary files a/src/dice/sunrise/preview.png and b/src/dice/sunrise/preview.png differ diff --git a/src/dice/sunset/albedo.jpg b/src/dice/sunset/albedo.jpg index d44c2c7..e3f9ac5 100755 Binary files a/src/dice/sunset/albedo.jpg and b/src/dice/sunset/albedo.jpg differ diff --git a/src/dice/sunset/metalRoughness.jpg b/src/dice/sunset/metalRoughness.jpg index c206d0d..6ca4da3 100755 Binary files a/src/dice/sunset/metalRoughness.jpg and b/src/dice/sunset/metalRoughness.jpg differ diff --git a/src/dice/sunset/normal.jpg b/src/dice/sunset/normal.jpg index b4b91e0..6a80908 100755 Binary files a/src/dice/sunset/normal.jpg and b/src/dice/sunset/normal.jpg differ diff --git a/src/dice/sunset/preview.png b/src/dice/sunset/preview.png index 64eb54c..a123cf3 100644 Binary files a/src/dice/sunset/preview.png and b/src/dice/sunset/preview.png differ diff --git a/src/dice/walnut/albedo.jpg b/src/dice/walnut/albedo.jpg index 798d3cd..6926eac 100755 Binary files a/src/dice/walnut/albedo.jpg and b/src/dice/walnut/albedo.jpg differ diff --git a/src/dice/walnut/metalRoughness.jpg b/src/dice/walnut/metalRoughness.jpg index 6d29a56..c0df087 100755 Binary files a/src/dice/walnut/metalRoughness.jpg and b/src/dice/walnut/metalRoughness.jpg differ diff --git a/src/dice/walnut/normal.jpg b/src/dice/walnut/normal.jpg index c67888e..9aa998c 100755 Binary files a/src/dice/walnut/normal.jpg and b/src/dice/walnut/normal.jpg differ diff --git a/src/dice/walnut/preview.png b/src/dice/walnut/preview.png index 2961f2a..a603357 100644 Binary files a/src/dice/walnut/preview.png and b/src/dice/walnut/preview.png differ diff --git a/src/docs/assets/CustomTokensAdvanced.jpg b/src/docs/assets/CustomTokensAdvanced.jpg index 881a7b7..67b7a1d 100644 Binary files a/src/docs/assets/CustomTokensAdvanced.jpg and b/src/docs/assets/CustomTokensAdvanced.jpg differ diff --git a/src/docs/assets/EditingMapsAdvanced.jpg b/src/docs/assets/EditingMapsAdvanced.jpg index efa71fd..ce8a5e3 100644 Binary files a/src/docs/assets/EditingMapsAdvanced.jpg and b/src/docs/assets/EditingMapsAdvanced.jpg differ diff --git a/src/docs/assets/Settings.jpg b/src/docs/assets/Settings.jpg index fef4ff7..016ce61 100644 Binary files a/src/docs/assets/Settings.jpg and b/src/docs/assets/Settings.jpg differ diff --git a/src/docs/faq/database.md b/src/docs/faq/database.md index 7372d9c..5fef0af 100644 --- a/src/docs/faq/database.md +++ b/src/docs/faq/database.md @@ -2,6 +2,6 @@ --- -### Database is diasbled. +### Database is disabled. -Owlbear Rodeo uses a local database to store saved data. If you are seeing a database is disabled message this usually means you have data storage disabled. The most common occurrences of this is if you are using Private Browsing modes or in Firefox have the Never Remember History option enabled. The site will still function in these cases however all data will be lost when the page closes or reloads. \ No newline at end of file +Owlbear Rodeo uses a local database to store saved data. If you are seeing a database is disabled message this usually means you have data storage disabled. The most common occurrences of this is if you are using Private Browsing modes or in Firefox have the Never Remember History option enabled. The site will still function in these cases however all data will be lost when the page closes or reloads. diff --git a/src/docs/faq/maps.md b/src/docs/faq/maps.md index d6e35e5..d5cada1 100644 --- a/src/docs/faq/maps.md +++ b/src/docs/faq/maps.md @@ -14,7 +14,7 @@ This means that the token hasn’t loaded in just yet. If they still haven’t l ### How big can my maps be? -Owlbear Rodeo doesn't impose a limit on map sizes but keep in mind the larger the map you upload the longer it will take for your players to load. We recommend trying to keep your maps under 10MB with a good internet connection and under 5MB with slower internet. If you accidently upload a map that is too big you can use the quality option in the map's settings to lower the size without needing to re-upload your map. +Owlbear Rodeo has a map size limit of 50MB but keep in mind the larger the map you upload the longer it will take for your players to load. We recommend trying to keep your maps under 10MB in size and under 8000px on the largest edge in resolution. If you accidently upload a map that is too big you can use the quality option in the map's settings to lower the size without needing to re-upload your map. ### Where are my maps stored? @@ -31,3 +31,7 @@ We encourage users to create games every 24hrs. Any maps you have added or made ### I can join my game but the spinner is constantly loading, why? This could mean that the service is currently down. Please visit us on Twitter or Reddit and let us know. + +### Unable to find owner for map? + +If you see a message labelled "Unable to find owner for map" please ensure that the owner of the map is connected to the game. If they are, they may need to refresh their browser to reconnect. diff --git a/src/docs/howTo/shortcuts.md b/src/docs/howTo/shortcuts.md index 1155cee..8d0f823 100644 --- a/src/docs/howTo/shortcuts.md +++ b/src/docs/howTo/shortcuts.md @@ -1,16 +1,17 @@ ## General -| Shortcut | Description | -| ---------------- | ------------ | -| W | Move Tool | -| Space Bar (Hold) | Move Tool | -| F | Fog Tool | -| D | Drawing Tool | -| M | Measure Tool | -| Q | Pointer Tool | -| N | Note Tool | -| + | Zoom In | -| - | Zoom Out | +| Shortcut | Description | +| ---------------- | -------------- | +| W | Move Tool | +| Space Bar (Hold) | Move Tool | +| F | Fog Tool | +| D | Drawing Tool | +| M | Measure Tool | +| Q | Pointer Tool | +| N | Note Tool | +| + | Zoom In | +| - | Zoom Out | +| Shift + Zoom | Precision Zoom | ## Fog Tool diff --git a/src/docs/releaseNotes/v1.8.0.md b/src/docs/releaseNotes/v1.8.0.md index e162ff9..f4f6900 100644 --- a/src/docs/releaseNotes/v1.8.0.md +++ b/src/docs/releaseNotes/v1.8.0.md @@ -1,3 +1,5 @@ +[embed:](https://www.youtube.com/embed/MBy0VLsesL0) + ## Major Changes ### Hex Grid Support diff --git a/src/docs/releaseNotes/v1.8.1.md b/src/docs/releaseNotes/v1.8.1.md new file mode 100644 index 0000000..30bf902 --- /dev/null +++ b/src/docs/releaseNotes/v1.8.1.md @@ -0,0 +1,33 @@ +## Minor Changes + +This release focuses on improving performance for map zooming and asset loading. + +- Custom tokens now share more data between each instance of a token saving on memory. +- Zooming is more efficient by being better at choosing which elements need to be re-rendered. +- Default image assets have been further compressed to save on network bandwidth. +- Dice loading is now quicker and more efficient. +- Browser compatibility has been expanded by supporting older browsers that previously didn't work with moving maps or interacting with dice. +- Sending larger maps to players will no longer hitch the UI while the map is cached. +- Added a file size limit of 50MB to maps and tokens to prevent accidental browser crashes due to large file uploads. +- Importing maps and tokens will now give you a warning when trying to import images that are over 20MB in size. +- Using the fog brush tool has been optimised and is now less likely to result in small fog shapes on intersections. +- Fixed issue of not being able to draw a fog shape inside a ring of fog. +- Fixed a bug that could cause maps to be corrupted when an item was deleted at the same time it was edited. +- Added hover text for tokens in the token sidebar. +- Fixed an issue that would cause a double zoom when using a mouse wheel. +- Tool shortcuts now work with caps lock enabled. +- Added more messages for database issues or when a map owner can't be found. +- The dice select screen now better matches the other asset selectors. +- Fixed typo in the FAQ and added more detail for map loading issues. +- Fixed an issue with note text size not updating for players when changing between hidden and visible. +- Added a new accessibility option to control the opacity of fog while editing it. +- Added a new accessibility option to disable fog guides. +- You can now clear a map from being shared by clicking the select map button with no maps highlighted. +- Shared dice rolls now only show icons for the dice that have been rolled in the expanded view mode. +- Holding down the Shift key while zooming will now activate a precision zoom mode, allowing mouse users to more precisely zoom maps. +- The game reconnection message now has a more compact UI. +- Added notification for application updates. + +--- + +April 21 2021 diff --git a/src/helpers/KonvaBridge.js b/src/helpers/KonvaBridge.js new file mode 100644 index 0000000..b8fb3db --- /dev/null +++ b/src/helpers/KonvaBridge.js @@ -0,0 +1,143 @@ +import React, { useContext } from "react"; + +import { + InteractionEmitterContext, + SetPreventMapInteractionContext, + StageWidthContext, + StageHeightContext, + MapWidthContext, + MapHeightContext, + StageScaleContext, + DebouncedStageScaleContext, + useInteractionEmitter, + useSetPreventMapInteraction, + useStageWidth, + useStageHeight, + useMapWidth, + useMapHeight, + useStageScale, + useDebouncedStageScale, +} from "../contexts/MapInteractionContext"; +import { MapStageProvider, useMapStage } from "../contexts/MapStageContext"; +import AuthContext, { useAuth } from "../contexts/AuthContext"; +import SettingsContext, { useSettings } from "../contexts/SettingsContext"; +import KeyboardContext from "../contexts/KeyboardContext"; +import TokenDataContext, { useTokenData } from "../contexts/TokenDataContext"; +import { + ImageSourcesStateContext, + ImageSourcesUpdaterContext, +} from "../contexts/ImageSourceContext"; +import { + useGrid, + useGridCellPixelSize, + useGridCellNormalizedSize, + useGridStrokeWidth, + useGridCellPixelOffset, + useGridOffset, + useGridPixelSize, + GridContext, + GridPixelSizeContext, + GridCellPixelSizeContext, + GridCellNormalizedSizeContext, + GridOffsetContext, + GridStrokeWidthContext, + GridCellPixelOffsetContext, +} from "../contexts/GridContext"; + +/** + * Provide a bridge for konva that forwards our contexts + */ +function KonvaBridge({ stageRender, children }) { + const mapStageRef = useMapStage(); + const auth = useAuth(); + const settings = useSettings(); + const tokenData = useTokenData(); + const imageSources = useContext(ImageSourcesStateContext); + const setImageSources = useContext(ImageSourcesUpdaterContext); + const keyboardValue = useContext(KeyboardContext); + + const stageScale = useStageScale(); + const stageWidth = useStageWidth(); + const stageHeight = useStageHeight(); + const setPreventMapInteraction = useSetPreventMapInteraction(); + const mapWidth = useMapWidth(); + const mapHeight = useMapHeight(); + const interactionEmitter = useInteractionEmitter(); + const debouncedStageScale = useDebouncedStageScale(); + + const grid = useGrid(); + const gridPixelSize = useGridPixelSize(); + const gridCellNormalizedSize = useGridCellNormalizedSize(); + const gridCellPixelSize = useGridCellPixelSize(); + const gridStrokeWidth = useGridStrokeWidth(); + const gridCellPixelOffset = useGridCellPixelOffset(); + const gridOffset = useGridOffset(); + + return stageRender( + + + + + + + + + + + + + + + + + + + + + + + {children} + + + + + + + + + + + + + + + + + + + + + + + ); +} + +export default KonvaBridge; diff --git a/src/helpers/actions.js b/src/helpers/actions.js index 58fb2e9..44c51b7 100644 --- a/src/helpers/actions.js +++ b/src/helpers/actions.js @@ -1,6 +1,8 @@ +import shortid from "shortid"; + export function addPolygonDifferenceToShapes(shape, difference, shapes) { for (let i = 0; i < difference.length; i++) { - let newId = `${shape.id}-dif-${i}`; + let newId = shortid.generate(); // Holes detected let holes = []; if (difference[i].length > 1) { @@ -24,7 +26,7 @@ export function addPolygonDifferenceToShapes(shape, difference, shapes) { export function addPolygonIntersectionToShapes(shape, intersection, shapes) { for (let i = 0; i < intersection.length; i++) { - let newId = `${shape.id}-int-${i}`; + let newId = shortid.generate(); const points = intersection[i][0].map(([x, y]) => ({ x, y })); diff --git a/src/helpers/diff.js b/src/helpers/diff.js index bb36900..c1b503a 100644 --- a/src/helpers/diff.js +++ b/src/helpers/diff.js @@ -1,8 +1,17 @@ import { applyChange, revertChange, diff as deepDiff } from "deep-diff"; +import get from "lodash.get"; export function applyChanges(target, changes) { for (let change of changes) { - applyChange(target, true, change); + if (change.path && (change.kind === "E" || change.kind === "A")) { + // If editing an object or array ensure that the value exists + const valid = get(target, change.path) !== undefined; + if (valid) { + applyChange(target, true, change); + } + } else { + applyChange(target, true, change); + } } } diff --git a/src/helpers/drawing.js b/src/helpers/drawing.js index 9f3f595..4491d61 100644 --- a/src/helpers/drawing.js +++ b/src/helpers/drawing.js @@ -260,11 +260,15 @@ export function mergeFogShapes(shapes, ignoreHidden = true) { /** * @param {Fog[]} shapes + * @param {boolean} maxPoints Max amount of points per shape to get bounds for * @returns {Vector2.BoundingBox[]} */ -export function getFogShapesBoundingBoxes(shapes) { +export function getFogShapesBoundingBoxes(shapes, maxPoints = 0) { let boxes = []; for (let shape of shapes) { + if (maxPoints > 0 && shape.data.points.length > maxPoints) { + continue; + } boxes.push(Vector2.getBoundingBox(shape.data.points)); } return boxes; diff --git a/src/hooks/useDataSource.js b/src/hooks/useDataSource.js deleted file mode 100644 index 6feb5a0..0000000 --- a/src/hooks/useDataSource.js +++ /dev/null @@ -1,54 +0,0 @@ -import { useEffect, useState } from "react"; - -// Helper function to load either file or default data -// into a URL and ensure that it is revoked if needed -function useDataSource(data, defaultSources, unknownSource) { - const [dataSource, setDataSource] = useState(null); - useEffect(() => { - if (!data) { - setDataSource(unknownSource); - return; - } - let url = unknownSource; - if (data.type === "file") { - if (data.resolutions) { - // Check is a resolution is specified - if (data.quality && data.resolutions[data.quality]) { - url = URL.createObjectURL( - new Blob([data.resolutions[data.quality].file]) - ); - } - // If no file available fallback to the highest resolution - else if (!data.file) { - const resolutionArray = Object.keys(data.resolutions); - url = URL.createObjectURL( - new Blob([ - data.resolutions[resolutionArray[resolutionArray.length - 1]] - .file, - ]) - ); - } else { - url = URL.createObjectURL(new Blob([data.file])); - } - } else { - url = URL.createObjectURL(new Blob([data.file])); - } - } else if (data.type === "default") { - url = defaultSources[data.key]; - } - setDataSource(url); - - return () => { - if (data.type === "file" && url) { - // Remove file url after 5 seconds as we still may be using it while the next image loads - setTimeout(() => { - URL.revokeObjectURL(url); - }, 5000); - } - }; - }, [data, defaultSources, unknownSource]); - - return dataSource; -} - -export default useDataSource; diff --git a/src/hooks/useGridSnapping.js b/src/hooks/useGridSnapping.js index ddaaee9..9c1a85c 100644 --- a/src/hooks/useGridSnapping.js +++ b/src/hooks/useGridSnapping.js @@ -7,7 +7,12 @@ import { import useSetting from "./useSetting"; -import { useGrid } from "../contexts/GridContext"; +import { + useGrid, + useGridOffset, + useGridCellPixelSize, + useGridCellPixelOffset, +} from "../contexts/GridContext"; /** * Returns a function that when called will snap a node to the current grid @@ -19,12 +24,10 @@ function useGridSnapping(snappingSensitivity) { ); snappingSensitivity = snappingSensitivity || defaultSnappingSensitivity; - const { - grid, - gridOffset, - gridCellPixelSize, - gridCellPixelOffset, - } = useGrid(); + const grid = useGrid(); + const gridOffset = useGridOffset(); + const gridCellPixelSize = useGridCellPixelSize(); + const gridCellPixelOffset = useGridCellPixelOffset(); /** * @param {Vector2} node The node to snap diff --git a/src/hooks/useMapImage.js b/src/hooks/useMapImage.js index db28781..a67dc2b 100644 --- a/src/hooks/useMapImage.js +++ b/src/hooks/useMapImage.js @@ -1,12 +1,12 @@ import { useEffect, useState } from "react"; import useImage from "use-image"; -import useDataSource from "./useDataSource"; +import { useImageSource } from "../contexts/ImageSourceContext"; import { mapSources as defaultMapSources } from "../maps"; function useMapImage(map) { - const mapSource = useDataSource(map, defaultMapSources); + const mapSource = useImageSource(map, defaultMapSources); const [mapSourceImage, mapSourceImageStatus] = useImage(mapSource); // Create a map source that only updates when the image is fully loaded diff --git a/src/hooks/useStageInteraction.js b/src/hooks/useStageInteraction.js index 17714ee..a35d69c 100644 --- a/src/hooks/useStageInteraction.js +++ b/src/hooks/useStageInteraction.js @@ -1,8 +1,10 @@ -import { useRef, useEffect } from "react"; +import { useRef, useEffect, useState } from "react"; import { useGesture } from "react-use-gesture"; import normalizeWheel from "normalize-wheel"; -import { useKeyboard } from "../contexts/KeyboardContext"; +import { useKeyboard, useBlur } from "../contexts/KeyboardContext"; + +import shortcuts from "../shortcuts"; const wheelZoomSpeed = -1; const touchZoomSpeed = 0.005; @@ -23,6 +25,8 @@ function useStageInteraction( const pinchPreviousDistanceRef = useRef(); const pinchPreviousOriginRef = useRef(); + const [zoomSpeed, setZoomSpeed] = useState(1); + // Prevent accessibility pinch to zoom on Mac useEffect(() => { function handleGesture(e) { @@ -49,29 +53,34 @@ function useStageInteraction( if (preventInteraction || !isInteractingWithCanvas.current) { return; } - const { event } = props; - const { pixelY } = normalizeWheel(event); + const { event, last } = props; + if (!last) { + const { pixelY } = normalizeWheel(event); - const newScale = Math.min( - Math.max( - stageScale + - (pixelY * wheelZoomSpeed * stageScale) / window.innerHeight, - minZoom - ), - maxZoom - ); + const newScale = Math.min( + Math.max( + stageScale + + (pixelY * wheelZoomSpeed * stageScale * zoomSpeed) / + window.innerHeight, + minZoom + ), + maxZoom + ); - // Center on pointer - const pointer = stage.getPointerPosition(); - const newTranslate = { - x: pointer.x - ((pointer.x - stage.x()) / stageScale) * newScale, - y: pointer.y - ((pointer.y - stage.y()) / stageScale) * newScale, - }; + // Center on pointer + const pointer = stage.getPointerPosition(); + const newTranslate = { + x: pointer.x - ((pointer.x - stage.x()) / stageScale) * newScale, + y: pointer.y - ((pointer.y - stage.y()) / stageScale) * newScale, + }; - stage.position(newTranslate); - stageTranslateRef.current = newTranslate; + stage.position(newTranslate); + + stageTranslateRef.current = newTranslate; + + onStageScaleChange(newScale); + } - onStageScaleChange(newScale); gesture.onWheel && gesture.onWheel(props); }, onPinchStart: (props) => { @@ -99,7 +108,8 @@ function useStageInteraction( const originYDelta = originY - pinchPreviousOriginRef.current.y; const newScale = Math.min( Math.max( - stageScale + distanceDelta * touchZoomSpeed * stageScale, + stageScale + + distanceDelta * touchZoomSpeed * stageScale * zoomSpeed, minZoom ), maxZoom @@ -182,17 +192,13 @@ function useStageInteraction( if (preventInteraction) { return; } - const { key, ctrlKey, metaKey } = event; - if ( - (key === "=" || key === "+" || key === "-" || key === "_") && - !ctrlKey && - !metaKey - ) { - const pixelY = key === "=" || key === "+" ? -100 : 100; + if (shortcuts.stageZoomIn(event) || shortcuts.stageZoomOut(event)) { + const pixelY = shortcuts.stageZoomIn(event) ? -100 : 100; const newScale = Math.min( Math.max( stageScale + - (pixelY * wheelZoomSpeed * stageScale) / window.innerHeight, + (pixelY * wheelZoomSpeed * stageScale * zoomSpeed) / + window.innerHeight, minZoom ), maxZoom @@ -210,9 +216,25 @@ function useStageInteraction( onStageScaleChange(newScale); } + + if (shortcuts.stagePrecisionZoom(event)) { + setZoomSpeed(0.25); + } } - useKeyboard(handleKeyDown); + function handleKeyUp(event) { + if (shortcuts.stagePrecisionZoom(event)) { + setZoomSpeed(1); + } + } + + useKeyboard(handleKeyDown, handleKeyUp); + + function handleBlur() { + setZoomSpeed(1); + } + + useBlur(handleBlur); } export default useStageInteraction; diff --git a/src/icons/OfflineIcon.js b/src/icons/OfflineIcon.js new file mode 100644 index 0000000..75e1c05 --- /dev/null +++ b/src/icons/OfflineIcon.js @@ -0,0 +1,18 @@ +import React from "react"; + +function PointerToolIcon() { + return ( + + + + + ); +} + +export default PointerToolIcon; diff --git a/src/icons/ReconnectingIcon.js b/src/icons/ReconnectingIcon.js new file mode 100644 index 0000000..b7ccdc9 --- /dev/null +++ b/src/icons/ReconnectingIcon.js @@ -0,0 +1,28 @@ +import React from "react"; + +function ReconnectingIcon() { + return ( + + + + + + + ); +} + +export default ReconnectingIcon; diff --git a/src/images/DiagonalPattern.png b/src/images/DiagonalPattern.png index 5d44de9..55b3b17 100644 Binary files a/src/images/DiagonalPattern.png and b/src/images/DiagonalPattern.png differ diff --git a/src/images/HexPatternDark.png b/src/images/HexPatternDark.png index 5059cfd..8115190 100644 Binary files a/src/images/HexPatternDark.png and b/src/images/HexPatternDark.png differ diff --git a/src/images/HexPatternLight.png b/src/images/HexPatternLight.png index 94cf3a8..3fb7ec9 100644 Binary files a/src/images/HexPatternLight.png and b/src/images/HexPatternLight.png differ diff --git a/src/images/Loading.png b/src/images/Loading.png index 5c76e04..5ac7beb 100644 Binary files a/src/images/Loading.png and b/src/images/Loading.png differ diff --git a/src/images/Owlington.png b/src/images/Owlington.png index 191ac5f..5df4190 100644 Binary files a/src/images/Owlington.png and b/src/images/Owlington.png differ diff --git a/src/images/SquarePatternDark.png b/src/images/SquarePatternDark.png index 593aad6..7cd6278 100644 Binary files a/src/images/SquarePatternDark.png and b/src/images/SquarePatternDark.png differ diff --git a/src/images/SquarePatternLight.png b/src/images/SquarePatternLight.png index ad982f9..08e888b 100644 Binary files a/src/images/SquarePatternLight.png and b/src/images/SquarePatternLight.png differ diff --git a/src/index.js b/src/index.js index 09d6215..d21cd5b 100644 --- a/src/index.js +++ b/src/index.js @@ -11,10 +11,14 @@ import * as serviceWorker from "./serviceWorker"; import "./index.css"; +// Pointer events shim +if (!("PointerEvent" in window)) { + import("pepjs"); +} + if (process.env.REACT_APP_LOGGING === "true") { Sentry.init({ - dsn: - "https://971b4b9bd0004a2693653a2e5f97fa3d@o467475.ingest.sentry.io/5493956", + dsn: process.env.REACT_APP_SENTRY_DSN, release: "owlbear-rodeo@" + process.env.REACT_APP_VERSION, // Ignore resize error as it is triggered by going fullscreen on slower computers // Ignore quota error @@ -28,8 +32,46 @@ if (process.env.REACT_APP_LOGGING === "true") { "XDR encoding failure", "Assertion failed: Input argument is not an HTMLInputElement", "Extension context invalidated", - "InvalidStateError", - "Failed to execute 'transaction' on 'IDBDatabase': The database connection is closing.", + new RegExp( + "([InvalidStateError:\\s]*Failed to execute 'transaction' on 'IDBDatabase': The database connection is closing([.]*[\\s]*))+" + ), + // Random plugins/extensions + "top.GLOBALS", + // See: http://blog.errorception.com/2012/03/tale-of-unfindable-js-error.html + "originalCreateNotification", + "canvas.contentDocument", + "MyApp_RemoveAllHighlights", + "http://tt.epicplay.com", + "Can't find variable: ZiteReader", + "jigsaw is not defined", + "ComboSearch is not defined", + "http://loading.retry.widdit.com/", + "atomicFindClose", + // Facebook borked + "fb_xd_fragment", + // ISP "optimizing" proxy - `Cache-Control: no-transform` seems to + // reduce this. (thanks @acdha) + // See http://stackoverflow.com/questions/4113268 + "bmi_SafeAddOnload", + "EBCallBackMessageReceived", + // See http://toolbar.conduit.com/Developer/HtmlAndGadget/Methods/JSInjection.aspx + "conduitPage", + ], + denyUrls: [ + // Facebook flakiness + /graph\.facebook\.com/i, + // Facebook blocked + /connect\.facebook\.net\/en_US\/all\.js/i, + // Woopra flakiness + /eatdifferent\.com\.woopra-ns\.com/i, + /static\.woopra\.com\/js\/woopra\.js/i, + // Chrome extensions + /extensions\//i, + /^chrome:\/\//i, + // Other plugins + /127\.0\.0\.1:4001\/isrunning/i, // Cacaoweb + /webappstoolbarba\.texthelp\.com\//i, + /metrics\.itunes\.apple\.com\.edgesuite\.net\//i, ], }); } diff --git a/src/maps/Blank Grid 22x22.jpg b/src/maps/Blank Grid 22x22.jpg index 317b171..55da0c9 100755 Binary files a/src/maps/Blank Grid 22x22.jpg and b/src/maps/Blank Grid 22x22.jpg differ diff --git a/src/maps/Grass Grid 22x22.jpg b/src/maps/Grass Grid 22x22.jpg index 93a20fc..592f5f0 100755 Binary files a/src/maps/Grass Grid 22x22.jpg and b/src/maps/Grass Grid 22x22.jpg differ diff --git a/src/maps/Sand Grid 22x22.jpg b/src/maps/Sand Grid 22x22.jpg index 41995c3..9985172 100755 Binary files a/src/maps/Sand Grid 22x22.jpg and b/src/maps/Sand Grid 22x22.jpg differ diff --git a/src/maps/Stone Grid 22x22.jpg b/src/maps/Stone Grid 22x22.jpg index 1748205..7e3be20 100755 Binary files a/src/maps/Stone Grid 22x22.jpg and b/src/maps/Stone Grid 22x22.jpg differ diff --git a/src/maps/Unknown Grid 22x22.jpg b/src/maps/Unknown Grid 22x22.jpg index d4e9933..e538af1 100644 Binary files a/src/maps/Unknown Grid 22x22.jpg and b/src/maps/Unknown Grid 22x22.jpg differ diff --git a/src/maps/Water Grid 22x22.jpg b/src/maps/Water Grid 22x22.jpg index 83fbc3e..1525aac 100755 Binary files a/src/maps/Water Grid 22x22.jpg and b/src/maps/Water Grid 22x22.jpg differ diff --git a/src/maps/Wood Grid 22x22.jpg b/src/maps/Wood Grid 22x22.jpg index 5aae7a6..a1074f3 100755 Binary files a/src/maps/Wood Grid 22x22.jpg and b/src/maps/Wood Grid 22x22.jpg differ diff --git a/src/modals/ForceUpdateModal.js b/src/modals/ForceUpdateModal.js new file mode 100644 index 0000000..7edf7bc --- /dev/null +++ b/src/modals/ForceUpdateModal.js @@ -0,0 +1,23 @@ +import React from "react"; +import { Box, Label, Text } from "theme-ui"; + +import Modal from "../components/Modal"; + +function ForceUpdateModal({ isOpen }) { + return ( + + + + + Please refresh your browser to update to the latest version. + + + + ); +} + +export default ForceUpdateModal; diff --git a/src/modals/ImportExportModal.js b/src/modals/ImportExportModal.js index d7d33f7..b6e083c 100644 --- a/src/modals/ImportExportModal.js +++ b/src/modals/ImportExportModal.js @@ -8,21 +8,19 @@ import { useToasts } from "react-toast-notifications"; import Modal from "../components/Modal"; import LoadingOverlay from "../components/LoadingOverlay"; import LoadingBar from "../components/LoadingBar"; -import Banner from "../components/Banner"; +import ErrorBanner from "../components/banner/ErrorBanner"; import { useAuth } from "../contexts/AuthContext"; +import { useDatabase } from "../contexts/DatabaseContext"; import SelectDataModal from "./SelectDataModal"; import { getDatabase } from "../database"; -import DatabaseWorker from "worker-loader!../workers/DatabaseWorker"; // eslint-disable-line import/no-webpack-loader-syntax - -const worker = Comlink.wrap(new DatabaseWorker()); - const importDBName = "OwlbearRodeoImportDB"; function ImportExportModal({ isOpen, onRequestClose }) { + const { worker } = useDatabase(); const { userId } = useAuth(); const [isLoading, setIsLoading] = useState(false); @@ -117,7 +115,7 @@ function ImportExportModal({ isOpen, onRequestClose }) { } async function handleImportSelectorClose() { - const importDB = getDatabase({}, importDBName); + const importDB = getDatabase({ addons: [] }, importDBName); await importDB.delete(); importDB.close(); setShowImportSelector(false); @@ -129,7 +127,7 @@ function ImportExportModal({ isOpen, onRequestClose }) { setShowImportSelector(false); loadingProgressRef.current = 0; - const importDB = getDatabase({}, importDBName); + const importDB = getDatabase({ addons: [] }, importDBName); const db = getDatabase({}); try { // Keep track of a mapping of old token ids to new ones to apply them to the map states @@ -208,11 +206,12 @@ function ImportExportModal({ isOpen, onRequestClose }) { const tokenIds = checkedTokens.map((token) => token.id); try { - const blob = await worker.exportData( + const buffer = await worker.exportData( Comlink.proxy(handleDBProgress), mapIds, tokenIds ); + const blob = new Blob([buffer]); saveAs(blob, `${shortid.generate()}.owlbear`); addSuccessToast("Exported", checkedMaps, checkedTokens); } catch (e) { @@ -264,20 +263,16 @@ function ImportExportModal({ isOpen, onRequestClose }) { left: 0, }} > - - + + + + )} - setError()}> - - - Error: {error && error.message} - - - + setError()} /> - {isLoading && } + {isLoading && } ); diff --git a/src/modals/SelectDiceModal.js b/src/modals/SelectDiceModal.js index 81ecb50..693331e 100644 --- a/src/modals/SelectDiceModal.js +++ b/src/modals/SelectDiceModal.js @@ -6,13 +6,17 @@ import DiceTiles from "../components/dice/DiceTiles"; import { dice } from "../dice"; +import useResponsiveLayout from "../hooks/useResponsiveLayout"; + function SelectDiceModal({ isOpen, onRequestClose, onDone, defaultDice }) { const [selectedDice, setSelectedDice] = useState(defaultDice); + const layout = useResponsiveLayout(); + return ( 5e7) { + addToast(`Unable to import map ${file.name} as it is over 50MB`); + } else { + mapFiles.push(file); + } + } + + // Any file greater than 20MB + if (mapFiles.some((file) => file.size > 2e7)) { + largeImageWarningFiles.current = mapFiles; + setShowLargeImageWarning(true); + return; + } + + for (let file of mapFiles) { await handleImageUpload(file); } + + clearFileInput(); + } + + function clearFileInput() { // Set file input to null to allow adding the same image 2 times in a row if (fileInputRef.current) { fileInputRef.current.value = null; } } + function handleLargeImageWarningCancel() { + largeImageWarningFiles.current = undefined; + setShowLargeImageWarning(false); + clearFileInput(); + } + + async function handleLargeImageWarningConfirm() { + setShowLargeImageWarning(false); + const files = largeImageWarningFiles.current; + for (let file of files) { + await handleImageUpload(file); + } + largeImageWarningFiles.current = undefined; + clearFileInput(); + } + async function handleImageUpload(file) { if (!file) { return Promise.reject(); @@ -328,9 +375,11 @@ function SelectMapModal({ const map = selectedMaps[0]; const mapState = await getMapStateFromDB(map.id); if (map.type === "file") { + setIsLoading(true); await updateMap(map.id, { lastUsed }); const updatedMap = await getMapFromDB(map.id); onMapChange(updatedMap, mapState); + setIsLoading(false); } else { onMapChange(map, mapState); } @@ -343,17 +392,17 @@ function SelectMapModal({ /** * Shortcuts */ - function handleKeyDown({ key }) { + function handleKeyDown(event) { if (!isOpen) { return; } - if (key === "Shift") { + if (shortcuts.selectRange(event)) { setSelectMode("range"); } - if (key === "Control" || key === "Meta") { + if (shortcuts.selectMultiple(event)) { setSelectMode("multiple"); } - if (key === "Backspace" || key === "Delete") { + if (shortcuts.delete(event)) { // Selected maps and none are default if ( selectedMapIds.length > 0 && @@ -368,31 +417,26 @@ function SelectMapModal({ } } - function handleKeyUp({ key }) { + function handleKeyUp(event) { if (!isOpen) { return; } - if (key === "Shift" && selectMode === "range") { + if (shortcuts.selectRange(event) && selectMode === "range") { setSelectMode("single"); } - if ((key === "Control" || key === "Meta") && selectMode === "multiple") { + if (shortcuts.selectMultiple(event) && selectMode === "multiple") { setSelectMode("single"); } } useKeyboard(handleKeyDown, handleKeyUp); - // Set select mode to single when alt+tabing - useEffect(() => { - function handleBlur() { - setSelectMode("single"); - } + // Set select mode to single when cmd+tabing + function handleBlur() { + setSelectMode("single"); + } - window.addEventListener("blur", handleBlur); - return () => { - window.removeEventListener("blur", handleBlur); - }; - }, []); + useBlur(handleBlur); const layout = useResponsiveLayout(); @@ -438,7 +482,7 @@ function SelectMapModal({ /> @@ -316,6 +359,14 @@ function SelectTokensModal({ isOpen, onRequestClose }) { }`} description="This operation cannot be undone." /> + ); } diff --git a/src/modals/SettingsModal.js b/src/modals/SettingsModal.js index 45dadf7..1cc3db8 100644 --- a/src/modals/SettingsModal.js +++ b/src/modals/SettingsModal.js @@ -12,6 +12,7 @@ import prettyBytes from "pretty-bytes"; import Modal from "../components/Modal"; import Slider from "../components/Slider"; +import LoadingOverlay from "../components/LoadingOverlay"; import { useAuth } from "../contexts/AuthContext"; import { useDatabase } from "../contexts/DatabaseContext"; @@ -29,8 +30,11 @@ function SettingsModal({ isOpen, onRequestClose }) { const [gridSnappingSensitivity, setGridSnappingSensitivity] = useSetting( "map.gridSnappingSensitivity" ); + const [showFogGuides, setShowFogGuides] = useSetting("fog.showGuides"); + const [fogEditOpacity, setFogEditOpacity] = useSetting("fog.editOpacity"); const [storageEstimate, setStorageEstimate] = useState(); const [isImportExportModalOpen, setIsImportExportModalOpen] = useState(false); + const [isLoading, setIsLoading] = useState(false); useEffect(() => { async function estimateStorage() { @@ -52,12 +56,14 @@ function SettingsModal({ isOpen, onRequestClose }) { }, [isOpen]); async function handleEraseAllData() { + setIsLoading(true); localStorage.clear(); await database.delete(); window.location.reload(); } async function handleClearCache() { + setIsLoading(true); // Clear saved settings localStorage.clear(); // Clear map cache @@ -95,7 +101,7 @@ function SettingsModal({ isOpen, onRequestClose }) { + +