diff --git a/src/components/token/TokenTile.js b/src/components/token/TokenTile.js
index 1eb9e75..78df16b 100644
--- a/src/components/token/TokenTile.js
+++ b/src/components/token/TokenTile.js
@@ -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 (
onTokenSelect(token)}
sx={{
borderColor: "primary",
borderStyle: isSelected ? "solid" : "none",
@@ -52,6 +55,22 @@ function TokenTile({ token, isSelected }) {
{token.name}
+ {isSelected && !isDefault && (
+
+ {
+ onTokenRemove(token.id);
+ }}
+ bg="overlay"
+ sx={{ borderRadius: "50%" }}
+ m={1}
+ >
+
+
+
+ )}
);
}
diff --git a/src/components/token/TokenTiles.js b/src/components/token/TokenTiles.js
index e9f77f1..f9ae7e2 100644
--- a/src/components/token/TokenTiles.js
+++ b/src/components/token/TokenTiles.js
@@ -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 (
{tokens.map((token) => (
-
+
))}
diff --git a/src/components/token/Tokens.js b/src/components/token/Tokens.js
index 20405f8..3eb3ad1 100644
--- a/src/components/token/Tokens.js
+++ b/src/components/token/Tokens.js
@@ -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 }) {
}}
>
- {tokens.map((token) => (
-
- ))}
+ {ownedTokens
+ .filter((token) => token.owner === userId)
+ .map((token) => (
+
+ ))}
{
- 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 (
diff --git a/src/modals/SelectTokensModal.js b/src/modals/SelectTokensModal.js
index 09a8133..189257b 100644
--- a/src/modals/SelectTokensModal.js
+++ b/src/modals/SelectTokensModal.js
@@ -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 (
@@ -38,7 +96,13 @@ function SelectTokensModal({ isOpen, onRequestClose }) {
-
+
diff --git a/src/routes/Game.js b/src/routes/Game.js
index 298e581..cb55b96 100644
--- a/src/routes/Game.js
+++ b/src/routes/Game.js
@@ -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}
/>
-
+
setPeerError(null)}>