Added custom token loading and replication

This commit is contained in:
Mitchell McCaffrey 2020-05-19 19:03:36 +10:00
parent dbc3cd83e7
commit 0f5f90faa6
6 changed files with 287 additions and 58 deletions

View File

@ -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>
);
}

View File

@ -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>

View File

@ -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"

View File

@ -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}>

View File

@ -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>

View File

@ -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)}>