import React, { useRef, useState, useEffect } from "react"; import { Box, Button, Flex, Label, Input, Text } from "theme-ui"; import shortid from "shortid"; import db from "../database"; import Modal from "../components/Modal"; import MapTiles from "../components/map/MapTiles"; import * as defaultMaps from "../maps"; const defaultMapSize = 22; 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: [], }; function SelectMapModal({ isOpen, onRequestClose, onDone, onMapChange, onMapStateChange, // The map currently being view in the map screen currentMap, }) { const [imageLoading, setImageLoading] = useState(false); // The map selected in the modal const [selectedMap, setSelectedMap] = useState(null); const [maps, setMaps] = useState([]); // Load maps from the database and ensure state is properly setup useEffect(() => { async function loadDefaultMaps() { const defaultMapsWithIds = []; const defaultMapStates = []; // Store the default maps into the db in reverse so the whie map is first // in the UI const defaultMapArray = Object.values(defaultMaps).reverse(); for (let i = 0; i < defaultMapArray.length; i++) { const defaultMap = defaultMapArray[i]; const id = `${defaultMap.id}--${shortid.generate()}`; defaultMapsWithIds.push({ ...defaultMap, id, timestamp: Date.now() + i, }); defaultMapStates.push({ ...defaultMapState, mapId: id }); } await db.table("maps").bulkAdd(defaultMapsWithIds); await db.table("states").bulkAdd(defaultMapStates); setMaps(defaultMapsWithIds.sort((a, b) => b.timestamp - a.timestamp)); } async function loadMaps() { let storedMaps = await db.table("maps").toArray(); // If we have no stored maps load the default maps if (storedMaps.length === 0) { loadDefaultMaps(); } else { // Sort maps by the time they were added storedMaps.sort((a, b) => b.timestamp - a.timestamp); for (let map of storedMaps) { // Recreate image urls for file based maps if (map.file) { map.source = URL.createObjectURL(map.file); } } setMaps(storedMaps); } } loadMaps(); }, []); const [gridX, setGridX] = useState(defaultMapSize); const [gridY, setGridY] = useState(defaultMapSize); const fileInputRef = useRef(); function handleImageUpload(file) { if (!file) { return; } let fileGridX = defaultMapSize; let fileGridY = defaultMapSize; 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)) { fileGridX = matchX; fileGridY = matchY; } } } const url = URL.createObjectURL(file); let image = new Image(); setImageLoading(true); image.onload = function () { handleMapAdd({ file, gridX: fileGridX, gridY: fileGridY, width: image.width, height: image.height, source: url, id: shortid.generate(), timestamp: Date.now(), }); setImageLoading(false); }; image.src = url; } function openImageDialog() { if (fileInputRef.current) { fileInputRef.current.click(); } } async function handleMapAdd(map) { await db.table("maps").add(map); await db.table("states").add({ ...defaultMapState, mapId: map.id }); setMaps((prevMaps) => [map, ...prevMaps]); setSelectedMap(map); setGridX(map.gridX); setGridY(map.gridY); } // Keep track of removed maps async function handleMapRemove(id) { await db.table("maps").delete(id); await db.table("states").delete(id); setMaps((prevMaps) => { const filtered = prevMaps.filter((map) => map.id !== id); setSelectedMap(filtered[0]); return filtered; }); // Removed the map from the map screen if needed if (currentMap.id === selectedMap.id) { onMapChange(null); } } function handleMapSelect(map) { setSelectedMap(map); setGridX(map.gridX); setGridY(map.gridY); } async function handleMapReset(id) { const state = { ...defaultMapState, mapId: id }; await db.table("states").put(state); // Reset the state of the current map if needed if (currentMap.id === selectedMap.id) { onMapStateChange(state); } } async function handleSubmit(e) { e.preventDefault(); if (selectedMap) { let currentMapState = (await db.table("states").get(selectedMap.id)) || defaultMapState; onMapStateChange(currentMapState); onMapChange(selectedMap); onDone(); } onDone(); } async function handleGridXChange(e) { const newX = e.target.value; await db.table("maps").update(selectedMap.id, { gridX: newX }); setGridX(newX); setMaps((prevMaps) => { const newMaps = [...prevMaps]; const i = newMaps.findIndex((map) => map.id === selectedMap.id); if (i > -1) { newMaps[i].gridX = newX; } return newMaps; }); } async function handleGridYChange(e) { const newY = e.target.value; await db.table("maps").update(selectedMap.id, { gridY: newY }); setGridY(newY); setMaps((prevMaps) => { const newMaps = [...prevMaps]; const i = newMaps.findIndex((map) => map.id === selectedMap.id); if (i > -1) { newMaps[i].gridY = newY; } return newMaps; }); } /** * Drag and Drop */ 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]; if (file && file.type.startsWith("image")) { handleImageUpload(file); } setDragging(false); } return ( handleImageUpload(event.target.files[0])} type="file" accept="image/*" style={{ display: "none" }} ref={fileInputRef} /> {dragging && ( { e.preventDefault(); e.stopPropagation(); e.dataTransfer.dropEffect = "copy"; }} onDrop={handleImageDrop} > Drop map to upload )} ); } export default SelectMapModal;