2021-03-19 13:29:07 +11:00
|
|
|
import React, { useRef, useState } 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";
|
2021-04-15 16:17:12 +10:00
|
|
|
import { useToasts } from "react-toast-notifications";
|
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 { useSearch, useGroup, handleItemSelect } from "../helpers/select";
|
2021-02-08 16:53:56 +11:00
|
|
|
import { createThumbnail } from "../helpers/image";
|
2021-02-04 15:06:34 +11:00
|
|
|
|
|
|
|
|
import useResponsiveLayout from "../hooks/useResponsiveLayout";
|
2020-05-19 19:03:36 +10:00
|
|
|
|
2021-02-06 13:32:38 +11:00
|
|
|
import { useTokenData } from "../contexts/TokenDataContext";
|
|
|
|
|
import { useAuth } from "../contexts/AuthContext";
|
2021-03-19 13:29:07 +11:00
|
|
|
import { useKeyboard, useBlur } from "../contexts/KeyboardContext";
|
2020-05-19 16:21:01 +10:00
|
|
|
|
2021-03-25 16:31:06 +11:00
|
|
|
import shortcuts from "../shortcuts";
|
|
|
|
|
|
2020-05-19 16:21:01 +10:00
|
|
|
function SelectTokensModal({ isOpen, onRequestClose }) {
|
2021-04-15 16:17:12 +10:00
|
|
|
const { addToast } = useToasts();
|
|
|
|
|
|
2021-02-06 13:32:38 +11:00
|
|
|
const { userId } = useAuth();
|
2020-11-26 16:29:10 +11:00
|
|
|
const {
|
|
|
|
|
ownedTokens,
|
|
|
|
|
addToken,
|
|
|
|
|
removeTokens,
|
|
|
|
|
updateTokens,
|
|
|
|
|
tokensLoading,
|
2021-02-06 13:32:38 +11:00
|
|
|
} = useTokenData();
|
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) {
|
2021-02-08 16:53:56 +11:00
|
|
|
setIsLoading(true);
|
2020-10-01 22:32:21 +10:00
|
|
|
setIsGroupModalOpen(false);
|
|
|
|
|
await updateTokens(selectedTokenIds, { group });
|
2021-02-08 16:53:56 +11:00
|
|
|
setIsLoading(false);
|
2020-10-01 22:32:21 +10:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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();
|
2021-02-08 16:53:56 +11:00
|
|
|
const [isLoading, setIsLoading] = useState(false);
|
2020-10-01 22:32:21 +10:00
|
|
|
|
2021-04-15 16:17:12 +10:00
|
|
|
const [isLargeImageWarningModalOpen, setShowLargeImageWarning] = useState(
|
|
|
|
|
false
|
|
|
|
|
);
|
|
|
|
|
const largeImageWarningFiles = useRef();
|
|
|
|
|
|
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) {
|
2020-11-27 21:00:08 +11:00
|
|
|
if (navigator.storage) {
|
|
|
|
|
// Attempt to enable persistant storage
|
|
|
|
|
await navigator.storage.persist();
|
|
|
|
|
}
|
|
|
|
|
|
2021-04-15 16:17:12 +10:00
|
|
|
let tokenFiles = [];
|
|
|
|
|
for (let file of files) {
|
|
|
|
|
if (file.size > 5e7) {
|
|
|
|
|
addToast(`Unable to import token ${file.name} as it is over 50MB`);
|
|
|
|
|
} else {
|
|
|
|
|
tokenFiles.push(file);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Any file greater than 20MB
|
|
|
|
|
if (tokenFiles.some((file) => file.size > 2e7)) {
|
|
|
|
|
largeImageWarningFiles.current = tokenFiles;
|
|
|
|
|
setShowLargeImageWarning(true);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (let file of tokenFiles) {
|
|
|
|
|
await handleImageUpload(file);
|
|
|
|
|
}
|
2021-04-15 16:28:39 +10:00
|
|
|
|
|
|
|
|
clearFileInput();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function clearFileInput() {
|
|
|
|
|
// Set file input to null to allow adding the same image 2 times in a row
|
|
|
|
|
if (fileInputRef.current) {
|
|
|
|
|
fileInputRef.current.value = null;
|
|
|
|
|
}
|
2021-04-15 16:17:12 +10:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleLargeImageWarningCancel() {
|
|
|
|
|
largeImageWarningFiles.current = undefined;
|
|
|
|
|
setShowLargeImageWarning(false);
|
2021-04-15 16:28:39 +10:00
|
|
|
clearFileInput();
|
2021-04-15 16:17:12 +10:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function handleLargeImageWarningConfirm() {
|
|
|
|
|
setShowLargeImageWarning(false);
|
|
|
|
|
const files = largeImageWarningFiles.current;
|
|
|
|
|
for (let file of files) {
|
|
|
|
|
await handleImageUpload(file);
|
|
|
|
|
}
|
|
|
|
|
largeImageWarningFiles.current = undefined;
|
2021-04-15 16:28:39 +10:00
|
|
|
clearFileInput();
|
2020-05-31 10:53:33 +10:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function handleImageUpload(file) {
|
2020-12-11 16:36:27 +11:00
|
|
|
let name = "Unknown Token";
|
2020-05-19 19:03:36 +10:00
|
|
|
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();
|
2021-02-08 16:53:56 +11:00
|
|
|
setIsLoading(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) => {
|
2021-02-08 16:53:56 +11:00
|
|
|
image.onload = async function () {
|
|
|
|
|
const thumbnail = await createThumbnail(image, file.type);
|
|
|
|
|
|
2020-05-19 19:03:36 +10:00
|
|
|
handleTokenAdd({
|
|
|
|
|
file: buffer,
|
2021-02-08 16:53:56 +11:00
|
|
|
thumbnail,
|
2020-05-19 19:03:36 +10:00
|
|
|
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
|
|
|
});
|
2021-02-08 16:53:56 +11:00
|
|
|
setIsLoading(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() {
|
2021-02-08 17:06:54 +11:00
|
|
|
setIsLoading(true);
|
2020-10-10 15:32:59 +11:00
|
|
|
setIsTokensRemoveModalOpen(false);
|
2020-10-01 22:32:21 +10:00
|
|
|
await removeTokens(selectedTokenIds);
|
|
|
|
|
setSelectedTokenIds([]);
|
2021-02-08 17:06:54 +11:00
|
|
|
setIsLoading(false);
|
2020-06-28 15:43:45 +10:00
|
|
|
}
|
|
|
|
|
|
2020-10-09 16:18:52 +11:00
|
|
|
async function handleTokensHide(hideInSidebar) {
|
2021-02-08 17:06:54 +11:00
|
|
|
setIsLoading(true);
|
2020-10-09 16:18:52 +11:00
|
|
|
await updateTokens(selectedTokenIds, { hideInSidebar });
|
2021-02-08 17:06:54 +11:00
|
|
|
setIsLoading(false);
|
2020-10-09 16:18:52 +11:00
|
|
|
}
|
|
|
|
|
|
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
|
|
|
|
|
*/
|
2021-03-25 16:31:06 +11:00
|
|
|
function handleKeyDown(event) {
|
2020-10-01 22:32:21 +10:00
|
|
|
if (!isOpen) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2021-03-25 16:31:06 +11:00
|
|
|
if (shortcuts.selectRange(event)) {
|
2020-10-01 22:32:21 +10:00
|
|
|
setSelectMode("range");
|
|
|
|
|
}
|
2021-03-25 16:31:06 +11:00
|
|
|
if (shortcuts.selectMultiple(event)) {
|
2020-10-01 22:32:21 +10:00
|
|
|
setSelectMode("multiple");
|
2020-06-28 15:43:45 +10:00
|
|
|
}
|
2021-03-25 16:31:06 +11:00
|
|
|
if (shortcuts.delete(event)) {
|
2020-10-10 15:44:07 +11:00
|
|
|
// Selected tokens and none are default
|
|
|
|
|
if (
|
|
|
|
|
selectedTokenIds.length > 0 &&
|
|
|
|
|
!selectedTokens.some((token) => token.type === "default")
|
|
|
|
|
) {
|
2021-01-25 09:00:19 +11:00
|
|
|
// Ensure all other modals are closed
|
|
|
|
|
setIsEditModalOpen(false);
|
|
|
|
|
setIsGroupModalOpen(false);
|
2020-10-10 15:44:07 +11:00
|
|
|
setIsTokensRemoveModalOpen(true);
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-05-20 12:37:29 +10:00
|
|
|
}
|
|
|
|
|
|
2021-03-25 16:31:06 +11:00
|
|
|
function handleKeyUp(event) {
|
2020-10-01 22:32:21 +10:00
|
|
|
if (!isOpen) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2021-03-25 16:31:06 +11:00
|
|
|
if (shortcuts.selectRange(event) && selectMode === "range") {
|
2020-10-01 22:32:21 +10:00
|
|
|
setSelectMode("single");
|
|
|
|
|
}
|
2021-03-25 16:31:06 +11:00
|
|
|
if (shortcuts.selectMultiple(event) && selectMode === "multiple") {
|
2020-10-01 22:32:21 +10:00
|
|
|
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
|
|
|
|
2021-03-19 13:29:07 +11:00
|
|
|
// Set select mode to single when cmd+tabing
|
|
|
|
|
function handleBlur() {
|
|
|
|
|
setSelectMode("single");
|
|
|
|
|
}
|
2020-10-10 11:29:42 +11:00
|
|
|
|
2021-03-19 13:29:07 +11:00
|
|
|
useBlur(handleBlur);
|
2020-10-10 11:29:42 +11:00
|
|
|
|
2021-01-03 14:53:06 +11:00
|
|
|
const layout = useResponsiveLayout();
|
|
|
|
|
|
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}
|
2021-01-03 14:53:06 +11:00
|
|
|
style={{ maxWidth: layout.modalSize, 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"
|
2021-02-08 16:53:56 +11:00
|
|
|
disabled={isLoading}
|
2020-10-01 22:32:21 +10:00
|
|
|
onClick={onRequestClose}
|
2021-04-11 15:19:31 +10:00
|
|
|
mt={2}
|
2020-05-20 12:37:29 +10:00
|
|
|
>
|
|
|
|
|
Done
|
|
|
|
|
</Button>
|
2020-05-19 16:21:01 +10:00
|
|
|
</Flex>
|
|
|
|
|
</ImageDrop>
|
2021-02-08 16:53:56 +11:00
|
|
|
{(isLoading || tokensLoading) && <LoadingOverlay bg="overlay" />}
|
2020-10-01 22:32:21 +10:00
|
|
|
<EditTokenModal
|
|
|
|
|
isOpen={isEditModalOpen}
|
|
|
|
|
onDone={() => setIsEditModalOpen(false)}
|
2021-02-08 16:53:56 +11:00
|
|
|
tokenId={selectedTokens.length === 1 && selectedTokens[0].id}
|
2020-10-01 22:32:21 +10:00
|
|
|
/>
|
|
|
|
|
<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."
|
|
|
|
|
/>
|
2021-04-15 16:17:12 +10:00
|
|
|
<ConfirmModal
|
|
|
|
|
isOpen={isLargeImageWarningModalOpen}
|
|
|
|
|
onRequestClose={handleLargeImageWarningCancel}
|
|
|
|
|
onConfirm={handleLargeImageWarningConfirm}
|
|
|
|
|
confirmText="Continue"
|
|
|
|
|
label="Warning"
|
|
|
|
|
description="An imported image is larger than 20MB, this may cause slowness. Continue?"
|
|
|
|
|
/>
|
2020-05-19 16:21:01 +10:00
|
|
|
</Modal>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default SelectTokensModal;
|