Added custom token loading and replication
This commit is contained in:
parent
dbc3cd83e7
commit
0f5f90faa6
@ -1,15 +1,18 @@
|
||||
import React from "react";
|
||||
import { Flex, Image, Text } from "theme-ui";
|
||||
import { Flex, Image, Text, Box, IconButton } from "theme-ui";
|
||||
|
||||
import RemoveMapIcon from "../../icons/RemoveMapIcon";
|
||||
|
||||
import useDataSource from "../../helpers/useDataSource";
|
||||
|
||||
import { tokenSources as defaultTokenSources } from "../../tokens";
|
||||
|
||||
function TokenTile({ token, isSelected }) {
|
||||
function TokenTile({ token, isSelected, onTokenSelect, onTokenRemove }) {
|
||||
const tokenSource = useDataSource(token, defaultTokenSources);
|
||||
const isDefault = token.type === "default";
|
||||
|
||||
return (
|
||||
<Flex
|
||||
onClick={() => onTokenSelect(token)}
|
||||
sx={{
|
||||
borderColor: "primary",
|
||||
borderStyle: isSelected ? "solid" : "none",
|
||||
@ -52,6 +55,22 @@ function TokenTile({ token, isSelected }) {
|
||||
{token.name}
|
||||
</Text>
|
||||
</Flex>
|
||||
{isSelected && !isDefault && (
|
||||
<Box sx={{ position: "absolute", top: 0, right: 0 }}>
|
||||
<IconButton
|
||||
aria-label="Remove Map"
|
||||
title="Remove Map"
|
||||
onClick={() => {
|
||||
onTokenRemove(token.id);
|
||||
}}
|
||||
bg="overlay"
|
||||
sx={{ borderRadius: "50%" }}
|
||||
m={1}
|
||||
>
|
||||
<RemoveMapIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
@ -6,7 +6,13 @@ import AddIcon from "../../icons/AddIcon";
|
||||
|
||||
import TokenTile from "./TokenTile";
|
||||
|
||||
function TokenTiles({ tokens, onTokenAdd }) {
|
||||
function TokenTiles({
|
||||
tokens,
|
||||
onTokenAdd,
|
||||
onTokenSelect,
|
||||
selectedToken,
|
||||
onTokenRemove,
|
||||
}) {
|
||||
return (
|
||||
<SimpleBar style={{ maxHeight: "300px", width: "500px" }}>
|
||||
<Flex
|
||||
@ -45,7 +51,13 @@ function TokenTiles({ tokens, onTokenAdd }) {
|
||||
<AddIcon large />
|
||||
</Flex>
|
||||
{tokens.map((token) => (
|
||||
<TokenTile key={token.id} token={token} />
|
||||
<TokenTile
|
||||
key={token.id}
|
||||
token={token}
|
||||
isSelected={selectedToken && token.id === selectedToken.id}
|
||||
onTokenSelect={onTokenSelect}
|
||||
onTokenRemove={onTokenRemove}
|
||||
/>
|
||||
))}
|
||||
</Flex>
|
||||
</SimpleBar>
|
||||
|
@ -18,7 +18,7 @@ const listTokenClassName = "list-token";
|
||||
|
||||
function Tokens({ onMapTokenStateCreate }) {
|
||||
const { userId } = useContext(AuthContext);
|
||||
const { tokens } = useContext(TokenDataContext);
|
||||
const { ownedTokens, tokens } = useContext(TokenDataContext);
|
||||
|
||||
const [tokenSize, setTokenSize] = useState(1);
|
||||
|
||||
@ -28,6 +28,7 @@ function Tokens({ onMapTokenStateCreate }) {
|
||||
onMapTokenStateCreate({
|
||||
id: shortid.generate(),
|
||||
tokenId: token.id,
|
||||
tokenType: token.type,
|
||||
owner: userId,
|
||||
size: tokenSize,
|
||||
label: "",
|
||||
@ -49,13 +50,15 @@ function Tokens({ onMapTokenStateCreate }) {
|
||||
}}
|
||||
>
|
||||
<SimpleBar style={{ height: "calc(100% - 48px)", overflowX: "hidden" }}>
|
||||
{tokens.map((token) => (
|
||||
<ListToken
|
||||
key={token.id}
|
||||
token={token}
|
||||
className={listTokenClassName}
|
||||
/>
|
||||
))}
|
||||
{ownedTokens
|
||||
.filter((token) => token.owner === userId)
|
||||
.map((token) => (
|
||||
<ListToken
|
||||
key={token.id}
|
||||
token={token}
|
||||
className={listTokenClassName}
|
||||
/>
|
||||
))}
|
||||
</SimpleBar>
|
||||
<Flex
|
||||
bg="muted"
|
||||
|
@ -14,21 +14,77 @@ export function TokenDataProvider({ children }) {
|
||||
const [tokens, setTokens] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!userId) {
|
||||
if (!userId || !database) {
|
||||
return;
|
||||
}
|
||||
const defaultTokensWithIds = [];
|
||||
for (let defaultToken of defaultTokens) {
|
||||
defaultTokensWithIds.push({
|
||||
...defaultToken,
|
||||
id: `__default-${defaultToken.key}`,
|
||||
owner: userId,
|
||||
});
|
||||
function getDefaultTokes() {
|
||||
const defaultTokensWithIds = [];
|
||||
for (let defaultToken of defaultTokens) {
|
||||
defaultTokensWithIds.push({
|
||||
...defaultToken,
|
||||
id: `__default-${defaultToken.key}`,
|
||||
owner: userId,
|
||||
});
|
||||
}
|
||||
return defaultTokensWithIds;
|
||||
}
|
||||
setTokens(defaultTokensWithIds);
|
||||
}, [userId]);
|
||||
|
||||
const value = { tokens };
|
||||
async function loadTokens() {
|
||||
let storedTokens = await database.table("tokens").toArray();
|
||||
const sortedTokens = storedTokens.sort((a, b) => b.created - a.created);
|
||||
const defaultTokensWithIds = getDefaultTokes();
|
||||
const allTokens = [...sortedTokens, ...defaultTokensWithIds];
|
||||
setTokens(allTokens);
|
||||
}
|
||||
|
||||
loadTokens();
|
||||
}, [userId, database]);
|
||||
|
||||
async function addToken(token) {
|
||||
await database.table("tokens").add(token);
|
||||
setTokens((prevTokens) => [token, ...prevTokens]);
|
||||
}
|
||||
|
||||
async function removeToken(id) {
|
||||
// TODO when removing token also remove it from all states that reference it and replicate
|
||||
await database.table("tokens").delete(id);
|
||||
setTokens((prevTokens) => {
|
||||
const filtered = prevTokens.filter((token) => token.id !== id);
|
||||
return filtered;
|
||||
});
|
||||
}
|
||||
|
||||
async function updateToken(id, update) {
|
||||
const change = { ...update, lastModified: Date.now() };
|
||||
await database.table("tokens").update(id, change);
|
||||
setTokens((prevTokens) => {
|
||||
const newTokens = [...prevTokens];
|
||||
const i = newTokens.findIndex((token) => token.id === id);
|
||||
if (i > -1) {
|
||||
newTokens[i] = { ...newTokens[i], ...change };
|
||||
}
|
||||
return newTokens;
|
||||
});
|
||||
}
|
||||
|
||||
async function putToken(token) {
|
||||
if (tokens.includes((t) => t.id === token.id)) {
|
||||
await updateToken(token.id, token);
|
||||
} else {
|
||||
await addToken(token);
|
||||
}
|
||||
}
|
||||
|
||||
const ownedTokens = tokens.filter((token) => token.owner === userId);
|
||||
|
||||
const value = {
|
||||
tokens,
|
||||
ownedTokens,
|
||||
addToken,
|
||||
removeToken,
|
||||
updateToken,
|
||||
putToken,
|
||||
};
|
||||
|
||||
return (
|
||||
<TokenDataContext.Provider value={value}>
|
||||
|
@ -1,24 +1,82 @@
|
||||
import React, { useRef, useContext } from "react";
|
||||
import React, { useRef, useContext, useState } from "react";
|
||||
import { Flex, Label } from "theme-ui";
|
||||
import shortid from "shortid";
|
||||
|
||||
import Modal from "../components/Modal";
|
||||
import ImageDrop from "../components/ImageDrop";
|
||||
|
||||
import TokenTiles from "../components/token/TokenTiles";
|
||||
|
||||
import blobToBuffer from "../helpers/blobToBuffer";
|
||||
|
||||
import TokenDataContext from "../contexts/TokenDataContext";
|
||||
import AuthContext from "../contexts/AuthContext";
|
||||
|
||||
function SelectTokensModal({ isOpen, onRequestClose }) {
|
||||
const { tokens } = useContext(TokenDataContext);
|
||||
const { userId } = useContext(AuthContext);
|
||||
const { ownedTokens, addToken, removeToken } = useContext(TokenDataContext);
|
||||
const fileInputRef = useRef();
|
||||
|
||||
const [imageLoading, setImageLoading] = useState(false);
|
||||
|
||||
const [selectedTokenId, setSelectedTokenId] = useState(null);
|
||||
const selectedToken = ownedTokens.find(
|
||||
(token) => token.id === selectedTokenId
|
||||
);
|
||||
|
||||
function openImageDialog() {
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.click();
|
||||
}
|
||||
}
|
||||
|
||||
function handleImageUpload(image) {}
|
||||
function handleTokenAdd(token) {
|
||||
addToken(token);
|
||||
}
|
||||
|
||||
function handleImageUpload(file) {
|
||||
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();
|
||||
}
|
||||
let image = new Image();
|
||||
setImageLoading(true);
|
||||
blobToBuffer(file).then((buffer) => {
|
||||
// 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);
|
||||
image.onload = function () {
|
||||
handleTokenAdd({
|
||||
file: buffer,
|
||||
name,
|
||||
type: "file",
|
||||
id: shortid.generate(),
|
||||
created: Date.now(),
|
||||
lastModified: Date.now(),
|
||||
owner: userId,
|
||||
});
|
||||
};
|
||||
image.src = url;
|
||||
|
||||
// Set file input to null to allow adding the same image 2 times in a row
|
||||
fileInputRef.current.value = null;
|
||||
});
|
||||
}
|
||||
|
||||
function handleTokenSelect(token) {
|
||||
setSelectedTokenId(token.id);
|
||||
}
|
||||
|
||||
async function handleTokenRemove(id) {
|
||||
await removeToken(id);
|
||||
setSelectedTokenId(null);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onRequestClose={onRequestClose}>
|
||||
@ -38,7 +96,13 @@ function SelectTokensModal({ isOpen, onRequestClose }) {
|
||||
<Label pt={2} pb={1}>
|
||||
Edit or import a token
|
||||
</Label>
|
||||
<TokenTiles tokens={tokens} onTokenAdd={openImageDialog} />
|
||||
<TokenTiles
|
||||
tokens={ownedTokens}
|
||||
onTokenAdd={openImageDialog}
|
||||
selectedToken={selectedToken}
|
||||
onTokenSelect={handleTokenSelect}
|
||||
onTokenRemove={handleTokenRemove}
|
||||
/>
|
||||
</Flex>
|
||||
</ImageDrop>
|
||||
</Modal>
|
||||
|
@ -17,6 +17,7 @@ import AuthModal from "../modals/AuthModal";
|
||||
|
||||
import AuthContext from "../contexts/AuthContext";
|
||||
import DatabaseContext from "../contexts/DatabaseContext";
|
||||
import TokenDataContext from "../contexts/TokenDataContext";
|
||||
|
||||
function Game() {
|
||||
const { database } = useContext(DatabaseContext);
|
||||
@ -93,6 +94,7 @@ function Game() {
|
||||
peer.connection.send({ id: "map", data: null });
|
||||
peer.connection.send({ id: "mapState", data: newMapState });
|
||||
sendMapDataToPeer(peer, newMap);
|
||||
sendTokensToPeer(peer, newMapState);
|
||||
}
|
||||
}
|
||||
|
||||
@ -114,34 +116,6 @@ function Game() {
|
||||
}
|
||||
}
|
||||
|
||||
function handleMapTokenStateChange(token) {
|
||||
if (mapState === null) {
|
||||
return;
|
||||
}
|
||||
setMapState((prevMapState) => ({
|
||||
...prevMapState,
|
||||
tokens: {
|
||||
...prevMapState.tokens,
|
||||
[token.id]: token,
|
||||
},
|
||||
}));
|
||||
for (let peer of Object.values(peers)) {
|
||||
const data = { [token.id]: token };
|
||||
peer.connection.send({ id: "tokenStateEdit", data });
|
||||
}
|
||||
}
|
||||
|
||||
function handleMapTokenStateRemove(token) {
|
||||
setMapState((prevMapState) => {
|
||||
const { [token.id]: old, ...rest } = prevMapState.tokens;
|
||||
return { ...prevMapState, tokens: rest };
|
||||
});
|
||||
for (let peer of Object.values(peers)) {
|
||||
const data = { [token.id]: token };
|
||||
peer.connection.send({ id: "tokenStateRemove", data });
|
||||
}
|
||||
}
|
||||
|
||||
function addMapDrawActions(actions, indexKey, actionsKey) {
|
||||
setMapState((prevMapState) => {
|
||||
const newActions = [
|
||||
@ -228,6 +202,50 @@ function Game() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Token state
|
||||
*/
|
||||
|
||||
async function handleMapTokenStateCreate(tokenState) {
|
||||
// If file type token send the token to the other peers
|
||||
if (tokenState.tokenType === "file") {
|
||||
const token = await database.table("tokens").get(tokenState.tokenId);
|
||||
const { file, ...rest } = token;
|
||||
for (let peer of Object.values(peers)) {
|
||||
peer.connection.send({ id: "token", data: rest });
|
||||
}
|
||||
}
|
||||
handleMapTokenStateChange(tokenState);
|
||||
}
|
||||
|
||||
function handleMapTokenStateChange(tokenState) {
|
||||
if (mapState === null) {
|
||||
return;
|
||||
}
|
||||
setMapState((prevMapState) => ({
|
||||
...prevMapState,
|
||||
tokens: {
|
||||
...prevMapState.tokens,
|
||||
[tokenState.id]: tokenState,
|
||||
},
|
||||
}));
|
||||
for (let peer of Object.values(peers)) {
|
||||
const data = { [tokenState.id]: tokenState };
|
||||
peer.connection.send({ id: "tokenStateEdit", data });
|
||||
}
|
||||
}
|
||||
|
||||
function handleMapTokenStateRemove(tokenState) {
|
||||
setMapState((prevMapState) => {
|
||||
const { [tokenState.id]: old, ...rest } = prevMapState.tokens;
|
||||
return { ...prevMapState, tokens: rest };
|
||||
});
|
||||
for (let peer of Object.values(peers)) {
|
||||
const data = { [tokenState.id]: tokenState };
|
||||
peer.connection.send({ id: "tokenStateRemove", data });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Party state
|
||||
*/
|
||||
@ -255,10 +273,32 @@ function Game() {
|
||||
* Peer handlers
|
||||
*/
|
||||
|
||||
const { putToken } = useContext(TokenDataContext);
|
||||
|
||||
function sendTokensToPeer(peer, state) {
|
||||
let sentTokens = {};
|
||||
for (let tokenState of Object.values(state.tokens)) {
|
||||
if (
|
||||
tokenState.tokenType === "file" &&
|
||||
!(tokenState.tokenId in sentTokens)
|
||||
) {
|
||||
sentTokens[tokenState.tokenId] = true;
|
||||
database
|
||||
.table("tokens")
|
||||
.get(tokenState.tokenId)
|
||||
.then((token) => {
|
||||
const { file, ...rest } = token;
|
||||
peer.connection.send({ id: "token", data: rest });
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handlePeerData({ data, peer }) {
|
||||
if (data.id === "sync") {
|
||||
if (mapState) {
|
||||
peer.connection.send({ id: "mapState", data: mapState });
|
||||
sendTokensToPeer(peer, mapState);
|
||||
}
|
||||
if (map) {
|
||||
sendMapDataToPeer(peer, map);
|
||||
@ -306,6 +346,41 @@ function Game() {
|
||||
if (data.id === "mapState") {
|
||||
setMapState(data.data);
|
||||
}
|
||||
if (data.id === "token") {
|
||||
const newToken = data.data;
|
||||
if (newToken && newToken.type === "file") {
|
||||
database
|
||||
.table("tokens")
|
||||
.get(newToken.id)
|
||||
.then((cachedToken) => {
|
||||
if (
|
||||
!cachedToken ||
|
||||
cachedToken.lastModified !== newToken.lastModified
|
||||
) {
|
||||
setMapLoading(true);
|
||||
peer.connection.send({
|
||||
id: "tokenRequest",
|
||||
data: newToken.id,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
if (data.id === "tokenRequest") {
|
||||
database
|
||||
.table("tokens")
|
||||
.get(data.data)
|
||||
.then((token) => {
|
||||
peer.connection.send({ id: "tokenResponse", data: token });
|
||||
});
|
||||
}
|
||||
if (data.id === "tokenResponse") {
|
||||
setMapLoading(false);
|
||||
const newToken = data.data;
|
||||
if (newToken && newToken.type === "file") {
|
||||
putToken(newToken);
|
||||
}
|
||||
}
|
||||
if (data.id === "tokenStateEdit") {
|
||||
setMapState((prevMapState) => ({
|
||||
...prevMapState,
|
||||
@ -474,7 +549,7 @@ function Game() {
|
||||
allowFogDrawing={canEditFogDrawing}
|
||||
disabledTokens={disabledMapTokens}
|
||||
/>
|
||||
<Tokens onMapTokenStateCreate={handleMapTokenStateChange} />
|
||||
<Tokens onMapTokenStateCreate={handleMapTokenStateCreate} />
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Banner isOpen={!!peerError} onRequestClose={() => setPeerError(null)}>
|
||||
|
Loading…
Reference in New Issue
Block a user