2020-10-10 11:29:42 +11:00
|
|
|
import React, { useRef, useContext, useState, useEffect } from "react";
|
2020-05-20 12:37:29 +10:00
|
|
|
import { Flex, Label, Button } from "theme-ui";
|
2020-05-19 19:03:36 +10:00
|
|
|
import shortid from "shortid";
|
2020-10-09 13:10:30 +11:00
|
|
|
import Case from "case";
|
2020-05-19 16:21:01 +10:00
|
|
|
|
2020-10-01 22:32:21 +10:00
|
|
|
import EditTokenModal from "./EditTokenModal";
|
|
|
|
|
import EditGroupModal from "./EditGroupModal";
|
2020-10-10 15:32:59 +11:00
|
|
|
import ConfirmModal from "./ConfirmModal";
|
2020-10-01 22:32:21 +10:00
|
|
|
|
2020-05-19 16:21:01 +10:00
|
|
|
import Modal from "../components/Modal";
|
|
|
|
|
import ImageDrop from "../components/ImageDrop";
|
|
|
|
|
import TokenTiles from "../components/token/TokenTiles";
|
2020-11-26 16:29:10 +11:00
|
|
|
import LoadingOverlay from "../components/LoadingOverlay";
|
2020-05-19 16:21:01 +10:00
|
|
|
|
2020-05-19 19:03:36 +10:00
|
|
|
import blobToBuffer from "../helpers/blobToBuffer";
|
2020-10-01 22:32:21 +10:00
|
|
|
import useKeyboard from "../helpers/useKeyboard";
|
|
|
|
|
import { useSearch, useGroup, handleItemSelect } from "../helpers/select";
|
2020-05-19 19:03:36 +10:00
|
|
|
|
2020-05-19 16:21:01 +10:00
|
|
|
import TokenDataContext from "../contexts/TokenDataContext";
|
2020-05-19 19:03:36 +10:00
|
|
|
import AuthContext from "../contexts/AuthContext";
|
2020-05-19 16:21:01 +10:00
|
|
|
|
|
|
|
|
function SelectTokensModal({ isOpen, onRequestClose }) {
|
2020-05-19 19:03:36 +10:00
|
|
|
const { userId } = useContext(AuthContext);
|
2020-11-26 16:29:10 +11:00
|
|
|
const {
|
|
|
|
|
ownedTokens,
|
|
|
|
|
addToken,
|
|
|
|
|
removeTokens,
|
|
|
|
|
updateTokens,
|
|
|
|
|
tokensLoading,
|
|
|
|
|
} = useContext(TokenDataContext);
|
2020-05-19 16:21:01 +10:00
|
|
|
|
2020-10-01 22:32:21 +10:00
|
|
|
/**
|
|
|
|
|
* Search
|
|
|
|
|
*/
|
|
|
|
|
const [search, setSearch] = useState("");
|
|
|
|
|
const [filteredTokens, filteredTokenScores] = useSearch(ownedTokens, search);
|
2020-05-19 19:03:36 +10:00
|
|
|
|
2020-10-01 22:32:21 +10:00
|
|
|
function handleSearchChange(event) {
|
|
|
|
|
setSearch(event.target.value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Group
|
|
|
|
|
*/
|
|
|
|
|
const [isGroupModalOpen, setIsGroupModalOpen] = useState(false);
|
|
|
|
|
|
|
|
|
|
async function handleTokensGroup(group) {
|
|
|
|
|
setIsGroupModalOpen(false);
|
|
|
|
|
await updateTokens(selectedTokenIds, { group });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const [tokensByGroup, tokenGroups] = useGroup(
|
|
|
|
|
ownedTokens,
|
|
|
|
|
filteredTokens,
|
|
|
|
|
!!search,
|
|
|
|
|
filteredTokenScores
|
2020-05-19 19:03:36 +10:00
|
|
|
);
|
|
|
|
|
|
2020-10-01 22:32:21 +10:00
|
|
|
/**
|
|
|
|
|
* Image Upload
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
const fileInputRef = useRef();
|
|
|
|
|
const [imageLoading, setImageLoading] = useState(false);
|
|
|
|
|
|
2020-05-19 16:21:01 +10:00
|
|
|
function openImageDialog() {
|
|
|
|
|
if (fileInputRef.current) {
|
|
|
|
|
fileInputRef.current.click();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-31 10:53:33 +10:00
|
|
|
async function handleImagesUpload(files) {
|
|
|
|
|
for (let file of files) {
|
|
|
|
|
await handleImageUpload(file);
|
|
|
|
|
}
|
|
|
|
|
// Set file input to null to allow adding the same image 2 times in a row
|
|
|
|
|
fileInputRef.current.value = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function handleImageUpload(file) {
|
2020-05-19 19:03:36 +10:00
|
|
|
let name = "Unknown Map";
|
|
|
|
|
if (file.name) {
|
|
|
|
|
// Remove file extension
|
|
|
|
|
name = file.name.replace(/\.[^/.]+$/, "");
|
|
|
|
|
// Removed grid size expression
|
|
|
|
|
name = name.replace(/(\[ ?|\( ?)?\d+ ?(x|X) ?\d+( ?\]| ?\))?/, "");
|
|
|
|
|
// Clean string
|
|
|
|
|
name = name.replace(/ +/g, " ");
|
|
|
|
|
name = name.trim();
|
2020-10-09 13:10:30 +11:00
|
|
|
// Capitalize and remove underscores
|
|
|
|
|
name = Case.capital(name);
|
2020-05-19 19:03:36 +10:00
|
|
|
}
|
|
|
|
|
let image = new Image();
|
|
|
|
|
setImageLoading(true);
|
2020-05-31 10:53:33 +10:00
|
|
|
const buffer = await blobToBuffer(file);
|
|
|
|
|
|
|
|
|
|
// Copy file to avoid permissions issues
|
|
|
|
|
const blob = new Blob([buffer]);
|
|
|
|
|
// Create and load the image temporarily to get its dimensions
|
|
|
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
2020-05-19 19:03:36 +10:00
|
|
|
image.onload = function () {
|
|
|
|
|
handleTokenAdd({
|
|
|
|
|
file: buffer,
|
|
|
|
|
name,
|
|
|
|
|
id: shortid.generate(),
|
2020-06-28 15:43:45 +10:00
|
|
|
type: "file",
|
2020-05-19 19:03:36 +10:00
|
|
|
created: Date.now(),
|
|
|
|
|
lastModified: Date.now(),
|
2020-09-11 16:56:40 +10:00
|
|
|
lastUsed: Date.now(),
|
2020-05-19 19:03:36 +10:00
|
|
|
owner: userId,
|
2020-05-20 12:37:29 +10:00
|
|
|
defaultSize: 1,
|
2020-08-27 19:09:16 +10:00
|
|
|
category: "character",
|
2020-05-28 16:23:20 +10:00
|
|
|
hideInSidebar: false,
|
2020-10-01 22:32:21 +10:00
|
|
|
group: "",
|
2020-10-22 16:09:27 +11:00
|
|
|
width: image.width,
|
|
|
|
|
height: image.height,
|
2020-05-19 19:03:36 +10:00
|
|
|
});
|
2020-05-22 18:04:17 +10:00
|
|
|
setImageLoading(false);
|
2020-05-31 10:53:33 +10:00
|
|
|
resolve();
|
2020-05-19 19:03:36 +10:00
|
|
|
};
|
2020-05-31 10:53:33 +10:00
|
|
|
image.onerror = reject;
|
2020-05-19 19:03:36 +10:00
|
|
|
image.src = url;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-20 12:37:29 +10:00
|
|
|
/**
|
2020-10-01 22:32:21 +10:00
|
|
|
* Token controls
|
2020-05-20 12:37:29 +10:00
|
|
|
*/
|
2020-10-01 22:32:21 +10:00
|
|
|
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
|
|
|
|
|
const [selectedTokenIds, setSelectedTokenIds] = useState([]);
|
|
|
|
|
const selectedTokens = ownedTokens.filter((token) =>
|
|
|
|
|
selectedTokenIds.includes(token.id)
|
|
|
|
|
);
|
2020-05-20 12:37:29 +10:00
|
|
|
|
2020-10-01 22:32:21 +10:00
|
|
|
function handleTokenAdd(token) {
|
|
|
|
|
addToken(token);
|
|
|
|
|
setSelectedTokenIds([token.id]);
|
|
|
|
|
}
|
2020-06-28 15:43:45 +10:00
|
|
|
|
2020-10-10 15:32:59 +11:00
|
|
|
const [isTokensRemoveModalOpen, setIsTokensRemoveModalOpen] = useState(false);
|
2020-10-01 22:32:21 +10:00
|
|
|
async function handleTokensRemove() {
|
2020-10-10 15:32:59 +11:00
|
|
|
setIsTokensRemoveModalOpen(false);
|
2020-10-01 22:32:21 +10:00
|
|
|
await removeTokens(selectedTokenIds);
|
|
|
|
|
setSelectedTokenIds([]);
|
2020-06-28 15:43:45 +10:00
|
|
|
}
|
|
|
|
|
|
2020-10-09 16:18:52 +11:00
|
|
|
async function handleTokensHide(hideInSidebar) {
|
|
|
|
|
await updateTokens(selectedTokenIds, { hideInSidebar });
|
|
|
|
|
}
|
|
|
|
|
|
2020-10-01 22:32:21 +10:00
|
|
|
// Either single, multiple or range
|
|
|
|
|
const [selectMode, setSelectMode] = useState("single");
|
2020-08-07 13:56:03 +10:00
|
|
|
|
2020-10-01 22:32:21 +10:00
|
|
|
async function handleTokenSelect(token) {
|
|
|
|
|
handleItemSelect(
|
|
|
|
|
token,
|
|
|
|
|
selectMode,
|
|
|
|
|
selectedTokenIds,
|
|
|
|
|
setSelectedTokenIds,
|
|
|
|
|
tokensByGroup,
|
|
|
|
|
tokenGroups
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Shortcuts
|
|
|
|
|
*/
|
|
|
|
|
function handleKeyDown({ key }) {
|
|
|
|
|
if (!isOpen) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (key === "Shift") {
|
|
|
|
|
setSelectMode("range");
|
|
|
|
|
}
|
|
|
|
|
if (key === "Control" || key === "Meta") {
|
|
|
|
|
setSelectMode("multiple");
|
2020-06-28 15:43:45 +10:00
|
|
|
}
|
2020-10-10 15:44:07 +11:00
|
|
|
if (key === "Backspace" || key === "Delete") {
|
|
|
|
|
// Selected tokens and none are default
|
|
|
|
|
if (
|
|
|
|
|
selectedTokenIds.length > 0 &&
|
|
|
|
|
!selectedTokens.some((token) => token.type === "default")
|
|
|
|
|
) {
|
|
|
|
|
setIsTokensRemoveModalOpen(true);
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-05-20 12:37:29 +10:00
|
|
|
}
|
|
|
|
|
|
2020-10-01 22:32:21 +10:00
|
|
|
function handleKeyUp({ key }) {
|
|
|
|
|
if (!isOpen) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (key === "Shift" && selectMode === "range") {
|
|
|
|
|
setSelectMode("single");
|
|
|
|
|
}
|
|
|
|
|
if ((key === "Control" || key === "Meta") && selectMode === "multiple") {
|
|
|
|
|
setSelectMode("single");
|
|
|
|
|
}
|
2020-06-28 15:43:45 +10:00
|
|
|
}
|
|
|
|
|
|
2020-10-01 22:32:21 +10:00
|
|
|
useKeyboard(handleKeyDown, handleKeyUp);
|
2020-06-28 15:43:45 +10:00
|
|
|
|
2020-10-10 11:29:42 +11:00
|
|
|
// Set select mode to single when alt+tabing
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
function handleBlur() {
|
|
|
|
|
setSelectMode("single");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
window.addEventListener("blur", handleBlur);
|
|
|
|
|
return () => {
|
|
|
|
|
window.removeEventListener("blur", handleBlur);
|
|
|
|
|
};
|
|
|
|
|
}, []);
|
|
|
|
|
|
2020-05-19 16:21:01 +10:00
|
|
|
return (
|
2020-09-06 13:20:05 +10:00
|
|
|
<Modal
|
|
|
|
|
isOpen={isOpen}
|
2020-10-01 22:32:21 +10:00
|
|
|
onRequestClose={onRequestClose}
|
2020-09-06 16:24:23 +10:00
|
|
|
style={{ maxWidth: "542px", width: "calc(100% - 16px)" }}
|
2020-09-06 13:20:05 +10:00
|
|
|
>
|
2020-05-31 10:53:33 +10:00
|
|
|
<ImageDrop onDrop={handleImagesUpload} dropText="Drop token to upload">
|
2020-05-19 16:21:01 +10:00
|
|
|
<input
|
2020-05-31 10:53:33 +10:00
|
|
|
onChange={(event) => handleImagesUpload(event.target.files)}
|
2020-05-19 16:21:01 +10:00
|
|
|
type="file"
|
|
|
|
|
accept="image/*"
|
|
|
|
|
style={{ display: "none" }}
|
|
|
|
|
ref={fileInputRef}
|
2020-05-31 10:53:33 +10:00
|
|
|
multiple
|
2020-05-19 16:21:01 +10:00
|
|
|
/>
|
|
|
|
|
<Flex
|
|
|
|
|
sx={{
|
|
|
|
|
flexDirection: "column",
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Label pt={2} pb={1}>
|
|
|
|
|
Edit or import a token
|
|
|
|
|
</Label>
|
2020-05-19 19:03:36 +10:00
|
|
|
<TokenTiles
|
2020-10-01 22:32:21 +10:00
|
|
|
tokens={tokensByGroup}
|
|
|
|
|
groups={tokenGroups}
|
2020-05-19 19:03:36 +10:00
|
|
|
onTokenAdd={openImageDialog}
|
2020-10-01 22:32:21 +10:00
|
|
|
onTokenEdit={() => setIsEditModalOpen(true)}
|
2020-10-10 15:32:59 +11:00
|
|
|
onTokensRemove={() => setIsTokensRemoveModalOpen(true)}
|
2020-10-01 22:32:21 +10:00
|
|
|
selectedTokens={selectedTokens}
|
2020-05-19 19:03:36 +10:00
|
|
|
onTokenSelect={handleTokenSelect}
|
2020-10-01 22:32:21 +10:00
|
|
|
selectMode={selectMode}
|
|
|
|
|
onSelectModeChange={setSelectMode}
|
|
|
|
|
search={search}
|
|
|
|
|
onSearchChange={handleSearchChange}
|
|
|
|
|
onTokensGroup={() => setIsGroupModalOpen(true)}
|
2020-10-09 16:18:52 +11:00
|
|
|
onTokensHide={handleTokensHide}
|
2020-05-20 12:37:29 +10:00
|
|
|
/>
|
|
|
|
|
<Button
|
|
|
|
|
variant="primary"
|
|
|
|
|
disabled={imageLoading}
|
2020-10-01 22:32:21 +10:00
|
|
|
onClick={onRequestClose}
|
2020-05-20 12:37:29 +10:00
|
|
|
>
|
|
|
|
|
Done
|
|
|
|
|
</Button>
|
2020-05-19 16:21:01 +10:00
|
|
|
</Flex>
|
|
|
|
|
</ImageDrop>
|
2020-11-26 16:29:10 +11:00
|
|
|
{tokensLoading && <LoadingOverlay bg="overlay" />}
|
2020-10-01 22:32:21 +10:00
|
|
|
<EditTokenModal
|
|
|
|
|
isOpen={isEditModalOpen}
|
|
|
|
|
onDone={() => setIsEditModalOpen(false)}
|
|
|
|
|
token={selectedTokens.length === 1 && selectedTokens[0]}
|
|
|
|
|
/>
|
|
|
|
|
<EditGroupModal
|
|
|
|
|
isOpen={isGroupModalOpen}
|
|
|
|
|
onChange={handleTokensGroup}
|
|
|
|
|
groups={tokenGroups.filter(
|
|
|
|
|
(group) => group !== "" && group !== "default"
|
|
|
|
|
)}
|
|
|
|
|
onRequestClose={() => setIsGroupModalOpen(false)}
|
|
|
|
|
// Select the default group by testing whether all selected tokens are the same
|
|
|
|
|
defaultGroup={
|
|
|
|
|
selectedTokens.length > 0 &&
|
|
|
|
|
selectedTokens
|
|
|
|
|
.map((map) => map.group)
|
|
|
|
|
.reduce((prev, curr) => (prev === curr ? curr : undefined))
|
|
|
|
|
}
|
|
|
|
|
/>
|
2020-10-10 15:32:59 +11:00
|
|
|
<ConfirmModal
|
|
|
|
|
isOpen={isTokensRemoveModalOpen}
|
|
|
|
|
onRequestClose={() => setIsTokensRemoveModalOpen(false)}
|
|
|
|
|
onConfirm={handleTokensRemove}
|
|
|
|
|
confirmText="Remove"
|
|
|
|
|
label={`Remove ${selectedTokenIds.length} Token${
|
|
|
|
|
selectedTokenIds.length > 1 ? "s" : ""
|
|
|
|
|
}`}
|
|
|
|
|
description="This operation cannot be undone."
|
|
|
|
|
/>
|
2020-05-19 16:21:01 +10:00
|
|
|
</Modal>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default SelectTokensModal;
|