Merge branch 'master' into typescript

This commit is contained in:
Mitchell McCaffrey
2021-07-02 15:54:54 +10:00
157 changed files with 8114 additions and 4055 deletions

View File

@@ -0,0 +1,89 @@
import React, { useEffect, useRef, useState } from "react";
import { Box, IconButton } from "theme-ui";
import RemoveTokenIcon from "../../icons/RemoveTokenIcon";
function DragOverlay({ dragging, node, onRemove }) {
const [isRemoveHovered, setIsRemoveHovered] = useState(false);
const removeTokenRef = useRef();
// Detect token hover on remove icon manually to support touch devices
useEffect(() => {
const map = document.querySelector(".map");
const mapRect = map.getBoundingClientRect();
function detectRemoveHover() {
if (!node || !dragging || !removeTokenRef.current) {
return;
}
const stage = node.getStage();
if (!stage) {
return;
}
const pointerPosition = stage.getPointerPosition();
const screenSpacePointerPosition = {
x: pointerPosition.x + mapRect.left,
y: pointerPosition.y + mapRect.top,
};
const removeIconPosition = removeTokenRef.current.getBoundingClientRect();
if (
screenSpacePointerPosition.x > removeIconPosition.left &&
screenSpacePointerPosition.y > removeIconPosition.top &&
screenSpacePointerPosition.x < removeIconPosition.right &&
screenSpacePointerPosition.y < removeIconPosition.bottom
) {
if (!isRemoveHovered) {
setIsRemoveHovered(true);
}
} else if (isRemoveHovered) {
setIsRemoveHovered(false);
}
}
let handler;
if (node && dragging) {
handler = setInterval(detectRemoveHover, 100);
}
return () => {
if (handler) {
clearInterval(handler);
}
};
}, [isRemoveHovered, dragging, node]);
// Detect drag end of token image and remove it if it is over the remove icon
useEffect(() => {
if (!dragging && node && isRemoveHovered) {
onRemove();
}
});
return (
dragging && (
<Box
sx={{
position: "absolute",
bottom: "32px",
left: "50%",
borderRadius: "50%",
transform: isRemoveHovered
? "translateX(-50%) scale(2.0)"
: "translateX(-50%) scale(1.5)",
transition: "transform 250ms ease",
color: isRemoveHovered ? "primary" : "text",
pointerEvents: "none",
}}
bg="overlay"
ref={removeTokenRef}
>
<IconButton>
<RemoveTokenIcon />
</IconButton>
</Box>
)
);
}
export default DragOverlay;

View File

@@ -1,4 +1,6 @@
import React, { useState } from "react";
import { Box } from "theme-ui";
import { useToasts } from "react-toast-notifications";
import MapControls from "./MapControls";
import MapInteraction from "./MapInteraction";
@@ -141,6 +143,8 @@ function Map({
disabledTokens: any,
session: Session
}) {
const { addToast } = useToasts();
const { tokensById } = useTokenData();
const [selectedToolId, setSelectedToolId] = useState("move");
@@ -324,6 +328,7 @@ function Map({
onShapesCut={handleFogShapesCut}
onShapesRemove={handleFogShapesRemove}
onShapesEdit={handleFogShapesEdit}
onShapeError={addToast}
active={selectedToolId === "fog"}
toolSettings={settings.fog}
editable={allowFogDrawing && !settings.fog.preview}
@@ -427,30 +432,32 @@ function Map({
);
return (
<MapInteraction
map={map}
mapState={mapState}
controls={
<>
{mapControls}
{tokenMenu}
{noteMenu}
{tokenDragOverlay}
{noteDragOverlay}
</>
}
selectedToolId={selectedToolId}
onSelectedToolChange={setSelectedToolId}
disabledControls={disabledControls}
>
{mapGrid}
{mapDrawing}
{mapNotes}
{mapTokens}
{mapFog}
{mapPointer}
{mapMeasure}
</MapInteraction>
<Box sx={{ flexGrow: 1 }}>
<MapInteraction
map={map}
mapState={mapState}
controls={
<>
{mapControls}
{tokenMenu}
{noteMenu}
{tokenDragOverlay}
{noteDragOverlay}
</>
}
selectedToolId={selectedToolId}
onSelectedToolChange={setSelectedToolId}
disabledControls={disabledControls}
>
{mapGrid}
{mapDrawing}
{mapNotes}
{mapTokens}
{mapFog}
{mapPointer}
{mapMeasure}
</MapInteraction>
</Box>
);
}

View File

@@ -119,8 +119,7 @@ function MapDrawing({
}
const simplified = simplifyPoints(
[...prevPoints, brushPosition],
gridCellNormalizedSize,
stageScale
1 / 1000 / stageScale
);
return {
...prevShape,

View File

@@ -0,0 +1,159 @@
import React, { useState, useEffect } from "react";
import { Flex, Close, IconButton } from "theme-ui";
import { groupsFromIds, itemsFromGroups } from "../../helpers/group";
import ConfirmModal from "../../modals/ConfirmModal";
import ResetMapIcon from "../../icons/ResetMapIcon";
import RemoveMapIcon from "../../icons/RemoveMapIcon";
import { useGroup } from "../../contexts/GroupContext";
import { useMapData } from "../../contexts/MapDataContext";
import { useKeyboard } from "../../contexts/KeyboardContext";
import shortcuts from "../../shortcuts";
function MapEditBar({ currentMap, disabled, onMapChange, onMapReset, onLoad }) {
const [hasMapState, setHasMapState] = useState(false);
const { maps, mapStates, removeMaps, resetMap } = useMapData();
const { activeGroups, selectedGroupIds, onGroupSelect } = useGroup();
useEffect(() => {
const selectedGroups = groupsFromIds(selectedGroupIds, activeGroups);
const selectedMapStates = itemsFromGroups(
selectedGroups,
mapStates,
"mapId"
);
let _hasMapState = false;
for (let state of selectedMapStates) {
if (
Object.values(state.tokens).length > 0 ||
Object.values(state.drawShapes).length > 0 ||
Object.values(state.fogShapes).length > 0 ||
Object.values(state.notes).length > 0
) {
_hasMapState = true;
break;
}
}
setHasMapState(_hasMapState);
}, [selectedGroupIds, mapStates, activeGroups]);
function getSelectedMaps() {
const selectedGroups = groupsFromIds(selectedGroupIds, activeGroups);
return itemsFromGroups(selectedGroups, maps);
}
const [isMapsRemoveModalOpen, setIsMapsRemoveModalOpen] = useState(false);
async function handleMapsRemove() {
onLoad(true);
setIsMapsRemoveModalOpen(false);
const selectedMaps = getSelectedMaps();
const selectedMapIds = selectedMaps.map((map) => map.id);
onGroupSelect();
await removeMaps(selectedMapIds);
// Removed the map from the map screen if needed
if (currentMap && selectedMapIds.includes(currentMap.id)) {
onMapChange(null, null);
}
onLoad(false);
}
const [isMapsResetModalOpen, setIsMapsResetModalOpen] = useState(false);
async function handleMapsReset() {
onLoad(true);
setIsMapsResetModalOpen(false);
const selectedMaps = getSelectedMaps();
const selectedMapIds = selectedMaps.map((map) => map.id);
for (let id of selectedMapIds) {
const newState = await resetMap(id);
// Reset the state of the current map if needed
if (currentMap && currentMap.id === id) {
onMapReset(newState);
}
}
onLoad(false);
}
/**
* Shortcuts
*/
function handleKeyDown(event) {
if (disabled) {
return;
}
if (shortcuts.delete(event)) {
const selectedMaps = getSelectedMaps();
if (selectedMaps.length > 0) {
setIsMapsResetModalOpen(false);
setIsMapsRemoveModalOpen(true);
}
}
}
useKeyboard(handleKeyDown);
if (selectedGroupIds.length === 0) {
return null;
}
return (
<Flex
sx={{
position: "absolute",
bottom: 0,
left: 0,
right: 0,
justifyContent: "space-between",
}}
bg="overlay"
>
<Close
title="Clear Selection"
aria-label="Clear Selection"
onClick={() => onGroupSelect()}
/>
<Flex>
<IconButton
aria-label="Reset Selected Map(s)"
title="Reset Selected Map(s)"
onClick={() => setIsMapsResetModalOpen(true)}
disabled={!hasMapState}
>
<ResetMapIcon />
</IconButton>
<IconButton
aria-label="Remove Selected Map(s)"
title="Remove Selected Map(s)"
onClick={() => setIsMapsRemoveModalOpen(true)}
>
<RemoveMapIcon />
</IconButton>
</Flex>
<ConfirmModal
isOpen={isMapsResetModalOpen}
onRequestClose={() => setIsMapsResetModalOpen(false)}
onConfirm={handleMapsReset}
confirmText="Reset"
label="Reset Selected Map(s)"
description="This will remove all fog, drawings and tokens from the selected maps."
/>
<ConfirmModal
isOpen={isMapsRemoveModalOpen}
onRequestClose={() => setIsMapsRemoveModalOpen(false)}
onConfirm={handleMapsRemove}
confirmText="Remove"
label="Remove Selected Map(s)"
description="This operation cannot be undone."
/>
</Flex>
);
}
export default MapEditBar;

View File

@@ -23,7 +23,7 @@ import MapGrid from "./MapGrid";
import MapGridEditor from "./MapGridEditor";
function MapEditor({ map, onSettingsChange }) {
const [mapImageSource] = useMapImage(map);
const [mapImage] = useMapImage(map);
const [stageWidth, setStageWidth] = useState(1);
const [stageHeight, setStageHeight] = useState(1);
@@ -93,14 +93,14 @@ function MapEditor({ map, onSettingsChange }) {
interactionEmitter: null,
};
const canEditGrid = map.type !== "default";
const gridChanged =
map.grid.inset.topLeft.x !== defaultInset.topLeft.x ||
map.grid.inset.topLeft.y !== defaultInset.topLeft.y ||
map.grid.inset.bottomRight.x !== defaultInset.bottomRight.x ||
map.grid.inset.bottomRight.y !== defaultInset.bottomRight.y;
const gridValid = map.grid.size.x !== 0 && map.grid.size.y !== 0;
const layout = useResponsiveLayout();
return (
@@ -132,12 +132,8 @@ function MapEditor({ map, onSettingsChange }) {
)}
>
<Layer ref={mapLayerRef}>
<Image
image={mapImageSource}
width={mapWidth}
height={mapHeight}
/>
{showGridControls && canEditGrid && (
<Image image={mapImage} width={mapWidth} height={mapHeight} />
{showGridControls && gridValid && (
<>
<MapGrid map={map} />
<MapGridEditor map={map} onGridChange={handleGridChange} />
@@ -146,7 +142,7 @@ function MapEditor({ map, onSettingsChange }) {
</Layer>
</KonvaBridge>
</ReactResizeDetector>
{gridChanged && (
{gridChanged && gridValid && (
<IconButton
title="Reset Grid"
aria-label="Reset Grid"
@@ -163,28 +159,26 @@ function MapEditor({ map, onSettingsChange }) {
<ResetMapIcon />
</IconButton>
)}
{canEditGrid && (
<IconButton
title={
showGridControls ? "Hide Grid Controls" : "Show Grid Controls"
}
aria-label={
showGridControls ? "Hide Grid Controls" : "Show Grid Controls"
}
onClick={() => setShowGridControls(!showGridControls)}
bg="overlay"
sx={{
borderRadius: "50%",
position: "absolute",
bottom: 0,
right: 0,
}}
m={2}
p="6px"
>
{showGridControls ? <GridOnIcon /> : <GridOffIcon />}
</IconButton>
)}
<IconButton
title={
showGridControls ? "Hide Grid Controls" : "Show Grid Controls"
}
aria-label={
showGridControls ? "Hide Grid Controls" : "Show Grid Controls"
}
onClick={() => setShowGridControls(!showGridControls)}
bg="overlay"
sx={{
borderRadius: "50%",
position: "absolute",
bottom: 0,
right: 0,
}}
m={2}
p="6px"
>
{showGridControls ? <GridOnIcon /> : <GridOffIcon />}
</IconButton>
</Box>
</GridProvider>
</MapInteractionProvider>

View File

@@ -1,7 +1,8 @@
import React, { useState, useEffect, useCallback, useRef } from "react";
import React, { useState, useEffect, useCallback } from "react";
import shortid from "shortid";
import { Group, Rect, Line } from "react-konva";
import { Group, Line } from "react-konva";
import useImage from "use-image";
import Color from "color";
import diagonalPattern from "../../images/DiagonalPattern.png";
@@ -37,8 +38,10 @@ import {
Tick,
getRelativePointerPosition,
} from "../../helpers/konva";
import { keyBy } from "../../helpers/shared";
import SubtractShapeAction from "../../actions/SubtractShapeAction";
import CutShapeAction from "../../actions/CutShapeAction";
import useSetting from "../../hooks/useSetting";
@@ -51,6 +54,7 @@ function MapFog({
onShapesCut,
onShapesRemove,
onShapesEdit,
onShapeError,
active,
toolSettings,
editable,
@@ -175,8 +179,7 @@ function MapFog({
}
const simplified = simplifyPoints(
[...prevPoints, brushPosition],
gridCellNormalizedSize,
stageScale / 4
1 / 1000 / stageScale
);
return {
...prevShape,
@@ -214,6 +217,8 @@ function MapFog({
) {
const cut = toolSettings.useFogCut;
let drawingShapes = [drawingShape];
// Filter out hidden or visible shapes if single layer enabled
if (!toolSettings.multilayer) {
const shapesToSubtract = shapes.filter((shape) =>
cut ? !shape.visible : shape.visible
@@ -228,22 +233,32 @@ function MapFog({
}
if (drawingShapes.length > 0) {
drawingShapes = drawingShapes.map((shape) => {
if (cut) {
return {
id: shape.id,
type: shape.type,
data: shape.data,
};
} else {
return { ...shape, color: "black" };
}
});
if (cut) {
onShapesCut(drawingShapes);
// Run a pre-emptive cut action to check whether we've cut anything
const cutAction = new CutShapeAction(drawingShapes);
const state = cutAction.execute(keyBy(shapes, "id"));
if (Object.keys(state).length === shapes.length) {
onShapeError("No fog to cut");
} else {
onShapesCut(
drawingShapes.map((shape) => ({
id: shape.id,
type: shape.type,
data: shape.data,
}))
);
}
} else {
onShapesAdd(drawingShapes);
onShapesAdd(
drawingShapes.map((shape) => ({ ...shape, color: "black" }))
);
}
} else {
if (cut) {
onShapeError("Fog already cut");
} else {
onShapeError("Fog already placed");
}
}
setDrawingShape(null);
@@ -373,6 +388,7 @@ function MapFog({
};
let polygonShapes = [polygonShape];
// Filter out hidden or visible shapes if single layer enabled
if (!toolSettings.multilayer) {
const shapesToSubtract = shapes.filter((shape) =>
cut ? !shape.visible : shape.visible
@@ -388,7 +404,15 @@ function MapFog({
if (polygonShapes.length > 0) {
if (cut) {
onShapesCut(polygonShapes);
// Run a pre-emptive cut action to check whether we've cut anything
const cutAction = new CutShapeAction(polygonShapes);
const state = cutAction.execute(keyBy(shapes, "id"));
if (Object.keys(state).length === shapes.length) {
onShapeError("No fog to cut");
} else {
onShapesCut(polygonShapes);
}
} else {
onShapesAdd(
polygonShapes.map((shape) => ({
@@ -399,10 +423,23 @@ function MapFog({
}))
);
}
} else {
if (cut) {
onShapeError("Fog already cut");
} else {
onShapeError("Fog already placed");
}
}
setDrawingShape(null);
}, [toolSettings, drawingShape, onShapesCut, onShapesAdd, shapes]);
}, [
toolSettings,
drawingShape,
onShapesCut,
onShapesAdd,
onShapeError,
shapes,
]);
// Add keyboard shortcuts
function handleKeyDown(event) {
@@ -489,6 +526,15 @@ function MapFog({
const holes =
shape.data.holes &&
shape.data.holes.map((hole) => hole.reduce(reducePoints, []));
const opacity = editable ? editOpacity : 1;
// Control opacity only on fill as using opacity with stroke leads to performance issues
const fill = new Color(colors[shape.color] || shape.color)
.alpha(opacity)
.string();
const stroke =
editable && active
? colors.lightGray
: colors[shape.color] || shape.color;
return (
<HoleyLine
key={shape.id}
@@ -499,19 +545,12 @@ function MapFog({
onMouseUp={eraseHoveredShapes}
onTouchEnd={eraseHoveredShapes}
points={points}
stroke={
editable && active
? colors.lightGray
: colors[shape.color] || shape.color
}
fill={colors[shape.color] || shape.color}
stroke={stroke}
fill={fill}
closed
lineCap="round"
lineJoin="round"
strokeWidth={gridStrokeWidth * shape.strokeWidth}
opacity={
editable ? (!shape.visible ? editOpacity / 2 : editOpacity) : 1
}
fillPatternImage={patternImage}
fillPriority={editable && !shape.visible ? "pattern" : "color"}
holes={holes}
@@ -590,15 +629,9 @@ function MapFog({
}
}, [shapes, editable, active, toolSettings, shouldRenderGuides]);
const fogGroupRef = useRef();
return (
<Group>
<Group ref={fogGroupRef}>
{/* Render a blank shape so cache works with no fog shapes */}
<Rect width={1} height={1} />
{fogShapes.map(renderShape)}
</Group>
<Group>{fogShapes.map(renderShape)}</Group>
{shouldRenderGuides && renderGuides()}
{drawingShape && renderShape(drawingShape)}
{drawingShape &&

View File

@@ -1,7 +1,7 @@
import React, { useEffect, useState } from "react";
import useImage from "use-image";
import { useImageSource } from "../../contexts/ImageSourceContext";
import { useDataURL } from "../../contexts/AssetsContext";
import { mapSources as defaultMapSources } from "../../maps";
@@ -11,15 +11,13 @@ import Grid from "../Grid";
function MapGrid({ map }) {
let mapSourceMap = map;
// Use lowest resolution for grid lightness
if (map && map.type === "file" && map.resolutions) {
const resolutionArray = Object.keys(map.resolutions);
if (resolutionArray.length > 0) {
mapSourceMap = map.resolutions[resolutionArray[0]];
}
}
const mapSource = useImageSource(mapSourceMap, defaultMapSources);
const [mapImage, mapLoadingStatus] = useImage(mapSource);
const mapURL = useDataURL(
mapSourceMap,
defaultMapSources,
undefined,
map.type === "file"
);
const [mapImage, mapLoadingStatus] = useImage(mapURL);
const [isImageLight, setIsImageLight] = useState(true);

View File

@@ -77,7 +77,10 @@ function MapGridEditor({ map, onGridChange }) {
Vector2.subtract(position, previousPosition)
);
const inset = map.grid.inset;
const inset = {
topLeft: { ...map.grid.inset.topLeft },
bottomRight: { ...map.grid.inset.bottomRight },
};
if (direction.x === 0 && direction.y === 0) {
return inset;

View File

@@ -0,0 +1,18 @@
import React from "react";
import { Image } from "theme-ui";
import { useDataURL } from "../../contexts/AssetsContext";
import { mapSources as defaultMapSources } from "../../maps";
const MapTileImage = React.forwardRef(({ map, ...props }, ref) => {
const mapURL = useDataURL(
map,
defaultMapSources,
undefined,
map.type === "file"
);
return <Image src={mapURL} ref={ref} {...props} />;
});
export default MapTileImage;

View File

@@ -28,22 +28,16 @@ function MapInteraction({
onSelectedToolChange,
disabledControls,
}) {
const [mapImageSource, mapImageSourceStatus] = useMapImage(map);
const [mapImage, mapImageStatus] = useMapImage(map);
// Map loaded taking in to account different resolutions
const [mapLoaded, setMapLoaded] = useState(false);
useEffect(() => {
if (
!map ||
!mapState ||
(map.type === "file" && !map.file && !map.resolutions) ||
mapState.mapId !== map.id
) {
if (!map || !mapState || mapState.mapId !== map.id) {
setMapLoaded(false);
} else if (mapImageSourceStatus === "loaded") {
} else if (mapImageStatus === "loaded") {
setMapLoaded(true);
}
}, [mapImageSourceStatus, map, mapState]);
}, [mapImageStatus, map, mapState]);
const [stageWidth, setStageWidth] = useState(1);
const [stageHeight, setStageHeight] = useState(1);
@@ -187,11 +181,12 @@ function MapInteraction({
<GridProvider grid={map?.grid} width={mapWidth} height={mapHeight}>
<Box
sx={{
flexGrow: 1,
position: "relative",
cursor: getCursorForTool(selectedToolId),
touchAction: "none",
outline: "none",
width: "100%",
height: "100%",
}}
ref={containerRef}
className="map"
@@ -211,7 +206,7 @@ function MapInteraction({
>
<Layer ref={mapLayerRef}>
<Image
image={mapLoaded && mapImageSource}
image={mapLoaded && mapImage}
width={mapWidth}
height={mapHeight}
id="mapImage"

View File

@@ -44,7 +44,10 @@ function MapMeasure({ map, active }) {
const gridScale = parseGridScale(active && grid.measurement.scale);
const snapPositionToGrid = useGridSnapping();
const snapPositionToGrid = useGridSnapping(
grid.measurement.type === "euclidean" ? 0 : 1,
false
);
useEffect(() => {
if (!active) {

View File

@@ -4,7 +4,7 @@ import { Group } from "react-konva";
import { useInteractionEmitter } from "../../contexts/MapInteractionContext";
import { useMapStage } from "../../contexts/MapStageContext";
import { useAuth } from "../../contexts/AuthContext";
import { useUserId } from "../../contexts/UserIdContext";
import Vector2 from "../../helpers/Vector2";
import { getRelativePointerPosition } from "../../helpers/konva";
@@ -28,7 +28,7 @@ function MapNotes({
fadeOnHover,
}) {
const interactionEmitter = useInteractionEmitter();
const { userId } = useAuth();
const userId = useUserId();
const mapStageRef = useMapStage();
const [isBrushDown, setIsBrushDown] = useState(false);
const [noteData, setNoteData] = useState(null);

View File

@@ -1,11 +1,12 @@
import React from "react";
import { Flex, Box, Label, Input, Checkbox, IconButton } from "theme-ui";
import ExpandMoreIcon from "../../icons/ExpandMoreIcon";
import React, { useEffect, useState } from "react";
import { Flex, Box, Label, Input, Checkbox } from "theme-ui";
import { isEmpty } from "../../helpers/shared";
import { getGridUpdatedInset } from "../../helpers/grid";
import { useDataURL } from "../../contexts/AssetsContext";
import { mapSources as defaultMapSources } from "../../maps";
import Divider from "../Divider";
import Select from "../Select";
@@ -40,8 +41,6 @@ function MapSettings({
mapState,
onSettingsChange,
onStateSettingsChange,
showMore,
onShowMoreChange,
}) {
function handleFlagChange(event, flag) {
if (event.target.checked) {
@@ -116,16 +115,22 @@ function MapSettings({
onSettingsChange("grid", grid);
}
function getMapSize() {
let size = 0;
if (map.quality === "original") {
size = map.file.length;
} else {
size = map.resolutions[map.quality].file.length;
const mapURL = useDataURL(map, defaultMapSources);
const [mapSize, setMapSize] = useState(0);
useEffect(() => {
async function updateMapSize() {
if (mapURL) {
const response = await fetch(mapURL);
const blob = await response.blob();
let size = blob.size;
size /= 1000000; // Bytes to Megabytes
setMapSize(size.toFixed(2));
} else {
setMapSize(0);
}
}
size /= 1000000; // Bytes to Megabytes
return `${size.toFixed(2)}MB`;
}
updateMapSize();
}, [mapURL]);
const mapEmpty = !map || isEmpty(map);
const mapStateEmpty = !mapState || isEmpty(mapState);
@@ -140,7 +145,7 @@ function MapSettings({
name="gridX"
value={`${(map && map.grid.size.x) || 0}`}
onChange={handleGridSizeXChange}
disabled={mapEmpty || map.type === "default"}
disabled={mapEmpty}
min={1}
my={1}
/>
@@ -152,7 +157,7 @@ function MapSettings({
name="gridY"
value={`${(map && map.grid.size.y) || 0}`}
onChange={handleGridSizeYChange}
disabled={mapEmpty || map.type === "default"}
disabled={mapEmpty}
min={1}
my={1}
/>
@@ -164,176 +169,146 @@ function MapSettings({
name="name"
value={(map && map.name) || ""}
onChange={(e) => onSettingsChange("name", e.target.value)}
disabled={mapEmpty || map.type === "default"}
disabled={mapEmpty}
my={1}
/>
</Box>
{showMore && (
<>
<Flex
mt={2}
mb={mapEmpty || map.type === "default" ? 2 : 0}
sx={{ flexDirection: "column" }}
>
<Flex sx={{ alignItems: "flex-end" }}>
<Box mb={1} sx={{ width: "50%" }}>
<Label mb={1}>Grid Type</Label>
<Select
isDisabled={mapEmpty || map.type === "default"}
options={gridTypeSettings}
value={
!mapEmpty &&
gridTypeSettings.find((s) => s.value === map.grid.type)
}
onChange={handleGridTypeChange}
isSearchable={false}
/>
</Box>
<Flex sx={{ width: "50%", flexDirection: "column" }} ml={2}>
<Label>
<Checkbox
checked={!mapEmpty && map.showGrid}
disabled={mapEmpty || map.type === "default"}
onChange={(e) =>
onSettingsChange("showGrid", e.target.checked)
}
/>
Draw Grid
</Label>
<Label>
<Checkbox
checked={!mapEmpty && map.snapToGrid}
disabled={mapEmpty || map.type === "default"}
onChange={(e) =>
onSettingsChange("snapToGrid", e.target.checked)
}
/>
Snap to Grid
</Label>
</Flex>
</Flex>
<Flex sx={{ alignItems: "flex-end" }}>
<Box my={2} sx={{ width: "50%" }}>
<Label mb={1}>Grid Measurement</Label>
<Select
isDisabled={mapEmpty || map.type === "default"}
options={
map && map.grid.type === "square"
? gridSquareMeasurementTypeSettings
: gridHexMeasurementTypeSettings
}
value={
!mapEmpty &&
gridSquareMeasurementTypeSettings.find(
(s) => s.value === map.grid.measurement.type
)
}
onChange={handleGridMeasurementTypeChange}
isSearchable={false}
/>
</Box>
<Box mb={1} mx={2} sx={{ flexGrow: 1 }}>
<Label htmlFor="gridMeasurementScale">Grid Scale</Label>
<Input
name="gridMeasurementScale"
value={`${map && map.grid.measurement.scale}`}
onChange={handleGridMeasurementScaleChange}
disabled={mapEmpty || map.type === "default"}
min={1}
my={1}
autoComplete="off"
/>
</Box>
</Flex>
</Flex>
{!mapEmpty && map.type !== "default" && (
<Flex my={2} sx={{ alignItems: "center" }}>
<Box mb={1} sx={{ width: "50%" }}>
<Label mb={1}>Quality</Label>
<Select
options={qualitySettings}
value={
!mapEmpty &&
qualitySettings.find((s) => s.value === map.quality)
}
isDisabled={mapEmpty}
onChange={(option) =>
onSettingsChange("quality", option.value)
}
isOptionDisabled={(option) =>
mapEmpty ||
(option.value !== "original" &&
!map.resolutions[option.value])
}
isSearchable={false}
/>
</Box>
<Label sx={{ width: "50%" }} ml={2}>
Size: {getMapSize()}
</Label>
</Flex>
)}
<Divider fill />
<Box my={2} sx={{ flexGrow: 1 }}>
<Label>Allow Others to Edit</Label>
<Flex my={1}>
<Label>
<Checkbox
checked={!mapStateEmpty && mapState.editFlags.includes("fog")}
disabled={mapStateEmpty}
onChange={(e) => handleFlagChange(e, "fog")}
/>
Fog
</Label>
<Label>
<Checkbox
checked={
!mapStateEmpty && mapState.editFlags.includes("drawing")
}
disabled={mapStateEmpty}
onChange={(e) => handleFlagChange(e, "drawing")}
/>
Drawings
</Label>
<Label>
<Checkbox
checked={
!mapStateEmpty && mapState.editFlags.includes("tokens")
}
disabled={mapStateEmpty}
onChange={(e) => handleFlagChange(e, "tokens")}
/>
Tokens
</Label>
<Label>
<Checkbox
checked={
!mapStateEmpty && mapState.editFlags.includes("notes")
}
disabled={mapStateEmpty}
onChange={(e) => handleFlagChange(e, "notes")}
/>
Notes
</Label>
</Flex>
</Box>
</>
)}
<IconButton
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
onShowMoreChange(!showMore);
}}
sx={{
transform: `rotate(${showMore ? "180deg" : "0"})`,
alignSelf: "center",
}}
aria-label={showMore ? "Show Less" : "Show More"}
title={showMore ? "Show Less" : "Show More"}
<Flex
mt={2}
mb={mapEmpty || map.type === "default" ? 2 : 0}
sx={{ flexDirection: "column" }}
>
<ExpandMoreIcon />
</IconButton>
<Flex sx={{ alignItems: "flex-end" }}>
<Box sx={{ width: "50%" }}>
<Label>Grid Type</Label>
<Select
isDisabled={mapEmpty}
options={gridTypeSettings}
value={
!mapEmpty &&
gridTypeSettings.find((s) => s.value === map.grid.type)
}
onChange={handleGridTypeChange}
isSearchable={false}
/>
</Box>
<Flex sx={{ flexGrow: 1, flexDirection: "column" }} ml={2}>
<Label>
<Checkbox
checked={!mapEmpty && map.showGrid}
disabled={mapEmpty}
onChange={(e) => onSettingsChange("showGrid", e.target.checked)}
/>
Draw Grid
</Label>
<Label>
<Checkbox
checked={!mapEmpty && map.snapToGrid}
disabled={mapEmpty}
onChange={(e) =>
onSettingsChange("snapToGrid", e.target.checked)
}
/>
Snap to Grid
</Label>
</Flex>
</Flex>
<Flex sx={{ alignItems: "flex-end" }}>
<Box my={2} sx={{ width: "50%" }}>
<Label>Grid Measurement</Label>
<Select
isDisabled={mapEmpty}
options={
map && map.grid.type === "square"
? gridSquareMeasurementTypeSettings
: gridHexMeasurementTypeSettings
}
value={
!mapEmpty &&
gridSquareMeasurementTypeSettings.find(
(s) => s.value === map.grid.measurement.type
)
}
onChange={handleGridMeasurementTypeChange}
isSearchable={false}
/>
</Box>
<Box m={2} mr={0} sx={{ flexGrow: 1 }}>
<Label htmlFor="gridMeasurementScale">Grid Scale</Label>
<Input
name="gridMeasurementScale"
value={`${map && map.grid.measurement.scale}`}
onChange={handleGridMeasurementScaleChange}
disabled={mapEmpty}
min={1}
my={1}
autoComplete="off"
/>
</Box>
</Flex>
</Flex>
{!mapEmpty && map.type !== "default" && (
<Flex my={2} sx={{ alignItems: "center" }}>
<Box mb={1} sx={{ width: "50%" }}>
<Label>Quality</Label>
<Select
options={qualitySettings}
value={
!mapEmpty &&
qualitySettings.find((s) => s.value === map.quality)
}
isDisabled={mapEmpty}
onChange={(option) => onSettingsChange("quality", option.value)}
isOptionDisabled={(option) =>
mapEmpty ||
(option.value !== "original" && !map.resolutions[option.value])
}
isSearchable={false}
/>
</Box>
<Label sx={{ width: "50%" }} ml={2}>
Size: {mapSize > 0 && `${mapSize}MB`}
</Label>
</Flex>
)}
<Divider fill />
<Box my={2} sx={{ flexGrow: 1 }}>
<Label>Allow Others to Edit</Label>
<Flex my={1}>
<Label>
<Checkbox
checked={!mapStateEmpty && mapState.editFlags.includes("fog")}
disabled={mapStateEmpty}
onChange={(e) => handleFlagChange(e, "fog")}
/>
Fog
</Label>
<Label>
<Checkbox
checked={!mapStateEmpty && mapState.editFlags.includes("drawing")}
disabled={mapStateEmpty}
onChange={(e) => handleFlagChange(e, "drawing")}
/>
Drawings
</Label>
<Label>
<Checkbox
checked={!mapStateEmpty && mapState.editFlags.includes("tokens")}
disabled={mapStateEmpty}
onChange={(e) => handleFlagChange(e, "tokens")}
/>
Tokens
</Label>
<Label>
<Checkbox
checked={!mapStateEmpty && mapState.editFlags.includes("notes")}
disabled={mapStateEmpty}
onChange={(e) => handleFlagChange(e, "notes")}
/>
Notes
</Label>
</Flex>
</Box>
</Flex>
);
}

View File

@@ -1,5 +0,0 @@
import React from "react";
function MapTest() {}
export default MapTest;

View File

@@ -1,40 +1,30 @@
import React from "react";
import Tile from "../Tile";
import { useImageSource } from "../../contexts/ImageSourceContext";
import { mapSources as defaultMapSources, unknownSource } from "../../maps";
import Tile from "../tile/Tile";
import MapImage from "./MapImage";
function MapTile({
map,
isSelected,
onMapSelect,
onMapEdit,
onDone,
size,
onSelect,
onEdit,
onDoubleClick,
canEdit,
badges,
}) {
const mapSource = useImageSource(
map,
defaultMapSources,
unknownSource,
map.type === "file"
);
return (
<Tile
src={mapSource}
title={map.name}
isSelected={isSelected}
onSelect={() => onMapSelect(map)}
onEdit={() => onMapEdit(map.id)}
onDoubleClick={() => canEdit && onDone()}
size={size}
onSelect={() => onSelect(map.id)}
onEdit={() => onEdit(map.id)}
onDoubleClick={() => canEdit && onDoubleClick()}
canEdit={canEdit}
badges={badges}
editTitle="Edit Map"
/>
>
<MapImage map={map} />
</Tile>
);
}

View File

@@ -0,0 +1,41 @@
import React from "react";
import { Grid } from "theme-ui";
import Tile from "../tile/Tile";
import MapImage from "./MapImage";
import useResponsiveLayout from "../../hooks/useResponsiveLayout";
function MapTileGroup({ group, maps, isSelected, onSelect, onDoubleClick }) {
const layout = useResponsiveLayout();
return (
<Tile
title={group.name}
isSelected={isSelected}
onSelect={() => onSelect(group.id)}
onDoubleClick={onDoubleClick}
>
<Grid
columns={`repeat(${layout.groupGridColumns}, 1fr)`}
p={2}
sx={{
gridGap: 2,
gridTemplateRows: `repeat(${layout.groupGridColumns}, 1fr)`,
}}
>
{maps
.slice(0, layout.groupGridColumns * layout.groupGridColumns)
.map((map) => (
<MapImage
sx={{ borderRadius: "8px" }}
map={map}
key={`${map.id}-group-tile`}
/>
))}
</Grid>
</Tile>
);
}
export default MapTileGroup;

View File

@@ -1,179 +1,68 @@
import React from "react";
import { Flex, Box, Text, IconButton, Close, Label } from "theme-ui";
import SimpleBar from "simplebar-react";
import Case from "case";
import RemoveMapIcon from "../../icons/RemoveMapIcon";
import ResetMapIcon from "../../icons/ResetMapIcon";
import GroupIcon from "../../icons/GroupIcon";
import MapTile from "./MapTile";
import Link from "../Link";
import FilterBar from "../FilterBar";
import MapTileGroup from "./MapTileGroup";
import { useDatabase } from "../../contexts/DatabaseContext";
import SortableTiles from "../tile/SortableTiles";
import SortableTilesDragOverlay from "../tile/SortableTilesDragOverlay";
import useResponsiveLayout from "../../hooks/useResponsiveLayout";
import { getGroupItems } from "../../helpers/group";
function MapTiles({
maps,
groups,
selectedMaps,
selectedMapStates,
onMapSelect,
onMapsRemove,
onMapsReset,
onMapAdd,
onMapEdit,
onDone,
selectMode,
onSelectModeChange,
search,
onSearchChange,
onMapsGroup,
}) {
const { databaseStatus } = useDatabase();
const layout = useResponsiveLayout();
import { useGroup } from "../../contexts/GroupContext";
let hasMapState = false;
for (let state of selectedMapStates) {
if (
Object.values(state.tokens).length > 0 ||
Object.values(state.drawShapes).length > 0 ||
Object.values(state.fogShapes).length > 0 ||
Object.values(state.notes).length > 0
) {
hasMapState = true;
break;
function MapTiles({ mapsById, onMapEdit, onMapSelect, subgroup }) {
const {
selectedGroupIds,
selectMode,
onGroupOpen,
onGroupSelect,
} = useGroup();
function renderTile(group) {
if (group.type === "item") {
const map = mapsById[group.id];
if (map) {
const isSelected = selectedGroupIds.includes(group.id);
const canEdit =
isSelected &&
selectMode === "single" &&
selectedGroupIds.length === 1;
return (
<MapTile
key={map.id}
map={map}
isSelected={isSelected}
onSelect={onGroupSelect}
onEdit={onMapEdit}
onDoubleClick={() => canEdit && onMapSelect(group.id)}
canEdit={canEdit}
badges={[`${map.grid.size.x}x${map.grid.size.y}`]}
/>
);
}
} else {
const isSelected = selectedGroupIds.includes(group.id);
const items = getGroupItems(group);
const canOpen =
isSelected && selectMode === "single" && selectedGroupIds.length === 1;
return (
<MapTileGroup
key={group.id}
group={group}
maps={items.map((item) => mapsById[item.id])}
isSelected={isSelected}
onSelect={onGroupSelect}
onDoubleClick={() => canOpen && onGroupOpen(group.id)}
/>
);
}
}
let hasSelectedDefaultMap = selectedMaps.some(
(map) => map.type === "default"
);
function mapToTile(map) {
const isSelected = selectedMaps.includes(map);
return (
<MapTile
key={map.id}
map={map}
isSelected={isSelected}
onMapSelect={onMapSelect}
onMapEdit={onMapEdit}
onDone={onDone}
size={layout.tileSize}
canEdit={
isSelected && selectMode === "single" && selectedMaps.length === 1
}
badges={[`${map.grid.size.x}x${map.grid.size.y}`]}
/>
);
}
const multipleSelected = selectedMaps.length > 1;
return (
<Box sx={{ position: "relative" }}>
<FilterBar
onFocus={() => onMapSelect()}
search={search}
onSearchChange={onSearchChange}
selectMode={selectMode}
onSelectModeChange={onSelectModeChange}
onAdd={onMapAdd}
addTitle="Add Map"
/>
<SimpleBar
style={{ height: layout.screenSize === "large" ? "600px" : "400px" }}
>
<Flex
p={2}
pb={4}
pt={databaseStatus === "disabled" ? 4 : 2}
bg="muted"
sx={{
flexWrap: "wrap",
borderRadius: "4px",
minHeight: layout.screenSize === "large" ? "600px" : "400px",
alignContent: "flex-start",
}}
onClick={() => onMapSelect()}
>
{groups.map((group) => (
<React.Fragment key={group}>
<Label mx={1} mt={2}>
{Case.capital(group)}
</Label>
{maps[group].map(mapToTile)}
</React.Fragment>
))}
</Flex>
</SimpleBar>
{databaseStatus === "disabled" && (
<Box
sx={{
position: "absolute",
top: "39px",
left: 0,
right: 0,
textAlign: "center",
borderRadius: "2px",
}}
bg="highlight"
p={1}
>
<Text as="p" variant="body2">
Map saving is unavailable. See <Link to="/faq#saving">FAQ</Link> for
more information.
</Text>
</Box>
)}
{selectedMaps.length > 0 && (
<Flex
sx={{
position: "absolute",
bottom: 0,
left: 0,
right: 0,
justifyContent: "space-between",
}}
bg="overlay"
>
<Close
title="Clear Selection"
aria-label="Clear Selection"
onClick={() => onMapSelect()}
/>
<Flex>
<IconButton
aria-label={multipleSelected ? "Group Maps" : "Group Map"}
title={multipleSelected ? "Group Maps" : "Group Map"}
onClick={() => onMapsGroup()}
disabled={hasSelectedDefaultMap}
>
<GroupIcon />
</IconButton>
<IconButton
aria-label={multipleSelected ? "Reset Maps" : "Reset Map"}
title={multipleSelected ? "Reset Maps" : "Reset Map"}
onClick={() => onMapsReset()}
disabled={!hasMapState}
>
<ResetMapIcon />
</IconButton>
<IconButton
aria-label={multipleSelected ? "Remove Maps" : "Remove Map"}
title={multipleSelected ? "Remove Maps" : "Remove Map"}
onClick={() => onMapsRemove()}
disabled={hasSelectedDefaultMap}
>
<RemoveMapIcon />
</IconButton>
</Flex>
</Flex>
)}
</Box>
<>
<SortableTiles renderTile={renderTile} subgroup={subgroup} />
<SortableTilesDragOverlay renderTile={renderTile} subgroup={subgroup} />
</>
);
}

View File

@@ -1,30 +1,29 @@
import React, { useState, useEffect, useRef } from "react";
import React, { useState, useRef } from "react";
import { Image as KonvaImage, Group } from "react-konva";
import { useSpring, animated } from "react-spring/konva";
import useImage from "use-image";
import Konva from "konva";
import useDebounce from "../../hooks/useDebounce";
import usePrevious from "../../hooks/usePrevious";
import useGridSnapping from "../../hooks/useGridSnapping";
import { useAuth } from "../../contexts/AuthContext";
import { useUserId } from "../../contexts/UserIdContext";
import {
useSetPreventMapInteraction,
useMapWidth,
useMapHeight,
useDebouncedStageScale,
} from "../../contexts/MapInteractionContext";
import { useGridCellPixelSize } from "../../contexts/GridContext";
import { useImageSource } from "../../contexts/ImageSourceContext";
import { useDataURL } from "../../contexts/AssetsContext";
import TokenStatus from "../token/TokenStatus";
import TokenLabel from "../token/TokenLabel";
import TokenOutline from "../token/TokenOutline";
import { tokenSources, unknownSource } from "../../tokens";
import { Intersection, getScaledOutline } from "../../helpers/token";
import { tokenSources } from "../../tokens";
function MapToken({
token,
tokenState,
onTokenStateChange,
onTokenMenuOpen,
@@ -34,34 +33,31 @@ function MapToken({
fadeOnHover,
map,
}) {
const { userId } = useAuth();
const userId = useUserId();
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);
const tokenURL = useDataURL(tokenState, tokenSources);
const [tokenImage] = useImage(tokenURL);
useEffect(() => {
if (tokenSourceImage) {
setTokenAspectRatio(tokenSourceImage.width / tokenSourceImage.height);
}
}, [tokenSourceImage]);
const tokenAspectRatio = tokenState.width / tokenState.height;
const snapPositionToGrid = useGridSnapping();
function handleDragStart(event) {
const tokenGroup = event.target;
const tokenImage = imageRef.current;
if (token && token.category === "vehicle") {
// Enable hit detection for .intersects() function
Konva.hitOnDragEnabled = true;
if (tokenState.category === "vehicle") {
const tokenIntersection = new Intersection(
getScaledOutline(tokenState, tokenWidth, tokenHeight),
{ x: tokenX - tokenWidth / 2, y: tokenY - tokenHeight / 2 },
{ x: tokenX, y: tokenY },
tokenState.rotation
);
// Find all other tokens on the map
const layer = tokenGroup.getLayer();
@@ -70,12 +66,7 @@ function MapToken({
if (other === tokenGroup) {
continue;
}
const otherRect = other.getClientRect();
const otherCenter = {
x: otherRect.x + otherRect.width / 2,
y: otherRect.y + otherRect.height / 2,
};
if (tokenImage.intersects(otherCenter)) {
if (tokenIntersection.intersects(other.position())) {
// Save and restore token position after moving layer
const position = other.absolutePosition();
other.moveTo(tokenGroup);
@@ -99,9 +90,7 @@ function MapToken({
const tokenGroup = event.target;
const mountChanges = {};
if (token && token.category === "vehicle") {
Konva.hitOnDragEnabled = false;
if (tokenState.category === "vehicle") {
const parent = tokenGroup.getParent();
const mountedTokens = tokenGroup.find(".character");
for (let mountedToken of mountedTokens) {
@@ -185,33 +174,6 @@ function MapToken({
const tokenWidth = minCellSize * tokenState.size;
const tokenHeight = (minCellSize / tokenAspectRatio) * tokenState.size;
const debouncedStageScale = useDebounce(stageScale, 50);
const imageRef = useRef();
useEffect(() => {
const image = imageRef.current;
if (!image) {
return;
}
const canvas = image.getCanvas();
const pixelRatio = canvas.pixelRatio || 1;
if (tokenSourceStatus === "loaded" && tokenWidth > 0 && tokenHeight > 0) {
const maxImageSize = token ? Math.max(token.width, token.height) : 512; // Default to 512px
const maxTokenSize = Math.max(tokenWidth, tokenHeight);
// Constrain image buffer to original image size
const maxRatio = maxImageSize / maxTokenSize;
image.cache({
pixelRatio: Math.min(
Math.max(debouncedStageScale * pixelRatio, 1),
maxRatio
),
});
image.drawHitFromCache();
}
}, [debouncedStageScale, tokenWidth, tokenHeight, tokenSourceStatus, token]);
// Animate to new token positions if edited by others
const tokenX = tokenState.x * mapWidth;
const tokenY = tokenState.y * mapHeight;
@@ -232,8 +194,8 @@ function MapToken({
// Token name is used by on click to find whether a token is a vehicle or prop
let tokenName = "";
if (token) {
tokenName = token.category;
if (tokenState) {
tokenName = tokenState.category;
}
if (tokenState && tokenState.locked) {
tokenName = tokenName + "-locked";
@@ -260,28 +222,46 @@ function MapToken({
name={tokenName}
id={tokenState.id}
>
<KonvaImage
ref={imageRef}
<Group
width={tokenWidth}
height={tokenHeight}
x={0}
y={0}
image={tokenSourceImage}
rotation={tokenState.rotation}
offsetX={tokenWidth / 2}
offsetY={tokenHeight / 2}
>
<TokenOutline
outline={getScaledOutline(tokenState, tokenWidth, tokenHeight)}
hidden={!!tokenImage}
/>
</Group>
<KonvaImage
width={tokenWidth}
height={tokenHeight}
x={0}
y={0}
image={tokenImage}
rotation={tokenState.rotation}
offsetX={tokenWidth / 2}
offsetY={tokenHeight / 2}
hitFunc={() => {}}
/>
<Group offsetX={tokenWidth / 2} offsetY={tokenHeight / 2}>
<TokenStatus
tokenState={tokenState}
width={tokenWidth}
height={tokenHeight}
/>
<TokenLabel
tokenState={tokenState}
width={tokenWidth}
height={tokenHeight}
/>
{tokenState.statuses?.length > 0 ? (
<TokenStatus
tokenState={tokenState}
width={tokenWidth}
height={tokenHeight}
/>
) : null}
{tokenState.label ? (
<TokenLabel
tokenState={tokenState}
width={tokenWidth}
height={tokenHeight}
/>
) : null}
</Group>
</animated.Group>
);

View File

@@ -1,10 +1,8 @@
import React, { useEffect } from "react";
import React from "react";
import { Group } from "react-konva";
import MapToken from "./MapToken";
import { useTokenData } from "../../contexts/TokenDataContext";
function MapTokens({
map,
mapState,
@@ -15,31 +13,6 @@ function MapTokens({
selectedToolId,
disabledTokens,
}) {
const { tokensById, loadTokens } = useTokenData();
// Ensure tokens files have been loaded into the token data
useEffect(() => {
async function loadFileTokens() {
const tokenIds = new Set(
Object.values(mapState.tokens).map((state) => state.tokenId)
);
const tokensToLoad = [];
for (let tokenId of tokenIds) {
const token = tokensById[tokenId];
if (token && token.type === "file" && !token.file) {
tokensToLoad.push(tokenId);
}
}
if (tokensToLoad.length > 0) {
await loadTokens(tokensToLoad);
}
}
if (mapState) {
loadFileTokens();
}
}, [mapState, tokensById, loadTokens]);
function getMapTokenCategoryWeight(category) {
switch (category) {
case "character":
@@ -55,38 +28,28 @@ function MapTokens({
// Sort so vehicles render below other tokens
function sortMapTokenStates(a, b, tokenDraggingOptions) {
const tokenA = tokensById[a.tokenId];
const tokenB = tokensById[b.tokenId];
if (tokenA && tokenB) {
// If categories are different sort in order "prop", "vehicle", "character"
if (tokenB.category !== tokenA.category) {
const aWeight = getMapTokenCategoryWeight(tokenA.category);
const bWeight = getMapTokenCategoryWeight(tokenB.category);
return bWeight - aWeight;
} else if (
tokenDraggingOptions &&
tokenDraggingOptions.dragging &&
tokenDraggingOptions.tokenState.id === a.id
) {
// If dragging token a move above
return 1;
} else if (
tokenDraggingOptions &&
tokenDraggingOptions.dragging &&
tokenDraggingOptions.tokenState.id === b.id
) {
// If dragging token b move above
return -1;
} else {
// Else sort so last modified is on top
return a.lastModified - b.lastModified;
}
} else if (tokenA) {
// If categories are different sort in order "prop", "vehicle", "character"
if (b.category !== a.category) {
const aWeight = getMapTokenCategoryWeight(a.category);
const bWeight = getMapTokenCategoryWeight(b.category);
return bWeight - aWeight;
} else if (
tokenDraggingOptions &&
tokenDraggingOptions.dragging &&
tokenDraggingOptions.tokenState.id === a.id
) {
// If dragging token a move above
return 1;
} else if (tokenB) {
} else if (
tokenDraggingOptions &&
tokenDraggingOptions.dragging &&
tokenDraggingOptions.tokenState.id === b.id
) {
// If dragging token b move above
return -1;
} else {
return 0;
// Else sort so last modified is on top
return a.lastModified - b.lastModified;
}
}
@@ -97,7 +60,6 @@ function MapTokens({
.map((tokenState) => (
<MapToken
key={tokenState.id}
token={tokensById[tokenState.tokenId]}
tokenState={tokenState}
onTokenStateChange={onMapTokenStateChange}
onTokenMenuOpen={handleTokenMenuOpen}

View File

@@ -5,7 +5,7 @@ import SelectMapModal from "../../modals/SelectMapModal";
import SelectMapIcon from "../../icons/SelectMapIcon";
import { useMapData } from "../../contexts/MapDataContext";
import { useAuth } from "../../contexts/AuthContext";
import { useUserId } from "../../contexts/UserIdContext";
function SelectMapButton({
onMapChange,
@@ -17,7 +17,7 @@ function SelectMapButton({
const [isModalOpen, setIsModalOpen] = useState(false);
const { updateMapState } = useMapData();
const { userId } = useAuth();
const userId = useUserId();
function openModal() {
if (currentMapState && currentMap && currentMap.owner === userId) {
updateMapState(currentMapState.mapId, currentMapState);

View File

@@ -0,0 +1,32 @@
import React from "react";
import { Button } from "theme-ui";
import { useGroup } from "../../contexts/GroupContext";
import { findGroup } from "../../helpers/group";
function SelectMapSelectButton({ onMapSelect, disabled }) {
const { activeGroups, selectedGroupIds } = useGroup();
function handleSelectClick() {
if (selectedGroupIds.length === 1) {
const group = findGroup(activeGroups, selectedGroupIds[0]);
if (group && group.type === "item") {
onMapSelect(group.id);
}
}
}
return (
<Button
variant="primary"
disabled={disabled || selectedGroupIds.length > 1}
onClick={handleSelectClick}
mt={2}
>
Select
</Button>
);
}
export default SelectMapSelectButton;