Refactor image drop and to a hook and move global image drop

This commit is contained in:
Mitchell McCaffrey 2021-06-07 21:08:14 +10:00
parent 98f7744c1f
commit eb9afcc66a
8 changed files with 153 additions and 115 deletions

View File

@ -1,12 +1,9 @@
import React, { useState, useRef } from "react"; import React, { useState, useRef } from "react";
import { Box } from "theme-ui"; import { Flex, Text } from "theme-ui";
import { useToasts } from "react-toast-notifications"; import { useToasts } from "react-toast-notifications";
import ImageDrop from "./ImageDrop";
import LoadingOverlay from "../LoadingOverlay"; import LoadingOverlay from "../LoadingOverlay";
import ImageTypeModal from "../../modals/ImageTypeModal";
import ConfirmModal from "../../modals/ConfirmModal"; import ConfirmModal from "../../modals/ConfirmModal";
import { createMapFromFile } from "../../helpers/map"; import { createMapFromFile } from "../../helpers/map";
@ -17,7 +14,9 @@ import { useMapData } from "../../contexts/MapDataContext";
import { useTokenData } from "../../contexts/TokenDataContext"; import { useTokenData } from "../../contexts/TokenDataContext";
import { useAssets } from "../../contexts/AssetsContext"; import { useAssets } from "../../contexts/AssetsContext";
function GlobalImageDrop({ children }) { import useImageDrop from "../../hooks/useImageDrop";
function GlobalImageDrop({ children, onMapTokensStateCreate }) {
const { addToast } = useToasts(); const { addToast } = useToasts();
const { userId } = useAuth(); const { userId } = useAuth();
@ -25,20 +24,24 @@ function GlobalImageDrop({ children }) {
const { addToken } = useTokenData(); const { addToken } = useTokenData();
const { addAssets } = useAssets(); const { addAssets } = useAssets();
const [isImageTypeModalOpen, setIsImageTypeModalOpen] = useState(false);
const [isLargeImageWarningModalOpen, setShowLargeImageWarning] = useState( const [isLargeImageWarningModalOpen, setShowLargeImageWarning] = useState(
false false
); );
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const droppedImagesRef = useRef(); const droppedImagesRef = useRef();
const dropPositionRef = useRef();
// maps or tokens
const [droppingType, setDroppingType] = useState("maps");
async function handleDrop(files) { async function handleDrop(files, dropPosition) {
if (navigator.storage) { if (navigator.storage) {
// Attempt to enable persistant storage // Attempt to enable persistant storage
await navigator.storage.persist(); await navigator.storage.persist();
} }
dropPositionRef.current = dropPosition;
droppedImagesRef.current = []; droppedImagesRef.current = [];
for (let file of files) { for (let file of files) {
if (file.size > 5e7) { if (file.size > 5e7) {
@ -54,7 +57,11 @@ function GlobalImageDrop({ children }) {
return; return;
} }
setIsImageTypeModalOpen(true); if (droppingType === "maps") {
await handleMaps();
} else {
await handleTokens();
}
} }
function handleLargeImageWarningCancel() { function handleLargeImageWarningCancel() {
@ -64,11 +71,14 @@ function GlobalImageDrop({ children }) {
async function handleLargeImageWarningConfirm() { async function handleLargeImageWarningConfirm() {
setShowLargeImageWarning(false); setShowLargeImageWarning(false);
setIsImageTypeModalOpen(true); if (droppingType === "maps") {
await handleMaps();
} else {
await handleTokens();
}
} }
async function handleMaps() { async function handleMaps() {
setIsImageTypeModalOpen(false);
setIsLoading(true); setIsLoading(true);
for (let file of droppedImagesRef.current) { for (let file of droppedImagesRef.current) {
const { map, assets } = await createMapFromFile(file, userId); const { map, assets } = await createMapFromFile(file, userId);
@ -80,7 +90,6 @@ function GlobalImageDrop({ children }) {
} }
async function handleTokens() { async function handleTokens() {
setIsImageTypeModalOpen(false);
setIsLoading(true); setIsLoading(true);
for (let file of droppedImagesRef.current) { for (let file of droppedImagesRef.current) {
const { token, assets } = await createTokenFromFile(file, userId); const { token, assets } = await createTokenFromFile(file, userId);
@ -91,21 +100,66 @@ function GlobalImageDrop({ children }) {
droppedImagesRef.current = undefined; droppedImagesRef.current = undefined;
} }
function handleImageTypeClose() { function handleMapsOver() {
droppedImagesRef.current = undefined; setDroppingType("maps");
setIsImageTypeModalOpen(false);
} }
function handleTokensOver() {
setDroppingType("tokens");
}
const { dragging, containerListeners, overlayListeners } = useImageDrop(
handleDrop
);
return ( return (
<Box sx={{ height: "100%" }}> <Flex sx={{ height: "100%", flexGrow: 1 }} {...containerListeners}>
<ImageDrop onDrop={handleDrop}>{children}</ImageDrop> {children}
<ImageTypeModal {dragging && (
isOpen={isImageTypeModalOpen} <Flex
onMaps={handleMaps} sx={{
onTokens={handleTokens} position: "absolute",
onRequestClose={handleImageTypeClose} top: 0,
multiple={droppedImagesRef.current?.length > 1} right: 0,
/> left: 0,
bottom: 0,
cursor: "copy",
flexDirection: "column",
}}
{...overlayListeners}
>
<Flex
bg="overlay"
sx={{
width: "100%",
height: "20%",
justifyContent: "center",
alignItems: "center",
opacity: droppingType === "maps" ? 1 : 0.5,
}}
onDragEnter={handleMapsOver}
>
<Text sx={{ pointerEvents: "none" }}>
{"Drop image to import as a map"}
</Text>
</Flex>
<Flex
bg="overlay"
sx={{
width: "100%",
height: "80%",
justifyContent: "center",
alignItems: "center",
opacity: droppingType === "tokens" ? 1 : 0.5,
}}
onDragEnter={handleTokensOver}
>
<Text sx={{ pointerEvents: "none" }}>
{"Drop image to import as a token"}
</Text>
</Flex>
</Flex>
)}
<ConfirmModal <ConfirmModal
isOpen={isLargeImageWarningModalOpen} isOpen={isLargeImageWarningModalOpen}
onRequestClose={handleLargeImageWarningCancel} onRequestClose={handleLargeImageWarningCancel}
@ -115,7 +169,7 @@ function GlobalImageDrop({ children }) {
description="An imported image is larger than 20MB, this may cause slowness. Continue?" description="An imported image is larger than 20MB, this may cause slowness. Continue?"
/> />
{isLoading && <LoadingOverlay bg="overlay" />} {isLoading && <LoadingOverlay bg="overlay" />}
</Box> </Flex>
); );
} }

View File

@ -0,0 +1,37 @@
import React from "react";
import { Box, Flex, Text } from "theme-ui";
import useImageDrop from "../../hooks/useImageDrop";
function ImageDrop({ onDrop, dropText, children }) {
const { dragging, containerListeners, overlayListeners } = useImageDrop(
onDrop
);
return (
<Box {...containerListeners}>
{children}
{dragging && (
<Flex
bg="overlay"
sx={{
position: "absolute",
top: 0,
right: 0,
left: 0,
bottom: 0,
justifyContent: "center",
alignItems: "center",
cursor: "copy",
}}
{...overlayListeners}
>
<Text sx={{ pointerEvents: "none" }}>
{dropText || "Drop image to import"}
</Text>
</Flex>
)}
</Box>
);
}
export default ImageDrop;

View File

@ -1,26 +1,34 @@
import React, { useState } from "react"; import { useState } from "react";
import { Box, Flex, Text } from "theme-ui";
import { useToasts } from "react-toast-notifications"; import { useToasts } from "react-toast-notifications";
const supportFileTypes = ["image/jpeg", "image/gif", "image/png", "image/webp"]; import Vector2 from "../helpers/Vector2";
function ImageDrop({ onDrop, dropText, children }) { function useImageDrop(
onImageDrop,
supportFileTypes = ["image/jpeg", "image/gif", "image/png", "image/webp"]
) {
const { addToast } = useToasts(); const { addToast } = useToasts();
const [dragging, setDragging] = useState(false); const [dragging, setDragging] = useState(false);
function handleImageDragEnter(event) { function onDragEnter(event) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
setDragging(true); setDragging(true);
} }
function handleImageDragLeave(event) { function onDragLeave(event) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
setDragging(false); setDragging(false);
} }
async function handleImageDrop(event) { function onDragOver(event) {
event.preventDefault();
event.stopPropagation();
event.dataTransfer.dropEffect = "copy";
}
async function onDrop(event) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
let imageFiles = []; let imageFiles = [];
@ -63,41 +71,15 @@ function ImageDrop({ onDrop, dropText, children }) {
addToast(`Unsupported file type for ${file.name}`); addToast(`Unsupported file type for ${file.name}`);
} }
} }
onDrop(imageFiles); const dropPosition = new Vector2(event.clientX, event.clientY);
onImageDrop(imageFiles, dropPosition);
setDragging(false); setDragging(false);
} }
return ( const containerListeners = { onDragEnter };
<Box onDragEnter={handleImageDragEnter} sx={{ height: "100%" }}> const overlayListeners = { onDragLeave, onDragOver, onDrop };
{children}
{dragging && ( return { dragging, containerListeners, overlayListeners };
<Flex
bg="overlay"
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" }}>
{dropText || "Drop image to import"}
</Text>
</Flex>
)}
</Box>
);
} }
export default ImageDrop; export default useImageDrop;

View File

@ -1,34 +0,0 @@
import React from "react";
import { Box, Label, Flex, Button } from "theme-ui";
import Modal from "../components/Modal";
function ImageTypeModal({
isOpen,
onRequestClose,
multiple,
onTokens,
onMaps,
}) {
return (
<Modal
isOpen={isOpen}
onRequestClose={onRequestClose}
style={{ maxWidth: "300px" }}
>
<Box>
<Label py={2}>Import image{multiple ? "s" : ""} as</Label>
<Flex py={2}>
<Button sx={{ flexGrow: 1 }} m={1} ml={0} onClick={onTokens}>
Token{multiple ? "s" : ""}
</Button>
<Button sx={{ flexGrow: 1 }} m={1} mr={0} onClick={onMaps}>
Map{multiple ? "s" : ""}
</Button>
</Flex>
</Box>
</Modal>
);
}
export default ImageTypeModal;

View File

@ -9,7 +9,7 @@ import ConfirmModal from "./ConfirmModal";
import Modal from "../components/Modal"; import Modal from "../components/Modal";
import LoadingOverlay from "../components/LoadingOverlay"; import LoadingOverlay from "../components/LoadingOverlay";
import ImageDrop from "../components/file/ImageDrop"; import ImageDrop from "../components/image/ImageDrop";
import MapTiles from "../components/map/MapTiles"; import MapTiles from "../components/map/MapTiles";
import MapEditBar from "../components/map/MapEditBar"; import MapEditBar from "../components/map/MapEditBar";

View File

@ -9,7 +9,7 @@ import ConfirmModal from "./ConfirmModal";
import Modal from "../components/Modal"; import Modal from "../components/Modal";
import LoadingOverlay from "../components/LoadingOverlay"; import LoadingOverlay from "../components/LoadingOverlay";
import ImageDrop from "../components/file/ImageDrop"; import ImageDrop from "../components/image/ImageDrop";
import TokenTiles from "../components/token/TokenTiles"; import TokenTiles from "../components/token/TokenTiles";
import TokenEditBar from "../components/token/TokenEditBar"; import TokenEditBar from "../components/token/TokenEditBar";

View File

@ -20,6 +20,8 @@ import Session from "./Session";
import Map from "../components/map/Map"; import Map from "../components/map/Map";
import TokenBar from "../components/token/TokenBar"; import TokenBar from "../components/token/TokenBar";
import GlobalImageDrop from "../components/image/GlobalImageDrop";
const defaultMapActions = { const defaultMapActions = {
mapDrawActions: [], mapDrawActions: [],
mapDrawActionIndex: -1, mapDrawActionIndex: -1,
@ -457,7 +459,7 @@ function NetworkedMapAndTokens({ session }) {
} }
return ( return (
<> <GlobalImageDrop>
<Map <Map
map={currentMap} map={currentMap}
mapState={currentMapState} mapState={currentMapState}
@ -482,7 +484,7 @@ function NetworkedMapAndTokens({ session }) {
session={session} session={session}
/> />
<TokenBar onMapTokensStateCreate={handleMapTokensStateCreate} /> <TokenBar onMapTokensStateCreate={handleMapTokensStateCreate} />
</> </GlobalImageDrop>
); );
} }

View File

@ -8,7 +8,6 @@ import OfflineBanner from "../components/banner/OfflineBanner";
import LoadingOverlay from "../components/LoadingOverlay"; import LoadingOverlay from "../components/LoadingOverlay";
import Link from "../components/Link"; import Link from "../components/Link";
import MapLoadingOverlay from "../components/map/MapLoadingOverlay"; import MapLoadingOverlay from "../components/map/MapLoadingOverlay";
import GlobalImageDrop from "../components/file/GlobalImageDrop";
import AuthModal from "../modals/AuthModal"; import AuthModal from "../modals/AuthModal";
import GameExpiredModal from "../modals/GameExpiredModal"; import GameExpiredModal from "../modals/GameExpiredModal";
@ -115,18 +114,16 @@ function Game() {
<PlayerProvider session={session}> <PlayerProvider session={session}>
<PartyProvider session={session}> <PartyProvider session={session}>
<MapStageProvider value={mapStageRef}> <MapStageProvider value={mapStageRef}>
<GlobalImageDrop> <Flex
<Flex sx={{
sx={{ justifyContent: "space-between",
justifyContent: "space-between", flexGrow: 1,
flexGrow: 1, height: "100%",
height: "100%", }}
}} >
> <NetworkedParty session={session} gameId={gameId} />
<NetworkedParty session={session} gameId={gameId} /> <NetworkedMapAndTokens session={session} />
<NetworkedMapAndTokens session={session} /> </Flex>
</Flex>
</GlobalImageDrop>
<Banner <Banner
isOpen={!!peerError} isOpen={!!peerError}
onRequestClose={() => setPeerError(null)} onRequestClose={() => setPeerError(null)}