2020-04-24 17:53:42 +10:00
|
|
|
import React, { useRef, useState, useEffect, useContext } from "react";
|
2020-04-23 11:54:29 +10:00
|
|
|
import { Box, Button, Flex, Label, Input, Text } from "theme-ui";
|
2020-04-23 15:02:03 +10:00
|
|
|
import shortid from "shortid";
|
|
|
|
|
|
|
|
|
|
import db from "../database";
|
2020-04-13 18:15:00 +10:00
|
|
|
|
|
|
|
|
import Modal from "../components/Modal";
|
2020-04-23 18:01:40 +10:00
|
|
|
import MapTiles from "../components/map/MapTiles";
|
2020-04-23 11:54:29 +10:00
|
|
|
|
2020-04-24 17:53:42 +10:00
|
|
|
import AuthContext from "../contexts/AuthContext";
|
|
|
|
|
|
2020-04-24 15:50:05 +10:00
|
|
|
import { maps as defaultMaps } from "../maps";
|
2020-04-23 11:54:29 +10:00
|
|
|
|
|
|
|
|
const defaultMapSize = 22;
|
2020-04-23 17:23:34 +10:00
|
|
|
const defaultMapState = {
|
|
|
|
|
tokens: {},
|
|
|
|
|
// An index into the draw actions array to which only actions before the
|
|
|
|
|
// index will be performed (used in undo and redo)
|
|
|
|
|
drawActionIndex: -1,
|
|
|
|
|
drawActions: [],
|
|
|
|
|
};
|
2020-04-23 11:54:29 +10:00
|
|
|
|
2020-04-23 21:54:58 +10:00
|
|
|
function SelectMapModal({
|
|
|
|
|
isOpen,
|
|
|
|
|
onRequestClose,
|
|
|
|
|
onDone,
|
|
|
|
|
onMapChange,
|
|
|
|
|
onMapStateChange,
|
|
|
|
|
// The map currently being view in the map screen
|
|
|
|
|
currentMap,
|
|
|
|
|
}) {
|
2020-04-24 17:53:42 +10:00
|
|
|
const { userId } = useContext(AuthContext);
|
|
|
|
|
|
2020-04-23 11:54:29 +10:00
|
|
|
const [imageLoading, setImageLoading] = useState(false);
|
|
|
|
|
|
2020-04-23 21:54:58 +10:00
|
|
|
// The map selected in the modal
|
|
|
|
|
const [selectedMap, setSelectedMap] = useState(null);
|
2020-04-23 21:19:52 +10:00
|
|
|
const [maps, setMaps] = useState([]);
|
2020-04-23 20:32:33 +10:00
|
|
|
// Load maps from the database and ensure state is properly setup
|
2020-04-23 11:54:29 +10:00
|
|
|
useEffect(() => {
|
2020-04-24 17:53:42 +10:00
|
|
|
if (!userId) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
async function getDefaultMaps() {
|
2020-04-23 21:19:52 +10:00
|
|
|
const defaultMapsWithIds = [];
|
2020-04-24 15:50:05 +10:00
|
|
|
// Reverse maps to ensure the blank map is first in the list
|
|
|
|
|
const sortedMaps = [...defaultMaps].reverse();
|
|
|
|
|
for (let i = 0; i < sortedMaps.length; i++) {
|
|
|
|
|
const defaultMap = sortedMaps[i];
|
2020-04-24 17:53:42 +10:00
|
|
|
const id = `__default-${defaultMap.name}`;
|
2020-04-23 21:19:52 +10:00
|
|
|
defaultMapsWithIds.push({
|
|
|
|
|
...defaultMap,
|
|
|
|
|
id,
|
2020-04-24 17:53:42 +10:00
|
|
|
owner: userId,
|
2020-04-24 15:50:05 +10:00
|
|
|
// Emulate the time increasing to avoid sort errors
|
2020-04-23 21:19:52 +10:00
|
|
|
timestamp: Date.now() + i,
|
|
|
|
|
});
|
2020-04-24 17:53:42 +10:00
|
|
|
// Add a state for the map if there isn't one already
|
|
|
|
|
const state = await db.table("states").get(id);
|
|
|
|
|
if (!state) {
|
|
|
|
|
await db
|
|
|
|
|
.table("states")
|
|
|
|
|
.add({ ...defaultMapState, mapId: id, owner: userId });
|
|
|
|
|
}
|
2020-04-23 11:54:29 +10:00
|
|
|
}
|
2020-04-24 17:53:42 +10:00
|
|
|
return defaultMapsWithIds;
|
2020-04-23 15:02:03 +10:00
|
|
|
}
|
2020-04-23 20:32:33 +10:00
|
|
|
|
2020-04-23 21:19:52 +10:00
|
|
|
async function loadMaps() {
|
|
|
|
|
let storedMaps = await db.table("maps").toArray();
|
2020-04-24 17:53:42 +10:00
|
|
|
const defaultMapsWithIds = await getDefaultMaps();
|
|
|
|
|
const sortedMaps = [...defaultMapsWithIds, ...storedMaps].sort(
|
|
|
|
|
(a, b) => b.timestamp - a.timestamp
|
|
|
|
|
);
|
|
|
|
|
setMaps(sortedMaps);
|
2020-04-23 20:32:33 +10:00
|
|
|
}
|
|
|
|
|
|
2020-04-23 15:02:03 +10:00
|
|
|
loadMaps();
|
2020-04-24 17:53:42 +10:00
|
|
|
}, [userId]);
|
2020-04-13 18:15:00 +10:00
|
|
|
|
2020-04-23 15:02:03 +10:00
|
|
|
const [gridX, setGridX] = useState(defaultMapSize);
|
|
|
|
|
const [gridY, setGridY] = useState(defaultMapSize);
|
2020-04-13 18:15:00 +10:00
|
|
|
const fileInputRef = useRef();
|
|
|
|
|
|
2020-04-20 16:34:38 +10:00
|
|
|
function handleImageUpload(file) {
|
2020-04-23 11:54:29 +10:00
|
|
|
if (!file) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
let fileGridX = defaultMapSize;
|
|
|
|
|
let fileGridY = defaultMapSize;
|
2020-04-20 16:34:38 +10:00
|
|
|
if (file.name) {
|
|
|
|
|
// Match against a regex to find the grid size in the file name
|
|
|
|
|
// e.g. Cave 22x23 will return [["22x22", "22", "x", "23"]]
|
|
|
|
|
const gridMatches = [...file.name.matchAll(/(\d+) ?(x|X) ?(\d+)/g)];
|
|
|
|
|
if (gridMatches.length > 0) {
|
|
|
|
|
const lastMatch = gridMatches[gridMatches.length - 1];
|
|
|
|
|
const matchX = parseInt(lastMatch[1]);
|
|
|
|
|
const matchY = parseInt(lastMatch[3]);
|
|
|
|
|
if (!isNaN(matchX) && !isNaN(matchY)) {
|
2020-04-23 11:54:29 +10:00
|
|
|
fileGridX = matchX;
|
|
|
|
|
fileGridY = matchY;
|
2020-04-20 16:34:38 +10:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-04-23 11:54:29 +10:00
|
|
|
let image = new Image();
|
|
|
|
|
setImageLoading(true);
|
2020-04-24 15:50:05 +10:00
|
|
|
// Create and load the image temporarily to get its dimensions
|
|
|
|
|
const url = URL.createObjectURL(file);
|
2020-04-23 11:54:29 +10:00
|
|
|
image.onload = function () {
|
2020-04-23 15:02:03 +10:00
|
|
|
handleMapAdd({
|
|
|
|
|
file,
|
2020-04-24 15:50:05 +10:00
|
|
|
type: "file",
|
2020-04-23 15:02:03 +10:00
|
|
|
gridX: fileGridX,
|
|
|
|
|
gridY: fileGridY,
|
|
|
|
|
width: image.width,
|
|
|
|
|
height: image.height,
|
|
|
|
|
id: shortid.generate(),
|
2020-04-23 21:19:52 +10:00
|
|
|
timestamp: Date.now(),
|
2020-04-24 17:53:42 +10:00
|
|
|
owner: userId,
|
2020-04-23 11:54:29 +10:00
|
|
|
});
|
|
|
|
|
setImageLoading(false);
|
2020-04-24 15:50:05 +10:00
|
|
|
URL.revokeObjectURL(url);
|
2020-04-23 11:54:29 +10:00
|
|
|
};
|
|
|
|
|
image.src = url;
|
2020-04-20 16:34:38 +10:00
|
|
|
}
|
|
|
|
|
|
2020-04-13 18:15:00 +10:00
|
|
|
function openImageDialog() {
|
|
|
|
|
if (fileInputRef.current) {
|
|
|
|
|
fileInputRef.current.click();
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-04-15 21:15:16 +10:00
|
|
|
|
2020-04-23 15:02:03 +10:00
|
|
|
async function handleMapAdd(map) {
|
|
|
|
|
await db.table("maps").add(map);
|
2020-04-24 17:53:42 +10:00
|
|
|
await db
|
|
|
|
|
.table("states")
|
|
|
|
|
.add({ ...defaultMapState, mapId: map.id, owner: userId });
|
2020-04-23 15:02:03 +10:00
|
|
|
setMaps((prevMaps) => [map, ...prevMaps]);
|
2020-04-23 21:54:58 +10:00
|
|
|
setSelectedMap(map);
|
2020-04-23 15:02:03 +10:00
|
|
|
setGridX(map.gridX);
|
|
|
|
|
setGridY(map.gridY);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function handleMapRemove(id) {
|
|
|
|
|
await db.table("maps").delete(id);
|
2020-04-23 17:23:34 +10:00
|
|
|
await db.table("states").delete(id);
|
2020-04-23 15:02:03 +10:00
|
|
|
setMaps((prevMaps) => {
|
|
|
|
|
const filtered = prevMaps.filter((map) => map.id !== id);
|
2020-04-23 21:54:58 +10:00
|
|
|
setSelectedMap(filtered[0]);
|
2020-04-23 15:02:03 +10:00
|
|
|
return filtered;
|
|
|
|
|
});
|
2020-04-23 21:54:58 +10:00
|
|
|
// Removed the map from the map screen if needed
|
2020-04-24 15:50:05 +10:00
|
|
|
if (currentMap && currentMap.id === selectedMap.id) {
|
2020-04-24 16:18:48 +10:00
|
|
|
onMapChange(null, null);
|
2020-04-23 21:54:58 +10:00
|
|
|
}
|
2020-04-23 15:02:03 +10:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleMapSelect(map) {
|
2020-04-23 21:54:58 +10:00
|
|
|
setSelectedMap(map);
|
2020-04-23 15:02:03 +10:00
|
|
|
setGridX(map.gridX);
|
|
|
|
|
setGridY(map.gridY);
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-23 20:32:33 +10:00
|
|
|
async function handleMapReset(id) {
|
2020-04-24 17:53:42 +10:00
|
|
|
const state = { ...defaultMapState, mapId: id, owner: userId };
|
2020-04-23 21:54:58 +10:00
|
|
|
await db.table("states").put(state);
|
|
|
|
|
// Reset the state of the current map if needed
|
2020-04-24 15:50:05 +10:00
|
|
|
if (currentMap && currentMap.id === selectedMap.id) {
|
2020-04-23 21:54:58 +10:00
|
|
|
onMapStateChange(state);
|
|
|
|
|
}
|
2020-04-23 20:32:33 +10:00
|
|
|
}
|
|
|
|
|
|
2020-04-23 17:23:34 +10:00
|
|
|
async function handleSubmit(e) {
|
2020-04-23 15:02:03 +10:00
|
|
|
e.preventDefault();
|
2020-04-23 21:54:58 +10:00
|
|
|
if (selectedMap) {
|
2020-04-24 17:53:42 +10:00
|
|
|
let currentMapState = await db.table("states").get(selectedMap.id);
|
2020-04-24 16:18:48 +10:00
|
|
|
onMapChange(selectedMap, currentMapState);
|
2020-04-23 21:54:58 +10:00
|
|
|
onDone();
|
2020-04-23 17:23:34 +10:00
|
|
|
}
|
2020-04-23 21:54:58 +10:00
|
|
|
onDone();
|
2020-04-23 15:02:03 +10:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function handleGridXChange(e) {
|
|
|
|
|
const newX = e.target.value;
|
2020-04-23 21:54:58 +10:00
|
|
|
await db.table("maps").update(selectedMap.id, { gridX: newX });
|
2020-04-23 15:02:03 +10:00
|
|
|
setGridX(newX);
|
|
|
|
|
setMaps((prevMaps) => {
|
|
|
|
|
const newMaps = [...prevMaps];
|
2020-04-23 21:54:58 +10:00
|
|
|
const i = newMaps.findIndex((map) => map.id === selectedMap.id);
|
2020-04-23 15:02:03 +10:00
|
|
|
if (i > -1) {
|
|
|
|
|
newMaps[i].gridX = newX;
|
|
|
|
|
}
|
|
|
|
|
return newMaps;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function handleGridYChange(e) {
|
|
|
|
|
const newY = e.target.value;
|
2020-04-23 21:54:58 +10:00
|
|
|
await db.table("maps").update(selectedMap.id, { gridY: newY });
|
2020-04-23 15:02:03 +10:00
|
|
|
setGridY(newY);
|
|
|
|
|
setMaps((prevMaps) => {
|
|
|
|
|
const newMaps = [...prevMaps];
|
2020-04-23 21:54:58 +10:00
|
|
|
const i = newMaps.findIndex((map) => map.id === selectedMap.id);
|
2020-04-23 15:02:03 +10:00
|
|
|
if (i > -1) {
|
|
|
|
|
newMaps[i].gridY = newY;
|
|
|
|
|
}
|
|
|
|
|
return newMaps;
|
|
|
|
|
});
|
2020-04-23 13:31:54 +10:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Drag and Drop
|
|
|
|
|
*/
|
2020-04-15 21:15:16 +10:00
|
|
|
const [dragging, setDragging] = useState(false);
|
|
|
|
|
function handleImageDragEnter(event) {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
event.stopPropagation();
|
|
|
|
|
setDragging(true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleImageDragLeave(event) {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
event.stopPropagation();
|
|
|
|
|
setDragging(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleImageDrop(event) {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
event.stopPropagation();
|
|
|
|
|
const file = event.dataTransfer.files[0];
|
2020-04-15 21:57:18 +10:00
|
|
|
if (file && file.type.startsWith("image")) {
|
2020-04-20 16:34:38 +10:00
|
|
|
handleImageUpload(file);
|
2020-04-15 21:15:16 +10:00
|
|
|
}
|
|
|
|
|
setDragging(false);
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-13 18:15:00 +10:00
|
|
|
return (
|
|
|
|
|
<Modal isOpen={isOpen} onRequestClose={onRequestClose}>
|
2020-04-23 15:02:03 +10:00
|
|
|
<Box as="form" onSubmit={handleSubmit} onDragEnter={handleImageDragEnter}>
|
2020-04-13 18:15:00 +10:00
|
|
|
<input
|
2020-04-20 16:34:38 +10:00
|
|
|
onChange={(event) => handleImageUpload(event.target.files[0])}
|
2020-04-13 18:15:00 +10:00
|
|
|
type="file"
|
|
|
|
|
accept="image/*"
|
|
|
|
|
style={{ display: "none" }}
|
|
|
|
|
ref={fileInputRef}
|
|
|
|
|
/>
|
2020-04-15 21:15:16 +10:00
|
|
|
<Flex
|
|
|
|
|
sx={{
|
|
|
|
|
flexDirection: "column",
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Label pt={2} pb={1}>
|
2020-04-23 18:01:40 +10:00
|
|
|
Select or import a map
|
2020-04-15 21:15:16 +10:00
|
|
|
</Label>
|
2020-04-23 18:01:40 +10:00
|
|
|
<MapTiles
|
2020-04-23 13:31:54 +10:00
|
|
|
maps={maps}
|
|
|
|
|
onMapAdd={openImageDialog}
|
2020-04-23 15:02:03 +10:00
|
|
|
onMapRemove={handleMapRemove}
|
2020-04-23 21:54:58 +10:00
|
|
|
selectedMap={selectedMap && selectedMap.id}
|
2020-04-23 15:02:03 +10:00
|
|
|
onMapSelect={handleMapSelect}
|
2020-04-23 20:32:33 +10:00
|
|
|
onMapReset={handleMapReset}
|
2020-04-23 22:12:50 +10:00
|
|
|
onSubmit={handleSubmit}
|
2020-04-23 13:31:54 +10:00
|
|
|
/>
|
2020-04-13 18:15:00 +10:00
|
|
|
<Flex>
|
|
|
|
|
<Box mb={2} mr={1} sx={{ flexGrow: 1 }}>
|
2020-04-20 14:49:38 +10:00
|
|
|
<Label htmlFor="gridX">Columns</Label>
|
2020-04-13 18:15:00 +10:00
|
|
|
<Input
|
|
|
|
|
type="number"
|
2020-04-20 14:49:38 +10:00
|
|
|
name="gridX"
|
|
|
|
|
value={gridX}
|
2020-04-23 15:02:03 +10:00
|
|
|
onChange={handleGridXChange}
|
2020-04-23 21:54:58 +10:00
|
|
|
disabled={selectedMap === null || selectedMap.default}
|
2020-04-23 15:02:03 +10:00
|
|
|
min={1}
|
2020-04-13 18:15:00 +10:00
|
|
|
/>
|
|
|
|
|
</Box>
|
|
|
|
|
<Box mb={2} ml={1} sx={{ flexGrow: 1 }}>
|
2020-04-20 14:49:38 +10:00
|
|
|
<Label htmlFor="gridY">Rows</Label>
|
2020-04-13 18:15:00 +10:00
|
|
|
<Input
|
|
|
|
|
type="number"
|
2020-04-20 14:49:38 +10:00
|
|
|
name="gridY"
|
|
|
|
|
value={gridY}
|
2020-04-23 15:02:03 +10:00
|
|
|
onChange={handleGridYChange}
|
2020-04-23 21:54:58 +10:00
|
|
|
disabled={selectedMap === null || selectedMap.default}
|
2020-04-23 15:02:03 +10:00
|
|
|
min={1}
|
2020-04-13 18:15:00 +10:00
|
|
|
/>
|
|
|
|
|
</Box>
|
|
|
|
|
</Flex>
|
2020-04-23 11:54:29 +10:00
|
|
|
<Button variant="primary" disabled={imageLoading}>
|
|
|
|
|
Done
|
|
|
|
|
</Button>
|
2020-04-15 21:15:16 +10:00
|
|
|
{dragging && (
|
|
|
|
|
<Flex
|
|
|
|
|
bg="muted"
|
|
|
|
|
sx={{
|
|
|
|
|
position: "absolute",
|
|
|
|
|
top: 0,
|
|
|
|
|
right: 0,
|
|
|
|
|
left: 0,
|
|
|
|
|
bottom: 0,
|
|
|
|
|
justifyContent: "center",
|
|
|
|
|
alignItems: "center",
|
|
|
|
|
cursor: "copy",
|
|
|
|
|
}}
|
|
|
|
|
onDragLeave={handleImageDragLeave}
|
|
|
|
|
onDragOver={(e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
e.dataTransfer.dropEffect = "copy";
|
|
|
|
|
}}
|
|
|
|
|
onDrop={handleImageDrop}
|
|
|
|
|
>
|
|
|
|
|
<Text sx={{ pointerEvents: "none" }}>Drop map to upload</Text>
|
|
|
|
|
</Flex>
|
|
|
|
|
)}
|
2020-04-13 18:15:00 +10:00
|
|
|
</Flex>
|
|
|
|
|
</Box>
|
|
|
|
|
</Modal>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-23 18:01:40 +10:00
|
|
|
export default SelectMapModal;
|