commit
e4b5551aa7
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "owlbear-rodeo",
|
||||
"version": "1.5.1",
|
||||
"version": "1.5.2",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@babylonjs/core": "^4.1.0",
|
||||
@ -18,6 +18,7 @@
|
||||
"konva": "^6.0.0",
|
||||
"lodash.get": "^4.4.2",
|
||||
"lodash.set": "^4.3.2",
|
||||
"normalize-wheel": "^1.0.1",
|
||||
"polygon-clipping": "^0.14.3",
|
||||
"raw.macro": "^0.3.0",
|
||||
"react": "^16.13.0",
|
||||
|
@ -7,6 +7,7 @@ function StyledModal({
|
||||
onRequestClose,
|
||||
children,
|
||||
allowClose,
|
||||
style,
|
||||
...props
|
||||
}) {
|
||||
const { theme } = useThemeUI();
|
||||
@ -26,6 +27,7 @@ function StyledModal({
|
||||
marginRight: "-50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
maxHeight: "100%",
|
||||
...style,
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
@ -44,6 +46,7 @@ function StyledModal({
|
||||
|
||||
StyledModal.defaultProps = {
|
||||
allowClose: true,
|
||||
style: {},
|
||||
};
|
||||
|
||||
export default StyledModal;
|
||||
|
@ -1,28 +1,34 @@
|
||||
import React from "react";
|
||||
import { Flex, Image, Text } from "theme-ui";
|
||||
import { Flex, Image, Text, Box } from "theme-ui";
|
||||
|
||||
function DiceTile({ dice, isSelected, onDiceSelect, onDone }) {
|
||||
function DiceTile({ dice, isSelected, onDiceSelect, onDone, large }) {
|
||||
return (
|
||||
<Flex
|
||||
onClick={() => onDiceSelect(dice)}
|
||||
sx={{
|
||||
borderColor: "primary",
|
||||
borderStyle: isSelected ? "solid" : "none",
|
||||
borderWidth: "4px",
|
||||
position: "relative",
|
||||
width: "150px",
|
||||
height: "150px",
|
||||
width: large ? "48%" : "32%",
|
||||
height: "0",
|
||||
paddingTop: large ? "48%" : "32%",
|
||||
borderRadius: "4px",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
m={2}
|
||||
my={1}
|
||||
mx={`${large ? 1 : 2 / 3}%`}
|
||||
bg="muted"
|
||||
onDoubleClick={() => onDone(dice)}
|
||||
>
|
||||
<Image
|
||||
sx={{ width: "100%", height: "100%", objectFit: "contain" }}
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "contain",
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
}}
|
||||
src={dice.preview}
|
||||
/>
|
||||
<Flex
|
||||
@ -48,6 +54,20 @@ function DiceTile({ dice, isSelected, onDiceSelect, onDone }) {
|
||||
{dice.name}
|
||||
</Text>
|
||||
</Flex>
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
borderColor: "primary",
|
||||
borderStyle: isSelected ? "solid" : "none",
|
||||
borderWidth: "4px",
|
||||
pointerEvents: "none",
|
||||
borderRadius: "4px",
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
@ -1,18 +1,20 @@
|
||||
import React from "react";
|
||||
import { Flex } from "theme-ui";
|
||||
import SimpleBar from "simplebar-react";
|
||||
import { useMedia } from "react-media";
|
||||
|
||||
import DiceTile from "./DiceTile";
|
||||
|
||||
function DiceTiles({ dice, onDiceSelect, selectedDice, onDone }) {
|
||||
const isSmallScreen = useMedia({ query: "(max-width: 500px)" });
|
||||
|
||||
return (
|
||||
<SimpleBar style={{ maxHeight: "300px", width: "500px" }}>
|
||||
<SimpleBar style={{ maxHeight: "300px" }}>
|
||||
<Flex
|
||||
py={2}
|
||||
p={2}
|
||||
bg="muted"
|
||||
sx={{
|
||||
flexWrap: "wrap",
|
||||
width: "500px",
|
||||
borderRadius: "4px",
|
||||
}}
|
||||
>
|
||||
@ -23,6 +25,7 @@ function DiceTiles({ dice, onDiceSelect, selectedDice, onDone }) {
|
||||
isSelected={selectedDice && dice.key === selectedDice.key}
|
||||
onDiceSelect={onDiceSelect}
|
||||
onDone={onDone}
|
||||
large={isSmallScreen}
|
||||
/>
|
||||
))}
|
||||
</Flex>
|
||||
|
@ -24,6 +24,7 @@ import DiceTray from "../../dice/diceTray/DiceTray";
|
||||
import DiceLoadingContext from "../../contexts/DiceLoadingContext";
|
||||
|
||||
import { getDiceRoll } from "../../helpers/dice";
|
||||
import useSetting from "../../helpers/useSetting";
|
||||
|
||||
function DiceTrayOverlay({
|
||||
isOpen,
|
||||
@ -45,6 +46,7 @@ function DiceTrayOverlay({
|
||||
const { assetLoadStart, assetLoadFinish, isLoading } = useContext(
|
||||
DiceLoadingContext
|
||||
);
|
||||
const [fullScreen] = useSetting("map.fullScreen");
|
||||
|
||||
function handleAssetLoadStart() {
|
||||
assetLoadStart();
|
||||
@ -236,6 +238,8 @@ function DiceTrayOverlay({
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
let renderTimeout;
|
||||
let renderCleanup;
|
||||
function handleResize() {
|
||||
const map = document.querySelector(".map");
|
||||
const mapRect = map.getBoundingClientRect();
|
||||
@ -251,6 +255,11 @@ function DiceTrayOverlay({
|
||||
height = diceTraySize === "single" ? width * 2 : width;
|
||||
}
|
||||
|
||||
// Debounce a timeout to force re-rendering on resize
|
||||
renderTimeout = setTimeout(() => {
|
||||
renderCleanup = forceRender();
|
||||
}, 100);
|
||||
|
||||
setTraySize({ width, height });
|
||||
}
|
||||
|
||||
@ -260,8 +269,14 @@ function DiceTrayOverlay({
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", handleResize);
|
||||
if (renderTimeout) {
|
||||
clearTimeout(renderTimeout);
|
||||
}
|
||||
if (renderCleanup) {
|
||||
renderCleanup();
|
||||
}
|
||||
};
|
||||
}, [diceTraySize]);
|
||||
}, [diceTraySize, fullScreen, isOpen]);
|
||||
|
||||
// Update dice rolls
|
||||
useEffect(() => {
|
||||
|
@ -237,7 +237,7 @@ function Map({
|
||||
}
|
||||
}
|
||||
|
||||
const mapTokens = mapState && (
|
||||
const mapTokens = map && mapState && (
|
||||
<Group>
|
||||
{Object.values(mapState.tokens)
|
||||
.sort((a, b) => sortMapTokenStates(a, b, draggingTokenOptions))
|
||||
|
@ -16,6 +16,10 @@ import BrushToolIcon from "../../icons/BrushToolIcon";
|
||||
import MeasureToolIcon from "../../icons/MeasureToolIcon";
|
||||
import ExpandMoreIcon from "../../icons/ExpandMoreIcon";
|
||||
import PointerToolIcon from "../../icons/PointerToolIcon";
|
||||
import FullScreenIcon from "../../icons/FullScreenIcon";
|
||||
import FullScreenExitIcon from "../../icons/FullScreenExitIcon";
|
||||
|
||||
import useSetting from "../../helpers/useSetting";
|
||||
|
||||
function MapContols({
|
||||
onMapChange,
|
||||
@ -31,6 +35,7 @@ function MapContols({
|
||||
disabledSettings,
|
||||
}) {
|
||||
const [isExpanded, setIsExpanded] = useState(true);
|
||||
const [fullScreen, setFullScreen] = useSetting("map.fullScreen");
|
||||
|
||||
const toolsById = {
|
||||
pan: {
|
||||
@ -190,6 +195,24 @@ function MapContols({
|
||||
{controls}
|
||||
</Flex>
|
||||
{getToolSettings()}
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
right: "4px",
|
||||
bottom: 0,
|
||||
backgroundColor: "overlay",
|
||||
borderRadius: "50%",
|
||||
}}
|
||||
m={2}
|
||||
>
|
||||
<IconButton
|
||||
onClick={() => setFullScreen(!fullScreen)}
|
||||
aria-label={fullScreen ? "Exit Full Screen" : "Enter Full Screen"}
|
||||
title={fullScreen ? "Exit Full Screen" : "Enter Full Screen"}
|
||||
>
|
||||
{fullScreen ? <FullScreenExitIcon /> : <FullScreenIcon />}
|
||||
</IconButton>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import ReactResizeDetector from "react-resize-detector";
|
||||
import useImage from "use-image";
|
||||
import { Stage, Layer, Image } from "react-konva";
|
||||
import { EventEmitter } from "events";
|
||||
import normalizeWheel from "normalize-wheel";
|
||||
|
||||
import usePreventOverscroll from "../../helpers/usePreventOverscroll";
|
||||
import useDataSource from "../../helpers/useDataSource";
|
||||
@ -16,6 +17,7 @@ import MapStageContext, {
|
||||
MapStageProvider,
|
||||
} from "../../contexts/MapStageContext";
|
||||
import AuthContext from "../../contexts/AuthContext";
|
||||
import SettingsContext from "../../contexts/SettingsContext";
|
||||
|
||||
const wheelZoomSpeed = -0.001;
|
||||
const touchZoomSpeed = 0.005;
|
||||
@ -108,12 +110,14 @@ function MapInteraction({
|
||||
isInteractingWithCanvas.current =
|
||||
event.target === mapLayerRef.current.getCanvas()._canvas;
|
||||
},
|
||||
onWheel: ({ delta }) => {
|
||||
onWheel: ({ event }) => {
|
||||
event.persist();
|
||||
const { pixelY } = normalizeWheel(event);
|
||||
if (preventMapInteraction || !isInteractingWithCanvas.current) {
|
||||
return;
|
||||
}
|
||||
const newScale = Math.min(
|
||||
Math.max(stageScale + delta[1] * wheelZoomSpeed, minZoom),
|
||||
Math.max(stageScale + pixelY * wheelZoomSpeed, minZoom),
|
||||
maxZoom
|
||||
);
|
||||
setStageScale(newScale);
|
||||
@ -310,6 +314,7 @@ function MapInteraction({
|
||||
const mapImageRef = useRef();
|
||||
|
||||
const auth = useContext(AuthContext);
|
||||
const settings = useContext(SettingsContext);
|
||||
|
||||
const mapInteraction = {
|
||||
stageScale,
|
||||
@ -354,11 +359,13 @@ function MapInteraction({
|
||||
/>
|
||||
{/* Forward auth context to konva elements */}
|
||||
<AuthContext.Provider value={auth}>
|
||||
<SettingsContext.Provider value={settings}>
|
||||
<MapInteractionProvider value={mapInteraction}>
|
||||
<MapStageProvider value={mapStageRef}>
|
||||
{mapLoaded && children}
|
||||
</MapStageProvider>
|
||||
</MapInteractionProvider>
|
||||
</SettingsContext.Provider>
|
||||
</AuthContext.Provider>
|
||||
</Layer>
|
||||
</Stage>
|
||||
|
@ -102,7 +102,7 @@ function MapSettings({
|
||||
</Box>
|
||||
<Flex
|
||||
mt={2}
|
||||
mb={map.type === "default" ? 2 : 0}
|
||||
mb={mapEmpty || map.type === "default" ? 2 : 0}
|
||||
sx={{ alignItems: "flex-end" }}
|
||||
>
|
||||
<Box sx={{ width: "50%" }}>
|
||||
@ -154,8 +154,9 @@ function MapSettings({
|
||||
key={quality.id}
|
||||
value={quality.id}
|
||||
disabled={
|
||||
quality.id !== "original" &&
|
||||
!map.resolutions[quality.id]
|
||||
mapEmpty ||
|
||||
(quality.id !== "original" &&
|
||||
!map.resolutions[quality.id])
|
||||
}
|
||||
>
|
||||
{quality.name}
|
||||
|
@ -16,6 +16,7 @@ function MapTile({
|
||||
onMapRemove,
|
||||
onMapReset,
|
||||
onDone,
|
||||
large,
|
||||
}) {
|
||||
const [isMapTileMenuOpen, setIsTileMenuOpen] = useState(false);
|
||||
const isDefault = map.type === "default";
|
||||
@ -46,7 +47,7 @@ function MapTile({
|
||||
}}
|
||||
bg="overlay"
|
||||
sx={{ borderRadius: "50%" }}
|
||||
m={1}
|
||||
m={2}
|
||||
>
|
||||
<ExpandMoreDotIcon />
|
||||
</IconButton>
|
||||
@ -65,7 +66,7 @@ function MapTile({
|
||||
}}
|
||||
bg="overlay"
|
||||
sx={{ borderRadius: "50%" }}
|
||||
m={1}
|
||||
m={2}
|
||||
>
|
||||
<RemoveMapIcon />
|
||||
</IconButton>
|
||||
@ -85,7 +86,7 @@ function MapTile({
|
||||
}}
|
||||
bg="overlay"
|
||||
sx={{ borderRadius: "50%" }}
|
||||
m={1}
|
||||
m={2}
|
||||
>
|
||||
<ResetMapIcon />
|
||||
</IconButton>
|
||||
@ -96,18 +97,18 @@ function MapTile({
|
||||
<Flex
|
||||
key={map.id}
|
||||
sx={{
|
||||
borderColor: "primary",
|
||||
borderStyle: isSelected ? "solid" : "none",
|
||||
borderWidth: "4px",
|
||||
position: "relative",
|
||||
width: "150px",
|
||||
height: "150px",
|
||||
width: large ? "48%" : "32%",
|
||||
height: "0",
|
||||
paddingTop: large ? "48%" : "32%",
|
||||
borderRadius: "4px",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
cursor: "pointer",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
m={2}
|
||||
my={1}
|
||||
mx={`${large ? 1 : 2 / 3}%`}
|
||||
bg="muted"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
@ -123,7 +124,14 @@ function MapTile({
|
||||
}}
|
||||
>
|
||||
<UIImage
|
||||
sx={{ width: "100%", height: "100%", objectFit: "contain" }}
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "contain",
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
}}
|
||||
src={mapSource}
|
||||
/>
|
||||
<Flex
|
||||
@ -149,6 +157,20 @@ function MapTile({
|
||||
{map.name}
|
||||
</Text>
|
||||
</Flex>
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
borderColor: "primary",
|
||||
borderStyle: isSelected ? "solid" : "none",
|
||||
borderWidth: "4px",
|
||||
pointerEvents: "none",
|
||||
borderRadius: "4px",
|
||||
}}
|
||||
/>
|
||||
{/* Show expand button only if both reset and remove is available */}
|
||||
{isSelected && (
|
||||
<Box sx={{ position: "absolute", top: 0, right: 0 }}>
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React, { useContext } from "react";
|
||||
import { Flex, Box, Text } from "theme-ui";
|
||||
import SimpleBar from "simplebar-react";
|
||||
import { useMedia } from "react-media";
|
||||
|
||||
import AddIcon from "../../icons/AddIcon";
|
||||
|
||||
@ -20,15 +21,16 @@ function MapTiles({
|
||||
onDone,
|
||||
}) {
|
||||
const { databaseStatus } = useContext(DatabaseContext);
|
||||
const isSmallScreen = useMedia({ query: "(max-width: 500px)" });
|
||||
|
||||
return (
|
||||
<Box sx={{ position: "relative" }}>
|
||||
<SimpleBar style={{ maxHeight: "300px", width: "500px" }}>
|
||||
<SimpleBar style={{ maxHeight: "300px" }}>
|
||||
<Flex
|
||||
py={2}
|
||||
p={2}
|
||||
bg="muted"
|
||||
sx={{
|
||||
flexWrap: "wrap",
|
||||
width: "500px",
|
||||
borderRadius: "4px",
|
||||
}}
|
||||
onClick={() => onMapSelect(null)}
|
||||
@ -45,20 +47,33 @@ function MapTiles({
|
||||
":active": {
|
||||
color: "secondary",
|
||||
},
|
||||
width: "150px",
|
||||
height: "150px",
|
||||
width: isSmallScreen ? "48%" : "32%",
|
||||
height: "0",
|
||||
paddingTop: isSmallScreen ? "48%" : "32%",
|
||||
borderRadius: "4px",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
position: "relative",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
m={2}
|
||||
my={1}
|
||||
mx={`${isSmallScreen ? 1 : 2 / 3}%`}
|
||||
bg="muted"
|
||||
aria-label="Add Map"
|
||||
title="Add Map"
|
||||
>
|
||||
<Flex
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<AddIcon large />
|
||||
</Flex>
|
||||
</Flex>
|
||||
{maps.map((map) => {
|
||||
const isSelected = selectedMap && map.id === selectedMap.id;
|
||||
return (
|
||||
@ -73,6 +88,7 @@ function MapTiles({
|
||||
onMapRemove={onMapRemove}
|
||||
onMapReset={onMapReset}
|
||||
onDone={onDone}
|
||||
large={isSmallScreen}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
@ -6,6 +6,8 @@ import DiceTrayOverlay from "../dice/DiceTrayOverlay";
|
||||
|
||||
import { DiceLoadingProvider } from "../../contexts/DiceLoadingContext";
|
||||
|
||||
import useSetting from "../../helpers/useSetting";
|
||||
|
||||
function DiceTrayButton({
|
||||
shareDice,
|
||||
onShareDiceChage,
|
||||
@ -13,13 +15,14 @@ function DiceTrayButton({
|
||||
onDiceRollsChange,
|
||||
}) {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const [fullScreen] = useSetting("map.fullScreen");
|
||||
|
||||
return (
|
||||
<Flex
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: "100%",
|
||||
left: fullScreen ? "0" : "100%",
|
||||
bottom: 0,
|
||||
flexDirection: "column",
|
||||
alignItems: "flex-start",
|
||||
|
@ -11,6 +11,8 @@ import StartTimerButton from "./StartTimerButton";
|
||||
import Timer from "./Timer";
|
||||
import DiceTrayButton from "./DiceTrayButton";
|
||||
|
||||
import useSetting from "../../helpers/useSetting";
|
||||
|
||||
function Party({
|
||||
nickname,
|
||||
partyNicknames,
|
||||
@ -30,18 +32,28 @@ function Party({
|
||||
onDiceRollsChange,
|
||||
partyDiceRolls,
|
||||
}) {
|
||||
const [fullScreen] = useSetting("map.fullScreen");
|
||||
|
||||
return (
|
||||
<Flex
|
||||
p={3}
|
||||
<Box
|
||||
bg="background"
|
||||
sx={{
|
||||
position: "relative",
|
||||
// width: fullScreen ? "0" : "112px",
|
||||
// minWidth: fullScreen ? "0" : "112px",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
flexDirection: "column",
|
||||
overflow: "visible",
|
||||
alignItems: "center",
|
||||
position: "relative",
|
||||
height: "100%",
|
||||
display: fullScreen ? "none" : "flex",
|
||||
width: "112px",
|
||||
minWidth: "112px",
|
||||
}}
|
||||
p={3}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
@ -84,7 +96,10 @@ function Party({
|
||||
))}
|
||||
</SimpleBar>
|
||||
<Flex sx={{ flexDirection: "column" }}>
|
||||
<ChangeNicknameButton nickname={nickname} onChange={onNicknameChange} />
|
||||
<ChangeNicknameButton
|
||||
nickname={nickname}
|
||||
onChange={onNicknameChange}
|
||||
/>
|
||||
<AddPartyMemberButton gameId={gameId} />
|
||||
<StartStreamButton
|
||||
onStreamStart={onStreamStart}
|
||||
@ -98,13 +113,14 @@ function Party({
|
||||
/>
|
||||
<SettingsButton />
|
||||
</Flex>
|
||||
</Box>
|
||||
<DiceTrayButton
|
||||
shareDice={shareDice}
|
||||
onShareDiceChage={onShareDiceChage}
|
||||
diceRolls={diceRolls}
|
||||
onDiceRollsChange={onDiceRollsChange}
|
||||
/>
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -34,6 +34,9 @@ function TokenDragOverlay({
|
||||
x: pointerPosition.x + mapRect.left,
|
||||
y: pointerPosition.y + mapRect.top,
|
||||
};
|
||||
if (!removeTokenRef.current) {
|
||||
return;
|
||||
}
|
||||
const removeIconPosition = removeTokenRef.current.getBoundingClientRect();
|
||||
|
||||
if (
|
||||
|
@ -1,15 +1,25 @@
|
||||
import React, { useRef, useEffect, useState } from "react";
|
||||
import { Rect, Text, Group } from "react-konva";
|
||||
|
||||
import useSetting from "../../helpers/useSetting";
|
||||
|
||||
const maxTokenSize = 3;
|
||||
|
||||
function TokenLabel({ tokenState, width, height }) {
|
||||
const [labelSize] = useSetting("map.labelSize");
|
||||
|
||||
const fontSize =
|
||||
(height / 6 / tokenState.size) * Math.min(tokenState.size, maxTokenSize);
|
||||
(height / 6 / tokenState.size) *
|
||||
Math.min(tokenState.size, maxTokenSize) *
|
||||
labelSize;
|
||||
const paddingY =
|
||||
(height / 16 / tokenState.size) * Math.min(tokenState.size, maxTokenSize);
|
||||
(height / 16 / tokenState.size) *
|
||||
Math.min(tokenState.size, maxTokenSize) *
|
||||
labelSize;
|
||||
const paddingX =
|
||||
(height / 8 / tokenState.size) * Math.min(tokenState.size, maxTokenSize);
|
||||
(height / 8 / tokenState.size) *
|
||||
Math.min(tokenState.size, maxTokenSize) *
|
||||
labelSize;
|
||||
|
||||
const [rectWidth, setRectWidth] = useState(0);
|
||||
useEffect(() => {
|
||||
@ -19,7 +29,7 @@ function TokenLabel({ tokenState, width, height }) {
|
||||
} else {
|
||||
setRectWidth(0);
|
||||
}
|
||||
}, [tokenState.label, paddingX]);
|
||||
}, [tokenState.label, paddingX, width]);
|
||||
|
||||
const textRef = useRef();
|
||||
|
||||
|
@ -9,7 +9,7 @@ import {
|
||||
unknownSource,
|
||||
} from "../../tokens";
|
||||
|
||||
function TokenTile({ token, isSelected, onTokenSelect, onTokenRemove }) {
|
||||
function TokenTile({ token, isSelected, onTokenSelect, onTokenRemove, large }) {
|
||||
const tokenSource = useDataSource(token, defaultTokenSources, unknownSource);
|
||||
const isDefault = token.type === "default";
|
||||
|
||||
@ -17,22 +17,29 @@ function TokenTile({ token, isSelected, onTokenSelect, onTokenRemove }) {
|
||||
<Flex
|
||||
onClick={() => onTokenSelect(token)}
|
||||
sx={{
|
||||
borderColor: "primary",
|
||||
borderStyle: isSelected ? "solid" : "none",
|
||||
borderWidth: "4px",
|
||||
position: "relative",
|
||||
width: "150px",
|
||||
height: "150px",
|
||||
width: large ? "48%" : "32%",
|
||||
height: "0",
|
||||
paddingTop: large ? "48%" : "32%",
|
||||
borderRadius: "4px",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
cursor: "pointer",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
m={2}
|
||||
my={1}
|
||||
mx={`${large ? 1 : 2 / 3}%`}
|
||||
bg="muted"
|
||||
>
|
||||
<Image
|
||||
sx={{ width: "100%", height: "100%", objectFit: "contain" }}
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "contain",
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
}}
|
||||
src={tokenSource}
|
||||
/>
|
||||
<Flex
|
||||
@ -58,6 +65,20 @@ function TokenTile({ token, isSelected, onTokenSelect, onTokenRemove }) {
|
||||
{token.name}
|
||||
</Text>
|
||||
</Flex>
|
||||
<Box
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
borderColor: "primary",
|
||||
borderStyle: isSelected ? "solid" : "none",
|
||||
borderWidth: "4px",
|
||||
pointerEvents: "none",
|
||||
borderRadius: "4px",
|
||||
}}
|
||||
/>
|
||||
{isSelected && !isDefault && (
|
||||
<Box sx={{ position: "absolute", top: 0, right: 0 }}>
|
||||
<IconButton
|
||||
@ -68,7 +89,7 @@ function TokenTile({ token, isSelected, onTokenSelect, onTokenRemove }) {
|
||||
}}
|
||||
bg="overlay"
|
||||
sx={{ borderRadius: "50%" }}
|
||||
m={1}
|
||||
m={2}
|
||||
>
|
||||
<RemoveTokenIcon />
|
||||
</IconButton>
|
||||
|
@ -1,10 +1,14 @@
|
||||
import React from "react";
|
||||
import { Flex } from "theme-ui";
|
||||
import React, { useContext } from "react";
|
||||
import { Flex, Box, Text } from "theme-ui";
|
||||
import SimpleBar from "simplebar-react";
|
||||
import { useMedia } from "react-media";
|
||||
|
||||
import AddIcon from "../../icons/AddIcon";
|
||||
|
||||
import TokenTile from "./TokenTile";
|
||||
import Link from "../Link";
|
||||
|
||||
import DatabaseContext from "../../contexts/DatabaseContext";
|
||||
|
||||
function TokenTiles({
|
||||
tokens,
|
||||
@ -13,18 +17,21 @@ function TokenTiles({
|
||||
selectedToken,
|
||||
onTokenRemove,
|
||||
}) {
|
||||
const { databaseStatus } = useContext(DatabaseContext);
|
||||
const isSmallScreen = useMedia({ query: "(max-width: 500px)" });
|
||||
|
||||
return (
|
||||
<SimpleBar style={{ maxHeight: "300px", width: "500px" }}>
|
||||
<Box sx={{ position: "relative" }}>
|
||||
<SimpleBar style={{ maxHeight: "300px" }}>
|
||||
<Flex
|
||||
py={2}
|
||||
p={2}
|
||||
bg="muted"
|
||||
sx={{
|
||||
flexWrap: "wrap",
|
||||
width: "500px",
|
||||
borderRadius: "4px",
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
<Box
|
||||
onClick={onTokenAdd}
|
||||
sx={{
|
||||
":hover": {
|
||||
@ -36,20 +43,33 @@ function TokenTiles({
|
||||
":active": {
|
||||
color: "secondary",
|
||||
},
|
||||
width: "150px",
|
||||
height: "150px",
|
||||
width: isSmallScreen ? "48%" : "32%",
|
||||
height: "0",
|
||||
paddingTop: isSmallScreen ? "48%" : "32%",
|
||||
borderRadius: "4px",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
position: "relative",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
m={2}
|
||||
my={1}
|
||||
mx={`${isSmallScreen ? 1 : 2 / 3}%`}
|
||||
bg="muted"
|
||||
aria-label="Add Token"
|
||||
title="Add Token"
|
||||
>
|
||||
<Flex
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<AddIcon large />
|
||||
</Flex>
|
||||
</Box>
|
||||
{tokens.map((token) => (
|
||||
<TokenTile
|
||||
key={token.id}
|
||||
@ -57,10 +77,30 @@ function TokenTiles({
|
||||
isSelected={selectedToken && token.id === selectedToken.id}
|
||||
onTokenSelect={onTokenSelect}
|
||||
onTokenRemove={onTokenRemove}
|
||||
large={isSmallScreen}
|
||||
/>
|
||||
))}
|
||||
</Flex>
|
||||
</SimpleBar>
|
||||
{databaseStatus === "disabled" && (
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
textAlign: "center",
|
||||
}}
|
||||
bg="highlight"
|
||||
p={1}
|
||||
>
|
||||
<Text as="p" variant="body2">
|
||||
Token saving is unavailable. See <Link to="/faq#saving">FAQ</Link>{" "}
|
||||
for more information.
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -9,6 +9,7 @@ import ProxyToken from "./ProxyToken";
|
||||
import SelectTokensButton from "./SelectTokensButton";
|
||||
|
||||
import { fromEntries } from "../../helpers/shared";
|
||||
import useSetting from "../../helpers/useSetting";
|
||||
|
||||
import AuthContext from "../../contexts/AuthContext";
|
||||
import TokenDataContext from "../../contexts/TokenDataContext";
|
||||
@ -17,7 +18,8 @@ const listTokenClassName = "list-token";
|
||||
|
||||
function Tokens({ onMapTokenStateCreate }) {
|
||||
const { userId } = useContext(AuthContext);
|
||||
const { ownedTokens, tokens } = useContext(TokenDataContext);
|
||||
const { ownedTokens, tokens, updateToken } = useContext(TokenDataContext);
|
||||
const [fullScreen] = useSetting("map.fullScreen");
|
||||
|
||||
function handleProxyDragEnd(isOnMap, token) {
|
||||
if (isOnMap && onMapTokenStateCreate) {
|
||||
@ -37,6 +39,8 @@ function Tokens({ onMapTokenStateCreate }) {
|
||||
locked: false,
|
||||
visible: true,
|
||||
});
|
||||
// Update last used for cache invalidation
|
||||
updateToken(token.id, { lastUsed: Date.now() });
|
||||
}
|
||||
}
|
||||
|
||||
@ -48,6 +52,7 @@ function Tokens({ onMapTokenStateCreate }) {
|
||||
width: "80px",
|
||||
minWidth: "80px",
|
||||
overflow: "hidden",
|
||||
display: fullScreen ? "none" : "block",
|
||||
}}
|
||||
>
|
||||
<SimpleBar style={{ height: "calc(100% - 48px)", overflowX: "hidden" }}>
|
||||
|
@ -4,18 +4,27 @@ import shortid from "shortid";
|
||||
import DatabaseContext from "./DatabaseContext";
|
||||
|
||||
import { getRandomMonster } from "../helpers/monsters";
|
||||
import FakeStorage from "../helpers/FakeStorage";
|
||||
|
||||
const AuthContext = React.createContext();
|
||||
|
||||
let storage;
|
||||
try {
|
||||
sessionStorage.setItem("__test", "__test");
|
||||
sessionStorage.removeItem("__test");
|
||||
storage = sessionStorage;
|
||||
} catch (e) {
|
||||
console.warn("Session storage is disabled, no authentication will be saved");
|
||||
storage = new FakeStorage();
|
||||
}
|
||||
|
||||
export function AuthProvider({ children }) {
|
||||
const { database } = useContext(DatabaseContext);
|
||||
|
||||
const [password, setPassword] = useState(
|
||||
sessionStorage.getItem("auth") || ""
|
||||
);
|
||||
const [password, setPassword] = useState(storage.getItem("auth") || "");
|
||||
|
||||
useEffect(() => {
|
||||
sessionStorage.setItem("auth", password);
|
||||
storage.setItem("auth", password);
|
||||
}, [password]);
|
||||
|
||||
const [authenticationStatus, setAuthenticationStatus] = useState("unknown");
|
||||
|
@ -7,6 +7,9 @@ import { maps as defaultMaps } from "../maps";
|
||||
|
||||
const MapDataContext = React.createContext();
|
||||
|
||||
// Maximum number of maps to keep in the cache
|
||||
const cachedMapMax = 15;
|
||||
|
||||
const defaultMapState = {
|
||||
tokens: {},
|
||||
// An index into the draw actions array to which only actions before the
|
||||
@ -70,12 +73,19 @@ export function MapDataProvider({ children }) {
|
||||
loadMaps();
|
||||
}, [userId, database]);
|
||||
|
||||
/**
|
||||
* Adds a map to the database, also adds an assosiated state for that map
|
||||
* @param {Object} map map to add
|
||||
*/
|
||||
async function addMap(map) {
|
||||
await database.table("maps").add(map);
|
||||
const state = { ...defaultMapState, mapId: map.id };
|
||||
await database.table("states").add(state);
|
||||
setMaps((prevMaps) => [map, ...prevMaps]);
|
||||
setMapStates((prevStates) => [state, ...prevStates]);
|
||||
if (map.owner !== userId) {
|
||||
await updateCache();
|
||||
}
|
||||
}
|
||||
|
||||
async function removeMap(id) {
|
||||
@ -129,6 +139,11 @@ export function MapDataProvider({ children }) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a map to the database if none exists or replaces a map if it already exists
|
||||
* Note: this does not add a map state to do that use AddMap
|
||||
* @param {Object} map the map to put
|
||||
*/
|
||||
async function putMap(map) {
|
||||
await database.table("maps").put(map);
|
||||
setMaps((prevMaps) => {
|
||||
@ -141,6 +156,31 @@ export function MapDataProvider({ children }) {
|
||||
}
|
||||
return newMaps;
|
||||
});
|
||||
if (map.owner !== userId) {
|
||||
await updateCache();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep up to cachedMapMax amount of maps that you don't own
|
||||
* Sorted by when they we're last used
|
||||
*/
|
||||
async function updateCache() {
|
||||
const cachedMaps = await database
|
||||
.table("maps")
|
||||
.where("owner")
|
||||
.notEqual(userId)
|
||||
.sortBy("lastUsed");
|
||||
if (cachedMaps.length > cachedMapMax) {
|
||||
const cacheDeleteCount = cachedMaps.length - cachedMapMax;
|
||||
const idsToDelete = cachedMaps
|
||||
.slice(0, cacheDeleteCount)
|
||||
.map((map) => map.id);
|
||||
database.table("maps").where("id").anyOf(idsToDelete).delete();
|
||||
setMaps((prevMaps) => {
|
||||
return prevMaps.filter((map) => !idsToDelete.includes(map.id));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getMap(mapId) {
|
||||
|
@ -7,6 +7,8 @@ import { tokens as defaultTokens } from "../tokens";
|
||||
|
||||
const TokenDataContext = React.createContext();
|
||||
|
||||
const cachedTokenMax = 100;
|
||||
|
||||
export function TokenDataProvider({ children }) {
|
||||
const { database } = useContext(DatabaseContext);
|
||||
const { userId } = useContext(AuthContext);
|
||||
@ -45,6 +47,9 @@ export function TokenDataProvider({ children }) {
|
||||
async function addToken(token) {
|
||||
await database.table("tokens").add(token);
|
||||
setTokens((prevTokens) => [token, ...prevTokens]);
|
||||
if (token.owner !== userId) {
|
||||
await updateCache();
|
||||
}
|
||||
}
|
||||
|
||||
async function removeToken(id) {
|
||||
@ -80,6 +85,31 @@ export function TokenDataProvider({ children }) {
|
||||
}
|
||||
return newTokens;
|
||||
});
|
||||
if (token.owner !== userId) {
|
||||
await updateCache();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep up to cachedTokenMax amount of tokens that you don't own
|
||||
* Sorted by when they we're last used
|
||||
*/
|
||||
async function updateCache() {
|
||||
const cachedTokens = await database
|
||||
.table("tokens")
|
||||
.where("owner")
|
||||
.notEqual(userId)
|
||||
.sortBy("lastUsed");
|
||||
if (cachedTokens.length > cachedTokenMax) {
|
||||
const cacheDeleteCount = cachedTokens.length - cachedTokenMax;
|
||||
const idsToDelete = cachedTokens
|
||||
.slice(0, cacheDeleteCount)
|
||||
.map((token) => token.id);
|
||||
database.table("tokens").where("id").anyOf(idsToDelete).delete();
|
||||
setTokens((prevTokens) => {
|
||||
return prevTokens.filter((token) => !idsToDelete.includes(token.id));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getToken(tokenId) {
|
||||
|
@ -184,6 +184,28 @@ function loadVersions(db) {
|
||||
delete token.isVehicle;
|
||||
});
|
||||
});
|
||||
// v1.5.2 - Added automatic cache invalidation to maps
|
||||
db.version(11)
|
||||
.stores({})
|
||||
.upgrade(async (tx) => {
|
||||
return tx
|
||||
.table("maps")
|
||||
.toCollection()
|
||||
.modify((map) => {
|
||||
map.lastUsed = map.lastModified;
|
||||
});
|
||||
});
|
||||
// v1.5.2 - Added automatic cache invalidation to tokens
|
||||
db.version(12)
|
||||
.stores({})
|
||||
.upgrade(async (tx) => {
|
||||
return tx
|
||||
.table("tokens")
|
||||
.toCollection()
|
||||
.modify((token) => {
|
||||
token.lastUsed = token.lastModified;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Get the dexie database used in DatabaseContext
|
||||
|
20
src/docs/releaseNotes/v1.5.2.md
Normal file
20
src/docs/releaseNotes/v1.5.2.md
Normal file
@ -0,0 +1,20 @@
|
||||
## Minor Changes
|
||||
|
||||
This release focuses on better support for small screens and adds more accessibility options.
|
||||
|
||||
- Map, token and dice select screens now scale properly to small screens.
|
||||
- A new full screen button allows you to hide the party and token list. This helps with usability on small screens and fixes an issue where the dice tray couldn't be seen.
|
||||
- Fixed map zoom speed on Firefox on Windows to align with the other browsers.
|
||||
- Fixed a crash that would occur if the user had local storage disabled.
|
||||
- Fixed a bug that would cause the token label width to not update when the token size changed.
|
||||
- Added a new accessibility option in the game settings for Token Label Size. This is a scale applied to token labels locally and should help with those who use large maps or high resolution screens.
|
||||
- Fixed a bug that caused a crash when a map was deleted with unsaved changes.
|
||||
- Fixed an error that could happen when switching between two maps with custom tokens.
|
||||
- Added automatic cache invalidation to custom maps and tokens. This is an attempt to keep the storage of maps from getting out of hand as people use the site more. Practically this means that there is now a limit of 100 unique custom tokens per map. I'm intending to keep an eye on this and if there are issues I can make this number bigger.
|
||||
|
||||
[Reddit]()
|
||||
[Twitter]()
|
||||
|
||||
---
|
||||
|
||||
September 10 2020
|
23
src/helpers/FakeStorage.js
Normal file
23
src/helpers/FakeStorage.js
Normal file
@ -0,0 +1,23 @@
|
||||
/**
|
||||
* A faked local or session storage used when the user has disabled storage
|
||||
*/
|
||||
class FakeStorage {
|
||||
data = {};
|
||||
key(index) {
|
||||
return Object.keys(this.data)[index] || null;
|
||||
}
|
||||
getItem(keyName) {
|
||||
return this.data[keyName] || null;
|
||||
}
|
||||
setItem(keyName, keyValue) {
|
||||
this.data[keyName] = keyValue;
|
||||
}
|
||||
removeItem(keyName) {
|
||||
delete this.data[keyName];
|
||||
}
|
||||
clear() {
|
||||
this.data = {};
|
||||
}
|
||||
}
|
||||
|
||||
export default FakeStorage;
|
@ -1,12 +1,24 @@
|
||||
import FakeStorage from "./FakeStorage";
|
||||
|
||||
/**
|
||||
* An interface to a local storage back settings store with a versioning mechanism
|
||||
*/
|
||||
class Settings {
|
||||
name;
|
||||
currentVersion;
|
||||
storage;
|
||||
|
||||
constructor(name) {
|
||||
this.name = name;
|
||||
// Try and use local storage if it is available, if not mock it with an in memory storage
|
||||
try {
|
||||
localStorage.setItem("__test", "__test");
|
||||
localStorage.removeItem("__test");
|
||||
this.storage = localStorage;
|
||||
} catch (e) {
|
||||
console.warn("Local storage is disabled, no settings will be saved");
|
||||
this.storage = new FakeStorage();
|
||||
}
|
||||
this.currentVersion = this.get("__version");
|
||||
}
|
||||
|
||||
@ -18,7 +30,7 @@ class Settings {
|
||||
}
|
||||
|
||||
getAll() {
|
||||
return JSON.parse(localStorage.getItem(this.name));
|
||||
return JSON.parse(this.storage.getItem(this.name));
|
||||
}
|
||||
|
||||
get(key) {
|
||||
@ -27,7 +39,7 @@ class Settings {
|
||||
}
|
||||
|
||||
setAll(newSettings) {
|
||||
localStorage.setItem(
|
||||
this.storage.setItem(
|
||||
this.name,
|
||||
JSON.stringify({ ...newSettings, __version: this.currentVersion })
|
||||
);
|
||||
|
18
src/icons/FullScreenExitIcon.js
Normal file
18
src/icons/FullScreenExitIcon.js
Normal file
@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
|
||||
function FullScreenIcon() {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
fill="currentcolor"
|
||||
>
|
||||
<path d="M0 0h24v24H0V0z" fill="none" />
|
||||
<path d="M6 16h2v2c0 .55.45 1 1 1s1-.45 1-1v-3c0-.55-.45-1-1-1H6c-.55 0-1 .45-1 1s.45 1 1 1zm2-8H6c-.55 0-1 .45-1 1s.45 1 1 1h3c.55 0 1-.45 1-1V6c0-.55-.45-1-1-1s-1 .45-1 1v2zm7 11c.55 0 1-.45 1-1v-2h2c.55 0 1-.45 1-1s-.45-1-1-1h-3c-.55 0-1 .45-1 1v3c0 .55.45 1 1 1zm1-11V6c0-.55-.45-1-1-1s-1 .45-1 1v3c0 .55.45 1 1 1h3c.55 0 1-.45 1-1s-.45-1-1-1h-2z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default FullScreenIcon;
|
18
src/icons/FullScreenIcon.js
Normal file
18
src/icons/FullScreenIcon.js
Normal file
@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
|
||||
function FullScreenIcon() {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
fill="currentcolor"
|
||||
>
|
||||
<path d="M0 0h24v24H0V0z" fill="none" />
|
||||
<path d="M6 14c-.55 0-1 .45-1 1v3c0 .55.45 1 1 1h3c.55 0 1-.45 1-1s-.45-1-1-1H7v-2c0-.55-.45-1-1-1zm0-4c.55 0 1-.45 1-1V7h2c.55 0 1-.45 1-1s-.45-1-1-1H6c-.55 0-1 .45-1 1v3c0 .55.45 1 1 1zm11 7h-2c-.55 0-1 .45-1 1s.45 1 1 1h3c.55 0 1-.45 1-1v-3c0-.55-.45-1-1-1s-1 .45-1 1v2zM14 6c0 .55.45 1 1 1h2v2c0 .55.45 1 1 1s1-.45 1-1V6c0-.55-.45-1-1-1h-3c-.55 0-1 .45-1 1z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default FullScreenIcon;
|
@ -9,7 +9,11 @@ import { dice } from "../dice";
|
||||
function SelectDiceModal({ isOpen, onRequestClose, onDone, defaultDice }) {
|
||||
const [selectedDice, setSelectedDice] = useState(defaultDice);
|
||||
return (
|
||||
<Modal isOpen={isOpen} onRequestClose={onRequestClose}>
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onRequestClose={onRequestClose}
|
||||
style={{ maxWidth: "542px", width: "calc(100% - 16px)" }}
|
||||
>
|
||||
<Flex
|
||||
sx={{
|
||||
flexDirection: "column",
|
||||
|
@ -149,6 +149,7 @@ function SelectMapModal({
|
||||
id: shortid.generate(),
|
||||
created: Date.now(),
|
||||
lastModified: Date.now(),
|
||||
lastUsed: Date.now(),
|
||||
owner: userId,
|
||||
...defaultMapProps,
|
||||
});
|
||||
@ -174,9 +175,9 @@ function SelectMapModal({
|
||||
|
||||
async function handleMapRemove(id) {
|
||||
await removeMap(id);
|
||||
setSelectedMapId(null);
|
||||
setMapSettingChanges({});
|
||||
setMapStateSettingChanges({});
|
||||
setSelectedMapId(null);
|
||||
// Removed the map from the map screen if needed
|
||||
if (currentMap && currentMap.id === selectedMapId) {
|
||||
onMapChange(null, null);
|
||||
@ -213,7 +214,13 @@ function SelectMapModal({
|
||||
}
|
||||
if (selectedMapId) {
|
||||
await applyMapChanges();
|
||||
onMapChange(selectedMapWithChanges, selectedMapStateWithChanges);
|
||||
// Update last used for cache invalidation
|
||||
const lastUsed = Date.now();
|
||||
await updateMap(selectedMapId, { lastUsed });
|
||||
onMapChange(
|
||||
{ ...selectedMapWithChanges, lastUsed },
|
||||
selectedMapStateWithChanges
|
||||
);
|
||||
} else {
|
||||
onMapChange(null, null);
|
||||
}
|
||||
@ -265,14 +272,21 @@ function SelectMapModal({
|
||||
}
|
||||
}
|
||||
|
||||
const selectedMapWithChanges = { ...selectedMap, ...mapSettingChanges };
|
||||
const selectedMapStateWithChanges = {
|
||||
const selectedMapWithChanges = selectedMap && {
|
||||
...selectedMap,
|
||||
...mapSettingChanges,
|
||||
};
|
||||
const selectedMapStateWithChanges = selectedMapState && {
|
||||
...selectedMapState,
|
||||
...mapStateSettingChanges,
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onRequestClose={handleClose}>
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onRequestClose={handleClose}
|
||||
style={{ maxWidth: "542px", width: "calc(100% - 16px)" }}
|
||||
>
|
||||
<ImageDrop onDrop={handleImagesUpload} dropText="Drop map to upload">
|
||||
<input
|
||||
onChange={(event) => handleImagesUpload(event.target.files)}
|
||||
|
@ -75,6 +75,7 @@ function SelectTokensModal({ isOpen, onRequestClose }) {
|
||||
type: "file",
|
||||
created: Date.now(),
|
||||
lastModified: Date.now(),
|
||||
lastUsed: Date.now(),
|
||||
owner: userId,
|
||||
defaultSize: 1,
|
||||
category: "character",
|
||||
@ -131,7 +132,11 @@ function SelectTokensModal({ isOpen, onRequestClose }) {
|
||||
const selectedTokenWithChanges = { ...selectedToken, ...tokenSettingChanges };
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onRequestClose={handleRequestClose}>
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onRequestClose={handleRequestClose}
|
||||
style={{ maxWidth: "542px", width: "calc(100% - 16px)" }}
|
||||
>
|
||||
<ImageDrop onDrop={handleImagesUpload} dropText="Drop token to upload">
|
||||
<input
|
||||
onChange={(event) => handleImagesUpload(event.target.files)}
|
||||
|
@ -1,15 +1,27 @@
|
||||
import React, { useState, useContext } from "react";
|
||||
import { Box, Label, Flex, Button, useColorMode, Checkbox } from "theme-ui";
|
||||
import {
|
||||
Box,
|
||||
Label,
|
||||
Flex,
|
||||
Button,
|
||||
useColorMode,
|
||||
Checkbox,
|
||||
Slider,
|
||||
Divider,
|
||||
} from "theme-ui";
|
||||
|
||||
import Modal from "../components/Modal";
|
||||
|
||||
import AuthContext from "../contexts/AuthContext";
|
||||
import DatabaseContext from "../contexts/DatabaseContext";
|
||||
|
||||
import useSetting from "../helpers/useSetting";
|
||||
|
||||
function SettingsModal({ isOpen, onRequestClose }) {
|
||||
const { database } = useContext(DatabaseContext);
|
||||
const { userId } = useContext(AuthContext);
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [labelSize, setLabelSize] = useSetting("map.labelSize");
|
||||
|
||||
async function handleEraseAllData() {
|
||||
localStorage.clear();
|
||||
@ -52,16 +64,30 @@ function SettingsModal({ isOpen, onRequestClose }) {
|
||||
<Modal isOpen={isOpen} onRequestClose={onRequestClose}>
|
||||
<Flex sx={{ flexDirection: "column" }}>
|
||||
<Label py={2}>Settings</Label>
|
||||
<Divider bg="text" />
|
||||
<Label py={2}>Accesibility:</Label>
|
||||
<Label py={2}>
|
||||
Light theme
|
||||
<span style={{ marginRight: "4px" }}>Light theme</span>
|
||||
<Checkbox
|
||||
checked={colorMode === "light"}
|
||||
onChange={(e) =>
|
||||
setColorMode(e.target.checked ? "light" : "default")
|
||||
}
|
||||
pl={1}
|
||||
/>
|
||||
</Label>
|
||||
<Label py={2}>
|
||||
Token Label Size
|
||||
<Slider
|
||||
step={0.5}
|
||||
min={1}
|
||||
max={3}
|
||||
ml={1}
|
||||
sx={{ width: "initial" }}
|
||||
value={labelSize}
|
||||
onChange={(e) => setLabelSize(parseFloat(e.target.value))}
|
||||
/>
|
||||
</Label>
|
||||
<Divider bg="text" />
|
||||
<Flex py={2}>
|
||||
<Button sx={{ flexGrow: 1 }} onClick={handleClearCache}>
|
||||
Clear cache
|
||||
|
@ -32,7 +32,7 @@ function NetworkedMapAndTokens({ session }) {
|
||||
isLoading,
|
||||
} = useContext(MapLoadingContext);
|
||||
|
||||
const { putToken, getToken } = useContext(TokenDataContext);
|
||||
const { putToken, getToken, updateToken } = useContext(TokenDataContext);
|
||||
const { putMap, updateMap, getMapFromDB } = useContext(MapDataContext);
|
||||
|
||||
const [currentMap, setCurrentMap] = useState(null);
|
||||
@ -245,11 +245,15 @@ function NetworkedMapAndTokens({ session }) {
|
||||
if (newMap && newMap.type === "file") {
|
||||
const cachedMap = await getMapFromDB(newMap.id);
|
||||
if (cachedMap && cachedMap.lastModified >= newMap.lastModified) {
|
||||
setCurrentMap(cachedMap);
|
||||
// Update last used for cache invalidation
|
||||
const lastUsed = Date.now();
|
||||
await updateMap(cachedMap.id, { lastUsed });
|
||||
setCurrentMap({ ...cachedMap, lastUsed });
|
||||
} else {
|
||||
// Save map data but remove last modified so if there is an error
|
||||
// during the map request the cache is invalid
|
||||
await putMap({ ...newMap, lastModified: 0 });
|
||||
// during the map request the cache is invalid. Also add last used
|
||||
// for cache invalidation
|
||||
await putMap({ ...newMap, lastModified: 0, lastUsed: Date.now() });
|
||||
reply("mapRequest", newMap.id, "map");
|
||||
}
|
||||
} else {
|
||||
@ -326,16 +330,21 @@ function NetworkedMapAndTokens({ session }) {
|
||||
if (newToken && newToken.type === "file") {
|
||||
const cachedToken = getToken(newToken.id);
|
||||
if (
|
||||
!cachedToken ||
|
||||
cachedToken.lastModified !== newToken.lastModified
|
||||
cachedToken &&
|
||||
cachedToken.lastModified >= newToken.lastModified
|
||||
) {
|
||||
// Update last used for cache invalidation
|
||||
const lastUsed = Date.now();
|
||||
await updateToken(cachedToken.id, { lastUsed });
|
||||
} else {
|
||||
reply("tokenRequest", newToken.id, "token");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (id === "tokenRequest") {
|
||||
const token = getToken(data);
|
||||
reply("tokenResponse", token, "token");
|
||||
// Add a last used property for cache invalidation
|
||||
reply("tokenResponse", { ...token, lastUsed: Date.now() }, "token");
|
||||
}
|
||||
if (id === "tokenResponse") {
|
||||
const newToken = data;
|
||||
|
@ -19,6 +19,7 @@ const v141 = raw("../docs/releaseNotes/v1.4.1.md");
|
||||
const v142 = raw("../docs/releaseNotes/v1.4.2.md");
|
||||
const v150 = raw("../docs/releaseNotes/v1.5.0.md");
|
||||
const v151 = raw("../docs/releaseNotes/v1.5.1.md");
|
||||
const v152 = raw("../docs/releaseNotes/v1.5.2.md");
|
||||
|
||||
function ReleaseNotes() {
|
||||
const location = useLocation();
|
||||
@ -43,6 +44,11 @@ function ReleaseNotes() {
|
||||
<Text mb={2} variant="heading" as="h1" sx={{ fontSize: 5 }}>
|
||||
Release Notes
|
||||
</Text>
|
||||
<div id="v152">
|
||||
<Accordion heading="v1.5.2" defaultOpen>
|
||||
<Markdown source={v152} />
|
||||
</Accordion>
|
||||
</div>
|
||||
<div id="v151">
|
||||
<Accordion heading="v1.5.1" defaultOpen>
|
||||
<Markdown source={v151} />
|
||||
|
@ -27,6 +27,11 @@ function loadVersions(settings) {
|
||||
style: "galaxy",
|
||||
},
|
||||
}));
|
||||
// v1.5.2 - Added full screen support for map and label size
|
||||
settings.version(2, (prev) => ({
|
||||
...prev,
|
||||
map: { fullScreen: false, labelSize: 1 },
|
||||
}));
|
||||
}
|
||||
|
||||
export function getSettings() {
|
||||
|
@ -7896,6 +7896,11 @@ normalize-url@^3.0.0:
|
||||
resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-3.3.0.tgz#b2e1c4dc4f7c6d57743df733a4f5978d18650559"
|
||||
integrity sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg==
|
||||
|
||||
normalize-wheel@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/normalize-wheel/-/normalize-wheel-1.0.1.tgz#aec886affdb045070d856447df62ecf86146ec45"
|
||||
integrity sha1-rsiGr/2wRQcNhWRH32Ls+GFG7EU=
|
||||
|
||||
npm-run-path@^2.0.0:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
|
||||
|
Loading…
Reference in New Issue
Block a user