From 9f11161b235898434b33c719431c97b89e4cb64c Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Thu, 22 Apr 2021 16:53:35 +1000 Subject: [PATCH 001/176] Moved assets into new table in the database --- package.json | 1 + src/App.js | 72 +++---- src/components/map/MapEditor.js | 8 +- src/components/map/MapGrid.js | 9 +- src/components/map/MapInteraction.js | 9 +- src/components/map/MapTile.js | 6 +- src/components/map/MapToken.js | 4 +- src/components/token/ListToken.js | 6 +- src/components/token/TokenPreview.js | 10 +- src/components/token/TokenTile.js | 6 +- src/contexts/AssetsContext.js | 273 +++++++++++++++++++++++++++ src/contexts/ImageSourceContext.js | 157 --------------- src/database.js | 133 ++++++++++++- src/helpers/KonvaBridge.js | 113 +++++------ src/hooks/useMapImage.js | 16 +- src/workers/DatabaseWorker.js | 13 +- yarn.lock | 5 + 17 files changed, 544 insertions(+), 297 deletions(-) create mode 100644 src/contexts/AssetsContext.js delete mode 100644 src/contexts/ImageSourceContext.js diff --git a/package.json b/package.json index 5fa1a20..628ff9a 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "source-map-explorer": "^2.5.2", "theme-ui": "^0.3.1", "use-image": "^1.0.7", + "uuid": "^8.3.2", "webrtc-adapter": "^7.7.1" }, "resolutions": { diff --git a/src/App.js b/src/App.js index a77ae2b..7962f7b 100644 --- a/src/App.js +++ b/src/App.js @@ -18,7 +18,7 @@ import { TokenDataProvider } from "./contexts/TokenDataContext"; import { MapLoadingProvider } from "./contexts/MapLoadingContext"; import { SettingsProvider } from "./contexts/SettingsContext"; import { KeyboardProvider } from "./contexts/KeyboardContext"; -import { ImageSourcesProvider } from "./contexts/ImageSourceContext"; +import { AssetsProvider, AssetURLsProvider } from "./contexts/AssetsContext"; import { ToastProvider } from "./components/Toast"; @@ -30,40 +30,42 @@ function App() { - - - - - - - {/* Legacy support camel case routes */} - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + {/* Legacy support camel case routes */} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/map/MapEditor.js b/src/components/map/MapEditor.js index f8dbb8a..b042ab0 100644 --- a/src/components/map/MapEditor.js +++ b/src/components/map/MapEditor.js @@ -23,7 +23,7 @@ import MapGrid from "./MapGrid"; import MapGridEditor from "./MapGridEditor"; function MapEditor({ map, onSettingsChange }) { - const [mapImageSource] = useMapImage(map); + const [mapImage] = useMapImage(map); const [stageWidth, setStageWidth] = useState(1); const [stageHeight, setStageHeight] = useState(1); @@ -132,11 +132,7 @@ function MapEditor({ map, onSettingsChange }) { )} > - + {showGridControls && canEditGrid && ( <> diff --git a/src/components/map/MapGrid.js b/src/components/map/MapGrid.js index 5cfbf94..6b30859 100644 --- a/src/components/map/MapGrid.js +++ b/src/components/map/MapGrid.js @@ -1,7 +1,7 @@ import React, { useEffect, useState } from "react"; import useImage from "use-image"; -import { useImageSource } from "../../contexts/ImageSourceContext"; +import { useDataURL } from "../../contexts/AssetsContext"; import { mapSources as defaultMapSources } from "../../maps"; @@ -13,13 +13,14 @@ function MapGrid({ map }) { let mapSourceMap = map; // Use lowest resolution for grid lightness if (map && map.type === "file" && map.resolutions) { + // FIXME - move to resolutions array const resolutionArray = Object.keys(map.resolutions); if (resolutionArray.length > 0) { - mapSourceMap = map.resolutions[resolutionArray[0]]; + mapSourceMap.quality = resolutionArray[0]; } } - const mapSource = useImageSource(mapSourceMap, defaultMapSources); - const [mapImage, mapLoadingStatus] = useImage(mapSource); + const mapURL = useDataURL(mapSourceMap, defaultMapSources); + const [mapImage, mapLoadingStatus] = useImage(mapURL); const [isImageLight, setIsImageLight] = useState(true); diff --git a/src/components/map/MapInteraction.js b/src/components/map/MapInteraction.js index e1bb369..3d63a34 100644 --- a/src/components/map/MapInteraction.js +++ b/src/components/map/MapInteraction.js @@ -28,7 +28,7 @@ function MapInteraction({ onSelectedToolChange, disabledControls, }) { - const [mapImageSource, mapImageSourceStatus] = useMapImage(map); + const [mapImage, mapImageStatus] = useMapImage(map); // Map loaded taking in to account different resolutions const [mapLoaded, setMapLoaded] = useState(false); @@ -36,14 +36,15 @@ function MapInteraction({ if ( !map || !mapState || + // FIXME (map.type === "file" && !map.file && !map.resolutions) || mapState.mapId !== map.id ) { setMapLoaded(false); - } else if (mapImageSourceStatus === "loaded") { + } else if (mapImageStatus === "loaded") { setMapLoaded(true); } - }, [mapImageSourceStatus, map, mapState]); + }, [mapImageStatus, map, mapState]); const [stageWidth, setStageWidth] = useState(1); const [stageHeight, setStageHeight] = useState(1); @@ -211,7 +212,7 @@ function MapInteraction({ > onMapSelect(map)} diff --git a/src/components/map/MapToken.js b/src/components/map/MapToken.js index 1f7258f..e866c89 100644 --- a/src/components/map/MapToken.js +++ b/src/components/map/MapToken.js @@ -16,7 +16,7 @@ import { useDebouncedStageScale, } from "../../contexts/MapInteractionContext"; import { useGridCellPixelSize } from "../../contexts/GridContext"; -import { useImageSource } from "../../contexts/ImageSourceContext"; +import { useDataURL } from "../../contexts/AssetsContext"; import TokenStatus from "../token/TokenStatus"; import TokenLabel from "../token/TokenLabel"; @@ -43,7 +43,7 @@ function MapToken({ const gridCellPixelSize = useGridCellPixelSize(); - const tokenSource = useImageSource(token, tokenSources, unknownSource); + const tokenSource = useDataURL(token, tokenSources, unknownSource); const [tokenSourceImage, tokenSourceStatus] = useImage(tokenSource); const [tokenAspectRatio, setTokenAspectRatio] = useState(1); diff --git a/src/components/token/ListToken.js b/src/components/token/ListToken.js index 9a094e3..7ab1720 100644 --- a/src/components/token/ListToken.js +++ b/src/components/token/ListToken.js @@ -3,12 +3,12 @@ import { Box, Image } from "theme-ui"; import usePreventTouch from "../../hooks/usePreventTouch"; -import { useImageSource } from "../../contexts/ImageSourceContext"; +import { useDataURL } from "../../contexts/AssetsContext"; import { tokenSources, unknownSource } from "../../tokens"; function ListToken({ token, className }) { - const tokenSource = useImageSource( + const tokenURL = useDataURL( token, tokenSources, unknownSource, @@ -22,7 +22,7 @@ function ListToken({ token, className }) { return ( onTokenSelect(token)} diff --git a/src/contexts/AssetsContext.js b/src/contexts/AssetsContext.js new file mode 100644 index 0000000..77b40c2 --- /dev/null +++ b/src/contexts/AssetsContext.js @@ -0,0 +1,273 @@ +import React, { useState, useContext, useCallback, useEffect } from "react"; +import { decode } from "@msgpack/msgpack"; + +import { useDatabase } from "./DatabaseContext"; + +import { omit } from "../helpers/shared"; + +/** + * @typedef Asset + * @property {string} id + * @property {number} width + * @property {number} height + * @property {Uint8Array} file + * @property {string} mime + */ + +/** + * @callback getAsset + * @param {string} assetId + * @returns {Promise} + */ + +/** + * @typedef AssetsContext + * @property {getAsset} getAsset + */ + +/** + * @type {React.Context} + */ +const AssetsContext = React.createContext(); + +export function AssetsProvider({ children }) { + const { worker } = useDatabase(); + + const getAsset = useCallback( + async (assetId) => { + const packed = await worker.loadData("assets", assetId); + return decode(packed); + }, + [worker] + ); + + return ( + + {children} + + ); +} + +export function useAssets() { + const context = useContext(AssetsContext); + if (context === undefined) { + throw new Error("useAssets must be used within a AssetsProvider"); + } + return context; +} + +/** + * @typedef AssetURL + * @property {string} url + * @property {string} id + * @property {number} references + */ + +/** + * @type React.Context> + */ +export const AssetURLsStateContext = React.createContext(); + +/** + * @type React.Context>> + */ +export const AssetURLsUpdaterContext = React.createContext(); + +/** + * Helper to manage sharing of custom image sources between uses of useAssetURL + */ +export function AssetURLsProvider({ children }) { + const [assetURLs, setAssetURLs] = useState({}); + + // Revoke url when no more references + useEffect(() => { + let urlsToCleanup = []; + for (let url of Object.values(assetURLs)) { + if (url.references <= 0) { + URL.revokeObjectURL(url.url); + urlsToCleanup.push(url.id); + } + } + if (urlsToCleanup.length > 0) { + setAssetURLs((prevURLs) => omit(prevURLs, urlsToCleanup)); + } + }, [assetURLs]); + + return ( + + + {children} + + + ); +} + +/** + * Helper function to load either file or default asset into a URL + * @param {string} assetId + * @param {"file"|"default"} type + * @param {Object.} defaultSources + * @param {string} unknownSource + * @returns {string} + */ +export function useAssetURL(assetId, type, defaultSources, unknownSource = "") { + const assetURLs = useContext(AssetURLsStateContext); + if (assetURLs === undefined) { + throw new Error("useAssetURL must be used within a AssetURLsProvider"); + } + const setAssetURLs = useContext(AssetURLsUpdaterContext); + if (setAssetURLs === undefined) { + throw new Error("useAssetURL must be used within a AssetURLsProvider"); + } + + const { getAsset } = useAssets(); + + useEffect(() => { + if (!assetId || type !== "file") { + return; + } + + async function updateAssetURL() { + const asset = await getAsset(assetId); + if (asset) { + setAssetURLs((prevURLs) => { + if (assetId in prevURLs) { + // Check if the asset url is already added + return { + ...prevURLs, + [assetId]: { + ...prevURLs[assetId], + // Increase references + references: prevURLs[assetId].references + 1, + }, + }; + } else { + const url = URL.createObjectURL( + new Blob([asset.file], { type: asset.mime }) + ); + return { + ...prevURLs, + [assetId]: { url, id: assetId, references: 1 }, + }; + } + }); + } + } + + updateAssetURL(); + + return () => { + // Decrease references + setAssetURLs((prevURLs) => { + if (assetId in prevURLs) { + return { + ...prevURLs, + [assetId]: { + ...prevURLs[assetId], + references: prevURLs[assetId].references - 1, + }, + }; + } else { + return prevURLs; + } + }); + }; + }, [assetId, setAssetURLs, getAsset, type]); + + if (!assetId) { + return unknownSource; + } + + if (type === "default") { + return defaultSources[assetId]; + } + + if (type === "file") { + return assetURLs[assetId]?.url; + } + + return unknownSource; +} + +const dataResolutions = ["ultra", "high", "medium", "low"]; + +/** + * @typedef FileData + * @property {string} file + * @property {"file"} type + * @property {string} thumbnail + * @property {string=} quality + * @property {Object.=} resolutions + */ + +/** + * @typedef DefaultData + * @property {string} key + * @property {"default"} type + */ + +/** + * Load a map or token into a URL taking into account a thumbnail and multiple resolutions + * @param {FileData|DefaultData} data + * @param {Object.} defaultSources + * @param {string} unknownSource + * @param {boolean} thumbnail + * @returns {string} + */ +export function useDataURL( + data, + defaultSources, + unknownSource = "", + thumbnail = false +) { + const { database } = useDatabase(); + const [assetId, setAssetId] = useState(); + + useEffect(() => { + if (!data) { + return; + } + async function loastAssetId() { + if (data.type === "default") { + setAssetId(data.key); + } else { + if (thumbnail) { + setAssetId(data.thumbnail); + } else if (data.resolutions) { + const fileKeys = await database + .table("assets") + .where("id") + .equals(data.file) + .primaryKeys(); + const fileExists = fileKeys.length > 0; + // Check if a resolution is specified + if (data.quality && data.resolutions[data.quality]) { + setAssetId(data.resolutions[data.quality]); + } + // If no file available fallback to the highest resolution + else if (!fileExists) { + for (let res of dataResolutions) { + if (res in data.resolutions) { + setAssetId(data.resolutions[res]); + break; + } + } + } else { + setAssetId(data.file); + } + } else { + setAssetId(data.file); + } + } + } + + loastAssetId(); + }, [data, thumbnail, database]); + + const type = data?.type || "default"; + + const assetURL = useAssetURL(assetId, type, defaultSources, unknownSource); + return assetURL; +} + +export default AssetsContext; diff --git a/src/contexts/ImageSourceContext.js b/src/contexts/ImageSourceContext.js deleted file mode 100644 index b558383..0000000 --- a/src/contexts/ImageSourceContext.js +++ /dev/null @@ -1,157 +0,0 @@ -import React, { useContext, useState, useEffect } from "react"; - -import { omit } from "../helpers/shared"; - -export const ImageSourcesStateContext = React.createContext(); -export const ImageSourcesUpdaterContext = React.createContext(() => {}); - -/** - * Helper to manage sharing of custom image sources between uses of useImageSource - */ -export function ImageSourcesProvider({ children }) { - const [imageSources, setImageSources] = useState({}); - - // Revoke url when no more references - useEffect(() => { - let sourcesToCleanup = []; - for (let source of Object.values(imageSources)) { - if (source.references <= 0) { - URL.revokeObjectURL(source.url); - sourcesToCleanup.push(source.id); - } - } - if (sourcesToCleanup.length > 0) { - setImageSources((prevSources) => omit(prevSources, sourcesToCleanup)); - } - }, [imageSources]); - - return ( - - - {children} - - - ); -} - -/** - * Get id from image data - */ -function getImageFileId(data, thumbnail) { - if (thumbnail) { - return `${data.id}-thumbnail`; - } - if (data.resolutions) { - // Check is a resolution is specified - if (data.quality && data.resolutions[data.quality]) { - return `${data.id}-${data.quality}`; - } else if (!data.file) { - // Fallback to the highest resolution - const resolutionArray = Object.keys(data.resolutions); - const resolution = resolutionArray[resolutionArray.length - 1]; - return `${data.id}-${resolution.id}`; - } - } - return data.id; -} - -/** - * Helper function to load either file or default image into a URL - */ -export function useImageSource(data, defaultSources, unknownSource, thumbnail) { - const imageSources = useContext(ImageSourcesStateContext); - if (imageSources === undefined) { - throw new Error( - "useImageSource must be used within a ImageSourcesProvider" - ); - } - const setImageSources = useContext(ImageSourcesUpdaterContext); - if (setImageSources === undefined) { - throw new Error( - "useImageSource must be used within a ImageSourcesProvider" - ); - } - - useEffect(() => { - if (!data || data.type !== "file") { - return; - } - const id = getImageFileId(data, thumbnail); - - function updateImageSource(file) { - if (file) { - setImageSources((prevSources) => { - if (id in prevSources) { - // Check if the image source is already added - return { - ...prevSources, - [id]: { - ...prevSources[id], - // Increase references - references: prevSources[id].references + 1, - }, - }; - } else { - const url = URL.createObjectURL(new Blob([file])); - return { - ...prevSources, - [id]: { url, id, references: 1 }, - }; - } - }); - } - } - - if (thumbnail) { - updateImageSource(data.thumbnail.file); - } else if (data.resolutions) { - // Check is a resolution is specified - if (data.quality && data.resolutions[data.quality]) { - updateImageSource(data.resolutions[data.quality].file); - } - // If no file available fallback to the highest resolution - else if (!data.file) { - const resolutionArray = Object.keys(data.resolutions); - updateImageSource( - data.resolutions[resolutionArray[resolutionArray.length - 1]].file - ); - } else { - updateImageSource(data.file); - } - } else { - updateImageSource(data.file); - } - - return () => { - // Decrease references - setImageSources((prevSources) => { - if (id in prevSources) { - return { - ...prevSources, - [id]: { - ...prevSources[id], - references: prevSources[id].references - 1, - }, - }; - } else { - return prevSources; - } - }); - }; - }, [data, unknownSource, thumbnail, setImageSources]); - - if (!data) { - return unknownSource; - } - - if (data.type === "default") { - return defaultSources[data.key]; - } - - if (data.type === "file") { - const id = getImageFileId(data, thumbnail); - return imageSources[id]?.url; - } - - return unknownSource; -} diff --git a/src/database.js b/src/database.js index 5ed9ed2..3257635 100644 --- a/src/database.js +++ b/src/database.js @@ -2,6 +2,7 @@ import Dexie, { Version, DexieOptions } from "dexie"; import "dexie-observable"; import shortid from "shortid"; +import { v4 as uuid } from "uuid"; import blobToBuffer from "./helpers/blobToBuffer"; import { getGridDefaultInset } from "./helpers/grid"; @@ -431,9 +432,139 @@ const versions = { }); }); }, + // v1.9.0 - Move map assets into new table + 23(v) { + v.stores({ assets: "id" }).upgrade((tx) => { + tx.table("maps").each((map) => { + let assets = []; + assets.push({ + id: uuid(), + file: map.file, + width: map.width, + height: map.height, + mime: "", + prevId: map.id, + prevType: "map", + }); + + for (let resolution in map.resolutions) { + const mapRes = map.resolutions[resolution]; + assets.push({ + id: uuid(), + file: mapRes.file, + width: mapRes.width, + height: mapRes.height, + mime: "", + prevId: map.id, + prevType: "mapResolution", + resolution, + }); + } + + assets.push({ + id: uuid(), + file: map.thumbnail.file, + width: map.thumbnail.width, + height: map.thumbnail.height, + mime: "", + prevId: map.id, + prevType: "mapThumbnail", + }); + + tx.table("assets").bulkAdd(assets); + }); + }); + }, + // v1.9.0 - Move token assets into new table + 24(v) { + v.stores().upgrade((tx) => { + tx.table("tokens").each((token) => { + let assets = []; + assets.push({ + id: uuid(), + file: token.file, + width: token.width, + height: token.height, + mime: "", + prevId: token.id, + prevType: "token", + }); + assets.push({ + id: uuid(), + file: token.thumbnail.file, + width: token.thumbnail.width, + height: token.thumbnail.height, + mime: "", + prevId: token.id, + prevType: "tokenThumbnail", + }); + tx.table("assets").bulkAdd(assets); + }); + }); + }, + // v1.9.0 - Create foreign keys for assets + 25(v) { + v.stores().upgrade((tx) => { + tx.table("assets").each((asset) => { + if (asset.prevType === "map") { + tx.table("maps").update(asset.prevId, { + file: asset.id, + width: undefined, + height: undefined, + }); + } else if (asset.prevType === "token") { + tx.table("tokens").update(asset.prevId, { + file: asset.id, + width: undefined, + height: undefined, + }); + } else if (asset.prevType === "mapThumbnail") { + tx.table("maps").update(asset.prevId, { thumbnail: asset.id }); + } else if (asset.prevType === "tokenThumbnail") { + tx.table("tokens").update(asset.prevId, { thumbnail: asset.id }); + } else if (asset.prevType === "mapResolution") { + tx.table("maps").update(asset.prevId, { + resolutions: undefined, + [asset.resolution]: asset.id, + }); + } + }); + }); + }, + // v1.9.0 - Remove asset migration helpers + 26(v) { + v.stores().upgrade((tx) => { + tx.table("assets") + .toCollection() + .modify((asset) => { + delete asset.prevId; + if (asset.prevType === "mapResolution") { + delete asset.resolution; + } + delete asset.prevType; + }); + }); + }, + // v1.9.0 - Remap map resolution assets + 27(v) { + v.stores().upgrade((tx) => { + tx.table("maps") + .toCollection() + .modify((map) => { + const resolutions = ["low", "medium", "high", "ultra"]; + map.resolutions = {}; + for (let res of resolutions) { + if (res in map) { + map.resolutions[res] = map[res]; + delete map[res]; + } + } + }); + }); + }, }; -const latestVersion = 22; +const latestVersion = 27; /** * Load versions onto a database up to a specific version number diff --git a/src/helpers/KonvaBridge.js b/src/helpers/KonvaBridge.js index b8fb3db..0573859 100644 --- a/src/helpers/KonvaBridge.js +++ b/src/helpers/KonvaBridge.js @@ -23,10 +23,10 @@ import AuthContext, { useAuth } from "../contexts/AuthContext"; import SettingsContext, { useSettings } from "../contexts/SettingsContext"; import KeyboardContext from "../contexts/KeyboardContext"; import TokenDataContext, { useTokenData } from "../contexts/TokenDataContext"; -import { - ImageSourcesStateContext, - ImageSourcesUpdaterContext, -} from "../contexts/ImageSourceContext"; +import AssetsContext, { + AssetURLsStateContext, + AssetURLsUpdaterContext, +} from "../contexts/AssetsContext"; import { useGrid, useGridCellPixelSize, @@ -52,8 +52,9 @@ function KonvaBridge({ stageRender, children }) { const auth = useAuth(); const settings = useSettings(); const tokenData = useTokenData(); - const imageSources = useContext(ImageSourcesStateContext); - const setImageSources = useContext(ImageSourcesUpdaterContext); + const assets = useContext(AssetsContext); + const assetURLs = useContext(AssetURLsStateContext); + const setAssetURLs = useContext(AssetURLsUpdaterContext); const keyboardValue = useContext(KeyboardContext); const stageScale = useStageScale(); @@ -78,61 +79,63 @@ function KonvaBridge({ stageRender, children }) { - - - - - + + + + - - - - - - - - - + + + + + + + + - - - - - {children} - - - - - - - - - - - - - - - - - - + + {children} + + + + + + + + + + + + + + + + + + + diff --git a/src/hooks/useMapImage.js b/src/hooks/useMapImage.js index a67dc2b..02cf5b6 100644 --- a/src/hooks/useMapImage.js +++ b/src/hooks/useMapImage.js @@ -1,23 +1,23 @@ import { useEffect, useState } from "react"; import useImage from "use-image"; -import { useImageSource } from "../contexts/ImageSourceContext"; +import { useDataURL } from "../contexts/AssetsContext"; import { mapSources as defaultMapSources } from "../maps"; function useMapImage(map) { - const mapSource = useImageSource(map, defaultMapSources); - const [mapSourceImage, mapSourceImageStatus] = useImage(mapSource); + const mapURL = useDataURL(map, defaultMapSources); + const [mapImage, mapImageStatus] = useImage(mapURL); // Create a map source that only updates when the image is fully loaded - const [loadedMapSourceImage, setLoadedMapSourceImage] = useState(); + const [loadedMapImage, setLoadedMapImage] = useState(); useEffect(() => { - if (mapSourceImageStatus === "loaded") { - setLoadedMapSourceImage(mapSourceImage); + if (mapImageStatus === "loaded") { + setLoadedMapImage(mapImage); } - }, [mapSourceImage, mapSourceImageStatus]); + }, [mapImage, mapImageStatus]); - return [loadedMapSourceImage, mapSourceImageStatus]; + return [loadedMapImage, mapImageStatus]; } export default useMapImage; diff --git a/src/workers/DatabaseWorker.js b/src/workers/DatabaseWorker.js index 28d6c9c..8e4278f 100644 --- a/src/workers/DatabaseWorker.js +++ b/src/workers/DatabaseWorker.js @@ -15,26 +15,21 @@ let service = { * Load either a whole table or individual item from the DB * @param {string} table Table to load from * @param {string=} key Optional database key to load, if undefined whole table will be loaded - * @param {bool} excludeFiles Optional exclude files from loaded data when using whole table loading */ - async loadData(table, key, excludeFiles = true) { + async loadData(table, key) { try { let db = getDatabase({}); if (key) { // Load specific item const data = await db.table(table).get(key); - return data; + const packed = encode(data); + return Comlink.transfer(packed, [packed.buffer]); } else { // Load entire table let items = []; // Use a cursor instead of toArray to prevent IPC max size error await db.table(table).each((item) => { - if (excludeFiles) { - const { file, resolutions, ...rest } = item; - items.push(rest); - } else { - items.push(item); - } + items.push(item); }); // Pack data with msgpack so we can use transfer to avoid memory issues diff --git a/yarn.lock b/yarn.lock index 439014e..9ea015e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13070,6 +13070,11 @@ uuid@^8.3.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.1.tgz#2ba2e6ca000da60fce5a196954ab241131e05a31" integrity sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg== +uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + v8-compile-cache@^2.0.3: version "2.2.0" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.2.0.tgz#9471efa3ef9128d2f7c6a7ca39c4dd6b5055b132" From f078fb687e9290734454eba35432f5b0a2ac74ca Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Fri, 23 Apr 2021 11:46:52 +1000 Subject: [PATCH 002/176] Add types to database context --- src/contexts/DatabaseContext.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/contexts/DatabaseContext.js b/src/contexts/DatabaseContext.js index 5e1e3b5..40c5198 100644 --- a/src/contexts/DatabaseContext.js +++ b/src/contexts/DatabaseContext.js @@ -1,4 +1,6 @@ import React, { useState, useEffect, useContext } from "react"; +// eslint-disable-next-line no-unused-vars +import Dexie from "dexie"; import * as Comlink from "comlink"; import ErrorBanner from "../components/banner/ErrorBanner"; @@ -7,6 +9,17 @@ import { getDatabase } from "../database"; import DatabaseWorker from "worker-loader!../workers/DatabaseWorker"; // eslint-disable-line import/no-webpack-loader-syntax +/** + * @typedef DatabaseContext + * @property {Dexie|undefined} database + * @property {any} worker + * @property {string} databaseStatus + * @property {Error|undefined} databaseError + */ + +/** + * @type {React.Context} + */ const DatabaseContext = React.createContext(); const worker = Comlink.wrap(new DatabaseWorker()); From b2ed5d2f6bcf4e7cfb824baae7089b7d56c56244 Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Fri, 23 Apr 2021 11:47:08 +1000 Subject: [PATCH 003/176] Ensure session is available for disconnect --- src/network/Session.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/network/Session.js b/src/network/Session.js index b2973cc..b646708 100644 --- a/src/network/Session.js +++ b/src/network/Session.js @@ -111,7 +111,7 @@ class Session extends EventEmitter { } disconnect() { - this.socket.disconnect(); + this.socket?.disconnect(); } /** @@ -198,7 +198,12 @@ class Session extends EventEmitter { this._gameId = gameId; this._password = password; - this.socket.emit("join_game", gameId, password, process.env.REACT_APP_VERSION); + this.socket.emit( + "join_game", + gameId, + password, + process.env.REACT_APP_VERSION + ); this.emit("status", "joining"); } From 6b9665ffe80d1e61bdf8ffb1dc4b92d25c6e723b Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Fri, 23 Apr 2021 11:47:18 +1000 Subject: [PATCH 004/176] Fix token tile loading --- src/components/token/TokenTile.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/token/TokenTile.js b/src/components/token/TokenTile.js index 9e6eec3..6eb9547 100644 --- a/src/components/token/TokenTile.js +++ b/src/components/token/TokenTile.js @@ -2,7 +2,7 @@ import React from "react"; import Tile from "../Tile"; -import { useAssetURL } from "../../contexts/AssetsContext"; +import { useDataURL } from "../../contexts/AssetsContext"; import { tokenSources as defaultTokenSources, @@ -18,7 +18,7 @@ function TokenTile({ canEdit, badges, }) { - const tokenURL = useAssetURL( + const tokenURL = useDataURL( token, defaultTokenSources, unknownSource, From a023ef61ed77f9219dcd1c9fcc807993a6e46d7d Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Fri, 23 Apr 2021 11:48:24 +1000 Subject: [PATCH 005/176] Fix token and map editing and viewing --- src/contexts/AssetsContext.js | 38 +++++++--- src/database.js | 4 -- src/helpers/KonvaBridge.js | 123 +++++++++++++++++--------------- src/helpers/image.js | 16 +++-- src/modals/SelectMapModal.js | 41 ++++++++--- src/modals/SelectTokensModal.js | 31 ++++++-- 6 files changed, 155 insertions(+), 98 deletions(-) diff --git a/src/contexts/AssetsContext.js b/src/contexts/AssetsContext.js index 77b40c2..98bbb2d 100644 --- a/src/contexts/AssetsContext.js +++ b/src/contexts/AssetsContext.js @@ -20,9 +20,15 @@ import { omit } from "../helpers/shared"; * @returns {Promise} */ +/** + * @callback addAssets + * @param {Asset[]} assets + */ + /** * @typedef AssetsContext * @property {getAsset} getAsset + * @property {addAssets} addAssets */ /** @@ -31,7 +37,7 @@ import { omit } from "../helpers/shared"; const AssetsContext = React.createContext(); export function AssetsProvider({ children }) { - const { worker } = useDatabase(); + const { worker, database } = useDatabase(); const getAsset = useCallback( async (assetId) => { @@ -41,10 +47,20 @@ export function AssetsProvider({ children }) { [worker] ); + const addAssets = useCallback( + async (assets) => { + return database.table("assets").bulkAdd(assets); + }, + [database] + ); + + const value = { + getAsset, + addAssets, + }; + return ( - - {children} - + {children} ); } @@ -107,10 +123,10 @@ export function AssetURLsProvider({ children }) { * @param {string} assetId * @param {"file"|"default"} type * @param {Object.} defaultSources - * @param {string} unknownSource - * @returns {string} + * @param {string|undefined} unknownSource + * @returns {string|undefined} */ -export function useAssetURL(assetId, type, defaultSources, unknownSource = "") { +export function useAssetURL(assetId, type, defaultSources, unknownSource) { const assetURLs = useContext(AssetURLsStateContext); if (assetURLs === undefined) { throw new Error("useAssetURL must be used within a AssetURLsProvider"); @@ -183,7 +199,7 @@ export function useAssetURL(assetId, type, defaultSources, unknownSource = "") { } if (type === "file") { - return assetURLs[assetId]?.url; + return assetURLs[assetId]?.url || unknownSource; } return unknownSource; @@ -210,14 +226,14 @@ const dataResolutions = ["ultra", "high", "medium", "low"]; * Load a map or token into a URL taking into account a thumbnail and multiple resolutions * @param {FileData|DefaultData} data * @param {Object.} defaultSources - * @param {string} unknownSource + * @param {string|undefined} unknownSource * @param {boolean} thumbnail - * @returns {string} + * @returns {string|undefined} */ export function useDataURL( data, defaultSources, - unknownSource = "", + unknownSource, thumbnail = false ) { const { database } = useDatabase(); diff --git a/src/database.js b/src/database.js index 3257635..af20c1d 100644 --- a/src/database.js +++ b/src/database.js @@ -509,14 +509,10 @@ const versions = { if (asset.prevType === "map") { tx.table("maps").update(asset.prevId, { file: asset.id, - width: undefined, - height: undefined, }); } else if (asset.prevType === "token") { tx.table("tokens").update(asset.prevId, { file: asset.id, - width: undefined, - height: undefined, }); } else if (asset.prevType === "mapThumbnail") { tx.table("maps").update(asset.prevId, { thumbnail: asset.id }); diff --git a/src/helpers/KonvaBridge.js b/src/helpers/KonvaBridge.js index 0573859..6aeecb5 100644 --- a/src/helpers/KonvaBridge.js +++ b/src/helpers/KonvaBridge.js @@ -43,6 +43,7 @@ import { GridStrokeWidthContext, GridCellPixelOffsetContext, } from "../contexts/GridContext"; +import DatabaseContext, { useDatabase } from "../contexts/DatabaseContext"; /** * Provide a bridge for konva that forwards our contexts @@ -74,72 +75,78 @@ function KonvaBridge({ stageRender, children }) { const gridCellPixelOffset = useGridCellPixelOffset(); const gridOffset = useGridOffset(); + const database = useDatabase(); + return stageRender( - - - - - - - - - - + + + + + + + + + - - - - - - + + + + + - - - + + - - - - - {children} - - - - - - - - - - - - - - - - - - - - - - - + + {children} + + + + + + + + + + + + + + + + + + + + + + + + ); } diff --git a/src/helpers/image.js b/src/helpers/image.js index fbcf7a8..fb33712 100644 --- a/src/helpers/image.js +++ b/src/helpers/image.js @@ -1,3 +1,5 @@ +import { v4 as uuid } from "uuid"; + import blobToBuffer from "./blobToBuffer"; const lightnessDetectionOffset = 0.1; @@ -88,12 +90,12 @@ export async function resizeImage(image, size, type, quality) { } /** - * @typedef ImageFile - * @property {Uint8Array|null} file + * @typedef Asset + * @property {string} id * @property {number} width * @property {number} height - * @property {"file"} type - * @property {string} id + * @property {Uint8Array} file + * @property {string} mime */ /** @@ -102,7 +104,7 @@ export async function resizeImage(image, size, type, quality) { * @param {string} type the mime type of the image * @param {number} size the width and height of the thumbnail * @param {number} quality if image is a jpeg or webp this is the quality setting - * @returns {Promise} + * @returns {Promise} */ export async function createThumbnail(image, type, size = 300, quality = 0.5) { let canvas = document.createElement("canvas"); @@ -150,7 +152,7 @@ export async function createThumbnail(image, type, size = 300, quality = 0.5) { file: thumbnailBuffer, width: thumbnailImage.width, height: thumbnailImage.height, - type: "file", - id: "thumbnail", + mime: type, + id: uuid(), }; } diff --git a/src/modals/SelectMapModal.js b/src/modals/SelectMapModal.js index 517bc45..f759ac3 100644 --- a/src/modals/SelectMapModal.js +++ b/src/modals/SelectMapModal.js @@ -1,6 +1,6 @@ import React, { useRef, useState } from "react"; import { Button, Flex, Label } from "theme-ui"; -import shortid from "shortid"; +import { v4 as uuid } from "uuid"; import Case from "case"; import { useToasts } from "react-toast-notifications"; @@ -28,6 +28,7 @@ import useResponsiveLayout from "../hooks/useResponsiveLayout"; import { useMapData } from "../contexts/MapDataContext"; import { useAuth } from "../contexts/AuthContext"; import { useKeyboard, useBlur } from "../contexts/KeyboardContext"; +import { useAssets } from "../contexts/AssetsContext"; import shortcuts from "../shortcuts"; @@ -72,6 +73,7 @@ function SelectMapModal({ getMapFromDB, getMapStateFromDB, } = useMapData(); + const { addAssets } = useAssets(); /** * Search @@ -221,6 +223,8 @@ function SelectMapModal({ gridSize = { x: 22, y: 22 }; } + let assets = []; + // Create resolutions const resolutions = {}; for (let resolution of mapResolutions) { @@ -239,26 +243,38 @@ function SelectMapModal({ resolution.quality ); if (resized.blob) { + const assetId = uuid(); + resolutions[resolution.id] = assetId; const resizedBuffer = await blobToBuffer(resized.blob); - resolutions[resolution.id] = { + const asset = { file: resizedBuffer, width: resized.width, height: resized.height, - type: "file", - id: resolution.id, + id: assetId, + mime: file.type, }; + assets.push(asset); } } } // Create thumbnail const thumbnail = await createThumbnail(image, file.type); + assets.push(thumbnail); - handleMapAdd({ - // Save as a buffer to send with msgpack + const fileAsset = { + id: uuid(), file: buffer, - resolutions, - thumbnail, + width: image.width, + height: image.height, + mime: file.type, + }; + assets.push(fileAsset); + + const map = { name, + resolutions, + file: fileAsset.id, + thumbnail: thumbnail.id, type: "file", grid: { size: gridSize, @@ -275,13 +291,15 @@ function SelectMapModal({ }, width: image.width, height: image.height, - id: shortid.generate(), + id: uuid(), created: Date.now(), lastModified: Date.now(), lastUsed: Date.now(), owner: userId, ...defaultMapProps, - }); + }; + + handleMapAdd(map, assets); setIsLoading(false); URL.revokeObjectURL(url); resolve(); @@ -311,8 +329,9 @@ function SelectMapModal({ selectedMapIds.includes(state.mapId) ); - async function handleMapAdd(map) { + async function handleMapAdd(map, assets) { await addMap(map); + await addAssets(assets); setSelectedMapIds([map.id]); } diff --git a/src/modals/SelectTokensModal.js b/src/modals/SelectTokensModal.js index 91789c5..6f04bb9 100644 --- a/src/modals/SelectTokensModal.js +++ b/src/modals/SelectTokensModal.js @@ -1,6 +1,6 @@ import React, { useRef, useState } from "react"; import { Flex, Label, Button } from "theme-ui"; -import shortid from "shortid"; +import { v4 as uuid } from "uuid"; import Case from "case"; import { useToasts } from "react-toast-notifications"; @@ -22,6 +22,7 @@ import useResponsiveLayout from "../hooks/useResponsiveLayout"; import { useTokenData } from "../contexts/TokenDataContext"; import { useAuth } from "../contexts/AuthContext"; import { useKeyboard, useBlur } from "../contexts/KeyboardContext"; +import { useAssets } from "../contexts/AssetsContext"; import shortcuts from "../shortcuts"; @@ -36,6 +37,7 @@ function SelectTokensModal({ isOpen, onRequestClose }) { updateTokens, tokensLoading, } = useTokenData(); + const { addAssets } = useAssets(); /** * Search @@ -160,13 +162,24 @@ function SelectTokensModal({ isOpen, onRequestClose }) { return new Promise((resolve, reject) => { image.onload = async function () { + let assets = []; const thumbnail = await createThumbnail(image, file.type); + assets.push(thumbnail); - handleTokenAdd({ + const fileAsset = { + id: uuid(), file: buffer, - thumbnail, + width: image.width, + height: image.height, + mime: file.type, + }; + assets.push(fileAsset); + + const token = { name, - id: shortid.generate(), + thumbnail: thumbnail.id, + file: fileAsset.id, + id: uuid(), type: "file", created: Date.now(), lastModified: Date.now(), @@ -178,8 +191,11 @@ function SelectTokensModal({ isOpen, onRequestClose }) { group: "", width: image.width, height: image.height, - }); + }; + + handleTokenAdd(token, assets); setIsLoading(false); + URL.revokeObjectURL(url); resolve(); }; image.onerror = reject; @@ -196,8 +212,9 @@ function SelectTokensModal({ isOpen, onRequestClose }) { selectedTokenIds.includes(token.id) ); - function handleTokenAdd(token) { - addToken(token); + async function handleTokenAdd(token, assets) { + await addToken(token); + await addAssets(assets); setSelectedTokenIds([token.id]); } From 245a9cee4332b31033673836d5ebe6bfd09989f5 Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Fri, 23 Apr 2021 17:10:10 +1000 Subject: [PATCH 006/176] Move tokenState to work without backing token, add asset sync --- src/components/map/MapToken.js | 31 ++-- src/components/map/MapTokens.js | 78 +++------- src/components/token/TokenSettings.js | 8 +- src/components/token/Tokens.js | 15 +- src/contexts/AssetsContext.js | 15 ++ src/contexts/TokenDataContext.js | 36 ----- src/database.js | 42 +++++- src/helpers/image.js | 8 +- src/helpers/map.js | 27 ++++ src/modals/SelectMapModal.js | 5 +- src/modals/SelectTokensModal.js | 7 +- src/network/NetworkedMapAndTokens.js | 208 ++++++-------------------- src/tokens/index.js | 3 +- 13 files changed, 198 insertions(+), 285 deletions(-) create mode 100644 src/helpers/map.js diff --git a/src/components/map/MapToken.js b/src/components/map/MapToken.js index e866c89..ccb2770 100644 --- a/src/components/map/MapToken.js +++ b/src/components/map/MapToken.js @@ -24,7 +24,6 @@ import TokenLabel from "../token/TokenLabel"; import { tokenSources, unknownSource } from "../../tokens"; function MapToken({ - token, tokenState, onTokenStateChange, onTokenMenuOpen, @@ -43,7 +42,7 @@ function MapToken({ const gridCellPixelSize = useGridCellPixelSize(); - const tokenSource = useDataURL(token, tokenSources, unknownSource); + const tokenSource = useDataURL(tokenState, tokenSources, unknownSource); const [tokenSourceImage, tokenSourceStatus] = useImage(tokenSource); const [tokenAspectRatio, setTokenAspectRatio] = useState(1); @@ -59,7 +58,7 @@ function MapToken({ const tokenGroup = event.target; const tokenImage = imageRef.current; - if (token && token.category === "vehicle") { + if (tokenState.category === "vehicle") { // Enable hit detection for .intersects() function Konva.hitOnDragEnabled = true; @@ -99,7 +98,7 @@ function MapToken({ const tokenGroup = event.target; const mountChanges = {}; - if (token && token.category === "vehicle") { + if (tokenState.category === "vehicle") { Konva.hitOnDragEnabled = false; const parent = tokenGroup.getParent(); @@ -196,8 +195,16 @@ function MapToken({ const canvas = image.getCanvas(); const pixelRatio = canvas.pixelRatio || 1; - if (tokenSourceStatus === "loaded" && tokenWidth > 0 && tokenHeight > 0) { - const maxImageSize = token ? Math.max(token.width, token.height) : 512; // Default to 512px + if ( + tokenSourceStatus === "loaded" && + tokenWidth > 0 && + tokenHeight > 0 && + tokenSourceImage + ) { + const maxImageSize = Math.max( + tokenSourceImage.width, + tokenSourceImage.height + ); const maxTokenSize = Math.max(tokenWidth, tokenHeight); // Constrain image buffer to original image size const maxRatio = maxImageSize / maxTokenSize; @@ -210,7 +217,13 @@ function MapToken({ }); image.drawHitFromCache(); } - }, [debouncedStageScale, tokenWidth, tokenHeight, tokenSourceStatus, token]); + }, [ + debouncedStageScale, + tokenWidth, + tokenHeight, + tokenSourceStatus, + tokenSourceImage, + ]); // Animate to new token positions if edited by others const tokenX = tokenState.x * mapWidth; @@ -232,8 +245,8 @@ function MapToken({ // Token name is used by on click to find whether a token is a vehicle or prop let tokenName = ""; - if (token) { - tokenName = token.category; + if (tokenState) { + tokenName = tokenState.category; } if (tokenState && tokenState.locked) { tokenName = tokenName + "-locked"; diff --git a/src/components/map/MapTokens.js b/src/components/map/MapTokens.js index 426dcfe..957d038 100644 --- a/src/components/map/MapTokens.js +++ b/src/components/map/MapTokens.js @@ -1,10 +1,8 @@ -import React, { useEffect } from "react"; +import React from "react"; import { Group } from "react-konva"; import MapToken from "./MapToken"; -import { useTokenData } from "../../contexts/TokenDataContext"; - function MapTokens({ map, mapState, @@ -15,31 +13,6 @@ function MapTokens({ selectedToolId, disabledTokens, }) { - const { tokensById, loadTokens } = useTokenData(); - - // Ensure tokens files have been loaded into the token data - useEffect(() => { - async function loadFileTokens() { - const tokenIds = new Set( - Object.values(mapState.tokens).map((state) => state.tokenId) - ); - const tokensToLoad = []; - for (let tokenId of tokenIds) { - const token = tokensById[tokenId]; - if (token && token.type === "file" && !token.file) { - tokensToLoad.push(tokenId); - } - } - if (tokensToLoad.length > 0) { - await loadTokens(tokensToLoad); - } - } - - if (mapState) { - loadFileTokens(); - } - }, [mapState, tokensById, loadTokens]); - function getMapTokenCategoryWeight(category) { switch (category) { case "character": @@ -55,38 +28,28 @@ function MapTokens({ // Sort so vehicles render below other tokens function sortMapTokenStates(a, b, tokenDraggingOptions) { - const tokenA = tokensById[a.tokenId]; - const tokenB = tokensById[b.tokenId]; - if (tokenA && tokenB) { - // If categories are different sort in order "prop", "vehicle", "character" - if (tokenB.category !== tokenA.category) { - const aWeight = getMapTokenCategoryWeight(tokenA.category); - const bWeight = getMapTokenCategoryWeight(tokenB.category); - return bWeight - aWeight; - } else if ( - tokenDraggingOptions && - tokenDraggingOptions.dragging && - tokenDraggingOptions.tokenState.id === a.id - ) { - // If dragging token a move above - return 1; - } else if ( - tokenDraggingOptions && - tokenDraggingOptions.dragging && - tokenDraggingOptions.tokenState.id === b.id - ) { - // If dragging token b move above - return -1; - } else { - // Else sort so last modified is on top - return a.lastModified - b.lastModified; - } - } else if (tokenA) { + // If categories are different sort in order "prop", "vehicle", "character" + if (b.category !== a.category) { + const aWeight = getMapTokenCategoryWeight(a.category); + const bWeight = getMapTokenCategoryWeight(b.category); + return bWeight - aWeight; + } else if ( + tokenDraggingOptions && + tokenDraggingOptions.dragging && + tokenDraggingOptions.tokenState.id === a.id + ) { + // If dragging token a move above return 1; - } else if (tokenB) { + } else if ( + tokenDraggingOptions && + tokenDraggingOptions.dragging && + tokenDraggingOptions.tokenState.id === b.id + ) { + // If dragging token b move above return -1; } else { - return 0; + // Else sort so last modified is on top + return a.lastModified - b.lastModified; } } @@ -97,7 +60,6 @@ function MapTokens({ .map((tokenState) => ( - + - - - - - - ); -} - -export default EditGroupModal; From 05f68dcb92c41fb62f208c246433261054c19440 Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Sat, 5 Jun 2021 17:16:39 +1000 Subject: [PATCH 083/176] Add back range tile select --- src/contexts/GroupContext.js | 112 +++++++++++++++++++++++------------ 1 file changed, 73 insertions(+), 39 deletions(-) diff --git a/src/contexts/GroupContext.js b/src/contexts/GroupContext.js index a784d03..fa123d9 100644 --- a/src/contexts/GroupContext.js +++ b/src/contexts/GroupContext.js @@ -22,6 +22,9 @@ export function GroupProvider({ // Either single, multiple or range const [selectMode, setSelectMode] = useState("single"); + /** + * Group Open + */ const [openGroupId, setOpenGroupId] = useState(); const [openGroupItems, setOpenGroupItems] = useState([]); useEffect(() => { @@ -42,6 +45,47 @@ export function GroupProvider({ setOpenGroupId(); } + /** + * Search + */ + const [filter, setFilter] = useState(); + const [filteredGroupItems, setFilteredGroupItems] = useState([]); + const [fuse, setFuse] = useState(); + // Update search index when items change + useEffect(() => { + let items = []; + for (let group of groups) { + const itemsToAdd = getGroupItems(group); + const namedItems = itemsToAdd.map((item) => ({ + ...item, + name: itemNames[item.id], + })); + items.push(...namedItems); + } + setFuse(new Fuse(items, { keys: ["name"] })); + }, [groups, itemNames]); + + // Perform search when search changes + useEffect(() => { + if (filter) { + const query = fuse.search(filter); + setFilteredGroupItems(query.map((result) => result.item)); + setOpenGroupId(); + } else { + setFilteredGroupItems([]); + } + }, [filter, fuse]); + + /** + * Handlers + */ + + const activeGroups = openGroupId + ? openGroupItems + : filter + ? filteredGroupItems + : groups; + /** * @param {string|undefined} groupId The group to apply changes to, leave undefined to replace the full group object */ @@ -73,8 +117,35 @@ export function GroupProvider({ } break; case "range": - /// TODO: Fix when new groups system is added - return; + if (selectedGroupIds.length > 0) { + const currentIndex = activeGroups.findIndex( + (g) => g.id === groupId + ); + const lastIndex = activeGroups.findIndex( + (g) => g.id === selectedGroupIds[selectedGroupIds.length - 1] + ); + let idsToAdd = []; + let idsToRemove = []; + const direction = currentIndex > lastIndex ? 1 : -1; + for ( + let i = lastIndex + direction; + direction < 0 ? i >= currentIndex : i <= currentIndex; + i += direction + ) { + const id = activeGroups[i].id; + if (selectedGroupIds.includes(id)) { + idsToRemove.push(id); + } else { + idsToAdd.push(id); + } + } + groupIds = [...selectedGroupIds, ...idsToAdd].filter( + (id) => !idsToRemove.includes(id) + ); + } else { + groupIds = [groupId]; + } + break; default: groupIds = []; } @@ -119,43 +190,6 @@ export function GroupProvider({ useBlur(handleBlur); - /** - * Search - */ - const [filter, setFilter] = useState(); - const [filteredGroupItems, setFilteredGroupItems] = useState([]); - const [fuse, setFuse] = useState(); - // Update search index when items change - useEffect(() => { - let items = []; - for (let group of groups) { - const itemsToAdd = getGroupItems(group); - const namedItems = itemsToAdd.map((item) => ({ - ...item, - name: itemNames[item.id], - })); - items.push(...namedItems); - } - setFuse(new Fuse(items, { keys: ["name"] })); - }, [groups, itemNames]); - - // Perform search when search changes - useEffect(() => { - if (filter) { - const query = fuse.search(filter); - setFilteredGroupItems(query.map((result) => result.item)); - setOpenGroupId(); - } else { - setFilteredGroupItems([]); - } - }, [filter, fuse]); - - const activeGroups = openGroupId - ? openGroupItems - : filter - ? filteredGroupItems - : groups; - const value = { groups, activeGroups, From 27c68b499d95d5258e0bb36794a27be22c67b901 Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Sat, 5 Jun 2021 18:05:35 +1000 Subject: [PATCH 084/176] Add back group names --- src/components/tile/TilesOverlay.js | 53 ++++++++++++++++++++++++++--- src/helpers/group.js | 15 ++++++++ src/modals/GroupNameModal.js | 51 +++++++++++++++++++++++++++ 3 files changed, 115 insertions(+), 4 deletions(-) create mode 100644 src/modals/GroupNameModal.js diff --git a/src/components/tile/TilesOverlay.js b/src/components/tile/TilesOverlay.js index e280013..40215bf 100644 --- a/src/components/tile/TilesOverlay.js +++ b/src/components/tile/TilesOverlay.js @@ -1,5 +1,5 @@ import React, { useState } from "react"; -import { Box, Close, Grid, useThemeUI } from "theme-ui"; +import { Box, Close, Grid, useThemeUI, IconButton, Text, Flex } from "theme-ui"; import { useSpring, animated, config } from "react-spring"; import ReactResizeDetector from "react-resize-detector"; import SimpleBar from "simplebar-react"; @@ -10,8 +10,20 @@ import TilesUngroupDroppable from "./TilesUngroupDroppable"; import useResponsiveLayout from "../../hooks/useResponsiveLayout"; +import ChangeNicknameIcon from "../../icons/ChangeNicknameIcon"; + +import GroupNameModal from "../../modals/GroupNameModal"; + +import { renameGroup } from "../../helpers/group"; + function TilesOverlay({ children }) { - const { openGroupId, onGroupClose, onGroupSelect } = useGroup(); + const { + groups, + openGroupId, + onGroupClose, + onGroupSelect, + onGroupsChange, + } = useGroup(); const { theme } = useThemeUI(); @@ -34,6 +46,14 @@ function TilesOverlay({ children }) { setOverlaySize({ width, height }); } + const [isGroupNameModalOpen, setIsGroupNameModalOpen] = useState(false); + function handleGroupNameChange(name) { + onGroupsChange(renameGroup(groups, openGroupId, name)); + setIsGroupNameModalOpen(false); + } + + const group = groups.find((group) => group.id === openGroupId); + return ( <> {openGroupId && ( @@ -88,13 +108,32 @@ function TilesOverlay({ children }) { borderColor: "border", cursor: "default", display: "flex", - alignItems: "flex-end", - justifyContent: "center", + justifyContent: "flex-start", + alignItems: "center", position: "relative", + flexDirection: "column", }} bg="background" onClick={(e) => e.stopPropagation()} > + + + {group?.name} + + setIsGroupNameModalOpen(true)} + > + + + + setIsGroupNameModalOpen(false)} + /> ); } diff --git a/src/helpers/group.js b/src/helpers/group.js index 9b81aea..a23313e 100644 --- a/src/helpers/group.js +++ b/src/helpers/group.js @@ -221,3 +221,18 @@ export function getItemNames(items, itemKey = "id") { } return names; } + +/** + * Immutably rename a group + * @param {Group[]} groups + * @param {string} groupId + * @param {string} newName + */ +export function renameGroup(groups, groupId, newName) { + let newGroups = cloneDeep(groups); + const groupIndex = newGroups.findIndex((group) => group.id === groupId); + if (groupIndex >= 0) { + newGroups[groupIndex].name = newName; + } + return newGroups; +} diff --git a/src/modals/GroupNameModal.js b/src/modals/GroupNameModal.js new file mode 100644 index 0000000..f410256 --- /dev/null +++ b/src/modals/GroupNameModal.js @@ -0,0 +1,51 @@ +import React, { useState, useRef, useEffect } from "react"; +import { Box, Input, Button, Label, Flex } from "theme-ui"; + +import Modal from "../components/Modal"; + +function GroupNameModal({ isOpen, name, onSubmit, onRequestClose }) { + const [tmpName, setTempName] = useState(name); + + useEffect(() => { + setTempName(name); + }, [name]); + + function handleChange(event) { + setTempName(event.target.value); + } + + function handleSubmit(event) { + event.preventDefault(); + onSubmit(tmpName); + } + + const inputRef = useRef(); + function focusInput() { + inputRef.current && inputRef.current.focus(); + } + + return ( + + + + + + + + + + ); +} + +export default GroupNameModal; From f5ccbe8f8f25b75d121c86be4f00d4aedf1b9078 Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Sun, 6 Jun 2021 09:05:39 +1000 Subject: [PATCH 085/176] Update fog edit pattern opacity for new shading method --- src/components/map/MapFog.js | 6 +----- src/images/DiagonalPattern.png | Bin 860 -> 802 bytes 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/components/map/MapFog.js b/src/components/map/MapFog.js index f1206ac..8c97149 100644 --- a/src/components/map/MapFog.js +++ b/src/components/map/MapFog.js @@ -490,11 +490,7 @@ function MapFog({ const holes = shape.data.holes && shape.data.holes.map((hole) => hole.reduce(reducePoints, [])); - const opacity = editable - ? !shape.visible - ? editOpacity / 2 - : editOpacity - : 1; + const opacity = editable ? editOpacity : 1; // Control opacity only on fill as using opacity with stroke leads to performance issues const fill = new Color(colors[shape.color] || shape.color) .alpha(opacity) diff --git a/src/images/DiagonalPattern.png b/src/images/DiagonalPattern.png index 55b3b1721dffdbe630cfd94604aeed1e418ec431..31bc88d942e80df3e543e8125ad0fb16c5b5c639 100644 GIT binary patch literal 802 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K58911MRQ8&P5FiyB;1lBNUf=^{(g@@jd_D#= zTD~O6FPK5h-a&=a(wK!w&xTJ{*iM6y-Q1K-+g3nQk=xpUd7t~o#S9Egp`I>|Ar*`+ zCyp{64iI79a6aZ4+qe5hIrg$k&2HUj4t~OHq?V?4;+Du(%D~W-*m}I}cJ;66KU=pH3cTE3 z|Nr0L|H}XWJO7ja^nb$t_CNMd{wx0H|5<+mDD=Pk&;R4U-ronS`u+WV|DXTT5E%@` z`zQPZDuAj4n&8JoZVcmDam2o!=~ckcfJ3?Dvh>VSdy{slY~_5UjW-#_91d{77@`vV;4{7?Q@ z!wjqk22b0cdRI(U&LD>)g--PU6aUq*hNAMP|BWE`ng74f;Q60Ja#F;ibWk$)boFyt I=akR{076Z)zyJUM literal 860 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K58911MRQ8&P5Fk|=;1lBNUf=^{(ixQf-&6p! zQoSU|FPI_X&;JQ3PfiQ4Z~I_gY^Lh-a~_lI-K%0OtG+s>{%g{GeUN+m$1@Y_{~EV5 zFfiqNx;TbZFuI&L%IIV$!g62-dwKi6YL-X+9$WvihAhGI6 zd#=23@qg8O56l#&esE*esQ>pTsQzF4iNE#}f4ASUfAUZ9JO9r56MvoG$rt^f@Vj2& zd;RubpP!$fZx2-Y^Rx2zdZ-Ml;{RQD{9Awq<6Mx%#{QU_x z^uF@<{a6jH@4oY24QwsQ$o*ITOYQtG3UU>)X<(h+PyUHNfw}Sd^j{Nyub=RHJysv( zpMZJ=XlDG&`o25$7^ZB!@K?Nn8pH~Pz`dz{@q(qBI>>Z tN=$-X{qW0wYrn-ie#gfregpaKfBmV+GY@J@`rij-5KmV>mvv4FO#mSwvhe@_ From 948be7cac2ce039eb50713607ffa7de8b7dec4fa Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Sun, 6 Jun 2021 09:39:07 +1000 Subject: [PATCH 086/176] Move drawing simplify away from grid dependent scaling --- src/components/map/MapDrawing.js | 3 +-- src/components/map/MapFog.js | 3 +-- src/helpers/drawing.js | 11 +++-------- 3 files changed, 5 insertions(+), 12 deletions(-) diff --git a/src/components/map/MapDrawing.js b/src/components/map/MapDrawing.js index 54e1234..ccce9fd 100644 --- a/src/components/map/MapDrawing.js +++ b/src/components/map/MapDrawing.js @@ -119,8 +119,7 @@ function MapDrawing({ } const simplified = simplifyPoints( [...prevPoints, brushPosition], - gridCellNormalizedSize, - stageScale + 1 / 1000 / stageScale ); return { ...prevShape, diff --git a/src/components/map/MapFog.js b/src/components/map/MapFog.js index 8c97149..3e94081 100644 --- a/src/components/map/MapFog.js +++ b/src/components/map/MapFog.js @@ -176,8 +176,7 @@ function MapFog({ } const simplified = simplifyPoints( [...prevPoints, brushPosition], - gridCellNormalizedSize, - stageScale / 4 + 1 / 1000 / stageScale ); return { ...prevShape, diff --git a/src/helpers/drawing.js b/src/helpers/drawing.js index 4491d61..00966a3 100644 --- a/src/helpers/drawing.js +++ b/src/helpers/drawing.js @@ -192,18 +192,13 @@ export function getUpdatedShapeData( } } -const defaultSimplifySize = 1 / 100; /** * Simplify points to a grid size * @param {Vector2[]} points - * @param {Vector2} gridCellSize - * @param {number} scale + * @param {number} tolerance */ -export function simplifyPoints(points, gridCellSize, scale) { - return simplify( - points, - (Vector2.min(gridCellSize) * defaultSimplifySize) / scale - ); +export function simplifyPoints(points, tolerance) { + return simplify(points, tolerance); } /** From 9d9fd5b753e953e8e809829f0eacfb0b84e0a84e Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Sun, 6 Jun 2021 10:24:56 +1000 Subject: [PATCH 087/176] Allow editing of default map and tokens and add default label setting --- src/components/map/MapEditor.js | 46 ++++++++++++--------------- src/components/map/MapSettings.js | 16 +++++----- src/components/token/TokenSettings.js | 18 ++++++++--- src/components/token/TokenTiles.js | 5 +-- 4 files changed, 44 insertions(+), 41 deletions(-) diff --git a/src/components/map/MapEditor.js b/src/components/map/MapEditor.js index b042ab0..e9d66da 100644 --- a/src/components/map/MapEditor.js +++ b/src/components/map/MapEditor.js @@ -93,8 +93,6 @@ function MapEditor({ map, onSettingsChange }) { interactionEmitter: null, }; - const canEditGrid = map.type !== "default"; - const gridChanged = map.grid.inset.topLeft.x !== defaultInset.topLeft.x || map.grid.inset.topLeft.y !== defaultInset.topLeft.y || @@ -133,7 +131,7 @@ function MapEditor({ map, onSettingsChange }) { > - {showGridControls && canEditGrid && ( + {showGridControls && ( <> @@ -159,28 +157,26 @@ function MapEditor({ map, onSettingsChange }) { )} - {canEditGrid && ( - setShowGridControls(!showGridControls)} - bg="overlay" - sx={{ - borderRadius: "50%", - position: "absolute", - bottom: 0, - right: 0, - }} - m={2} - p="6px" - > - {showGridControls ? : } - - )} + setShowGridControls(!showGridControls)} + bg="overlay" + sx={{ + borderRadius: "50%", + position: "absolute", + bottom: 0, + right: 0, + }} + m={2} + p="6px" + > + {showGridControls ? : } + diff --git a/src/components/map/MapSettings.js b/src/components/map/MapSettings.js index 02a4c95..9eca0ba 100644 --- a/src/components/map/MapSettings.js +++ b/src/components/map/MapSettings.js @@ -149,7 +149,7 @@ function MapSettings({ name="gridX" value={`${(map && map.grid.size.x) || 0}`} onChange={handleGridSizeXChange} - disabled={mapEmpty || map.type === "default"} + disabled={mapEmpty} min={1} my={1} /> @@ -161,7 +161,7 @@ function MapSettings({ name="gridY" value={`${(map && map.grid.size.y) || 0}`} onChange={handleGridSizeYChange} - disabled={mapEmpty || map.type === "default"} + disabled={mapEmpty} min={1} my={1} /> @@ -173,7 +173,7 @@ function MapSettings({ name="name" value={(map && map.name) || ""} onChange={(e) => onSettingsChange("name", e.target.value)} - disabled={mapEmpty || map.type === "default"} + disabled={mapEmpty} my={1} /> @@ -188,7 +188,7 @@ function MapSettings({ onSettingsChange("name", e.target.value)} - disabled={tokenEmpty || token.type === "default"} + disabled={tokenEmpty} my={1} /> @@ -33,7 +33,7 @@ function TokenSettings({ token, onSettingsChange }) { !tokenEmpty && categorySettings.find((s) => s.value === token.defaultCategory) } - isDisabled={tokenEmpty || token.type === "default"} + isDisabled={tokenEmpty} onChange={(option) => onSettingsChange("defaultCategory", option.value) } @@ -41,7 +41,7 @@ function TokenSettings({ token, onSettingsChange }) { /> - + onSettingsChange("defaultSize", parseFloat(e.target.value)) } - disabled={tokenEmpty || token.type === "default"} + disabled={tokenEmpty} min={1} my={1} /> + + + onSettingsChange("defaultLabel", e.target.value)} + disabled={tokenEmpty} + my={1} + /> + ); } diff --git a/src/components/token/TokenTiles.js b/src/components/token/TokenTiles.js index 3b4e817..363eeb9 100644 --- a/src/components/token/TokenTiles.js +++ b/src/components/token/TokenTiles.js @@ -23,10 +23,7 @@ function TokenTiles({ tokens, onTokenEdit, subgroup }) { const token = tokens.find((token) => token.id === group.id); const isSelected = selectedGroupIds.includes(group.id); const canEdit = - isSelected && - token.type !== "default" && - selectMode === "single" && - selectedGroupIds.length === 1; + isSelected && selectMode === "single" && selectedGroupIds.length === 1; return ( Date: Sun, 6 Jun 2021 11:03:07 +1000 Subject: [PATCH 088/176] Fix custom token loading for players --- src/hooks/useNetworkedState.js | 3 ++- src/network/NetworkedMapAndTokens.js | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/hooks/useNetworkedState.js b/src/hooks/useNetworkedState.js index 7dbf356..99ef54d 100644 --- a/src/hooks/useNetworkedState.js +++ b/src/hooks/useNetworkedState.js @@ -1,4 +1,5 @@ import { useEffect, useState, useRef, useCallback } from "react"; +import cloneDeep from "lodash.clonedeep"; import useDebounce from "./useDebounce"; import { diff, applyChanges } from "../helpers/diff"; @@ -70,7 +71,7 @@ function useNetworkedState( } dirtyRef.current = false; forceUpdateRef.current = false; - lastSyncedStateRef.current = debouncedState; + lastSyncedStateRef.current = cloneDeep(debouncedState); } }, [ session.socket, diff --git a/src/network/NetworkedMapAndTokens.js b/src/network/NetworkedMapAndTokens.js index 1e209b9..6c258d1 100644 --- a/src/network/NetworkedMapAndTokens.js +++ b/src/network/NetworkedMapAndTokens.js @@ -91,15 +91,15 @@ function NetworkedMapAndTokens({ session }) { function addAssetsIfNeeded(assets) { setAssetManifest((prevManifest) => { if (prevManifest?.assets) { - let newManifset = { ...prevManifest }; + let newAssets = { ...prevManifest.assets }; for (let asset of assets) { const id = asset.id; - const exists = id in prevManifest.assets; + const exists = id in newAssets; if (!exists) { - newManifset[id] = asset; + newAssets[id] = asset; } } - return newManifset; + return { ...prevManifest, assets: newAssets }; } return prevManifest; }); From acaf580ac76a9d65fda0fd103b79df076fbec09c Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Sun, 6 Jun 2021 19:27:01 +1000 Subject: [PATCH 089/176] Add error toasts for image drag and more restrictive image mime type rejection --- src/components/ImageDrop.js | 25 +++++++++++++++++++++---- src/modals/SelectMapModal.js | 2 +- src/modals/SelectTokensModal.js | 2 +- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/components/ImageDrop.js b/src/components/ImageDrop.js index 4cbc74b..7ee9c74 100644 --- a/src/components/ImageDrop.js +++ b/src/components/ImageDrop.js @@ -1,7 +1,12 @@ import React, { useState } from "react"; import { Box, Flex, Text } from "theme-ui"; +import { useToasts } from "react-toast-notifications"; + +const supportFileTypes = ["image/jpeg", "image/gif", "image/png", "image/webp"]; function ImageDrop({ onDrop, dropText, children }) { + const { addToast } = useToasts(); + const [dragging, setDragging] = useState(false); function handleImageDragEnter(event) { event.preventDefault(); @@ -35,15 +40,27 @@ function ImageDrop({ onDrop, dropText, children }) { if (response.ok) { const file = await response.blob(); file.name = name; - imageFiles.push(file); + if (supportFileTypes.includes(file.type)) { + imageFiles.push(file); + } else { + addToast(`Unsupported file type for ${file.name}`); + } } - } catch {} + } catch (e) { + if (e.message === "Failed to fetch") { + addToast("Unable to import image: failed to fetch"); + } else { + addToast("Unable to import image"); + } + } } const files = event.dataTransfer.files; for (let file of files) { - if (file.type.startsWith("image")) { + if (supportFileTypes.includes(file.type)) { imageFiles.push(file); + } else { + addToast(`Unsupported file type for ${file.name}`); } } onDrop(imageFiles); @@ -75,7 +92,7 @@ function ImageDrop({ onDrop, dropText, children }) { onDrop={handleImageDrop} > - {dropText || "Drop image to upload"} + {dropText || "Drop image to import"} )} diff --git a/src/modals/SelectMapModal.js b/src/modals/SelectMapModal.js index 0fa9931..468444a 100644 --- a/src/modals/SelectMapModal.js +++ b/src/modals/SelectMapModal.js @@ -202,7 +202,7 @@ function SelectMapModal({ handleImagesUpload(event.target.files)} type="file" - accept="image/*" + accept="image/jpeg, image/gif, image/png, image/webp" style={{ display: "none" }} multiple ref={fileInputRef} diff --git a/src/modals/SelectTokensModal.js b/src/modals/SelectTokensModal.js index b379f6f..df27357 100644 --- a/src/modals/SelectTokensModal.js +++ b/src/modals/SelectTokensModal.js @@ -203,7 +203,7 @@ function SelectTokensModal({ isOpen, onRequestClose, onMapTokensStateCreate }) { handleImagesUpload(event.target.files)} type="file" - accept="image/*" + accept="image/jpeg, image/gif, image/png, image/webp" style={{ display: "none" }} ref={fileInputRef} multiple From 4e7ed1d625e9705cfd6833e68ba7955064304a0a Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Sun, 6 Jun 2021 19:27:23 +1000 Subject: [PATCH 090/176] Add better optional handling to db error handler --- src/contexts/DatabaseContext.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/contexts/DatabaseContext.js b/src/contexts/DatabaseContext.js index 40c5198..0ae94b1 100644 --- a/src/contexts/DatabaseContext.js +++ b/src/contexts/DatabaseContext.js @@ -58,18 +58,18 @@ export function DatabaseProvider({ children }) { function handleDatabaseError(event) { event.preventDefault(); - if (event.reason?.message.startsWith("QuotaExceededError")) { + if (event?.reason?.message?.startsWith("QuotaExceededError")) { setDatabaseError({ - name: event.reason.name, + name: event?.reason?.name, message: "Storage Quota Exceeded Please Clear Space and Try Again.", }); } else { setDatabaseError({ - name: event.reason.name, + name: event?.reason?.name, message: "Something went wrong, please refresh your browser.", }); } - console.error(event.reason); + console.error(event?.reason); } window.addEventListener("unhandledrejection", handleDatabaseError); From 98f7744c1feffc19fef07c6c9eba17ccfb748a2b Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Sun, 6 Jun 2021 21:04:46 +1000 Subject: [PATCH 091/176] Add a global image drop for dropping directly to the map screen --- src/components/Modal.js | 12 +++ src/components/file/GlobalImageDrop.js | 122 +++++++++++++++++++++++++ src/components/{ => file}/ImageDrop.js | 2 +- src/modals/ImageTypeModal.js | 34 +++++++ src/modals/SelectMapModal.js | 5 +- src/modals/SelectTokensModal.js | 5 +- src/routes/Game.js | 5 +- 7 files changed, 178 insertions(+), 7 deletions(-) create mode 100644 src/components/file/GlobalImageDrop.js rename src/components/{ => file}/ImageDrop.js (97%) create mode 100644 src/modals/ImageTypeModal.js diff --git a/src/components/Modal.js b/src/components/Modal.js index ae2fdcf..43db923 100644 --- a/src/components/Modal.js +++ b/src/components/Modal.js @@ -44,6 +44,18 @@ function StyledModal({ {content} )} + overlayElement={(props, content) => ( +
{ + // Prevent drag event from triggering with a modal open + e.preventDefault(); + e.stopPropagation(); + }} + {...props} + > + {content} +
+ )} {...props} > {children} diff --git a/src/components/file/GlobalImageDrop.js b/src/components/file/GlobalImageDrop.js new file mode 100644 index 0000000..a03578b --- /dev/null +++ b/src/components/file/GlobalImageDrop.js @@ -0,0 +1,122 @@ +import React, { useState, useRef } from "react"; +import { Box } from "theme-ui"; +import { useToasts } from "react-toast-notifications"; + +import ImageDrop from "./ImageDrop"; + +import LoadingOverlay from "../LoadingOverlay"; + +import ImageTypeModal from "../../modals/ImageTypeModal"; +import ConfirmModal from "../../modals/ConfirmModal"; + +import { createMapFromFile } from "../../helpers/map"; +import { createTokenFromFile } from "../../helpers/token"; + +import { useAuth } from "../../contexts/AuthContext"; +import { useMapData } from "../../contexts/MapDataContext"; +import { useTokenData } from "../../contexts/TokenDataContext"; +import { useAssets } from "../../contexts/AssetsContext"; + +function GlobalImageDrop({ children }) { + const { addToast } = useToasts(); + + const { userId } = useAuth(); + const { addMap } = useMapData(); + const { addToken } = useTokenData(); + const { addAssets } = useAssets(); + + const [isImageTypeModalOpen, setIsImageTypeModalOpen] = useState(false); + const [isLargeImageWarningModalOpen, setShowLargeImageWarning] = useState( + false + ); + const [isLoading, setIsLoading] = useState(false); + + const droppedImagesRef = useRef(); + + async function handleDrop(files) { + if (navigator.storage) { + // Attempt to enable persistant storage + await navigator.storage.persist(); + } + + droppedImagesRef.current = []; + for (let file of files) { + if (file.size > 5e7) { + addToast(`Unable to import image ${file.name} as it is over 50MB`); + } else { + droppedImagesRef.current.push(file); + } + } + + // Any file greater than 20MB + if (droppedImagesRef.current.some((file) => file.size > 2e7)) { + setShowLargeImageWarning(true); + return; + } + + setIsImageTypeModalOpen(true); + } + + function handleLargeImageWarningCancel() { + droppedImagesRef.current = undefined; + setShowLargeImageWarning(false); + } + + async function handleLargeImageWarningConfirm() { + setShowLargeImageWarning(false); + setIsImageTypeModalOpen(true); + } + + async function handleMaps() { + setIsImageTypeModalOpen(false); + setIsLoading(true); + for (let file of droppedImagesRef.current) { + const { map, assets } = await createMapFromFile(file, userId); + await addMap(map); + await addAssets(assets); + } + setIsLoading(false); + droppedImagesRef.current = undefined; + } + + async function handleTokens() { + setIsImageTypeModalOpen(false); + setIsLoading(true); + for (let file of droppedImagesRef.current) { + const { token, assets } = await createTokenFromFile(file, userId); + await addToken(token); + await addAssets(assets); + } + setIsLoading(false); + droppedImagesRef.current = undefined; + } + + function handleImageTypeClose() { + droppedImagesRef.current = undefined; + setIsImageTypeModalOpen(false); + } + + return ( + + {children} + 1} + /> + + {isLoading && } + + ); +} + +export default GlobalImageDrop; diff --git a/src/components/ImageDrop.js b/src/components/file/ImageDrop.js similarity index 97% rename from src/components/ImageDrop.js rename to src/components/file/ImageDrop.js index 7ee9c74..85bf8af 100644 --- a/src/components/ImageDrop.js +++ b/src/components/file/ImageDrop.js @@ -68,7 +68,7 @@ function ImageDrop({ onDrop, dropText, children }) { } return ( - + {children} {dragging && ( + + + + + + + + + ); +} + +export default ImageTypeModal; diff --git a/src/modals/SelectMapModal.js b/src/modals/SelectMapModal.js index 468444a..32a6532 100644 --- a/src/modals/SelectMapModal.js +++ b/src/modals/SelectMapModal.js @@ -7,9 +7,10 @@ import EditMapModal from "./EditMapModal"; import ConfirmModal from "./ConfirmModal"; import Modal from "../components/Modal"; -import ImageDrop from "../components/ImageDrop"; import LoadingOverlay from "../components/LoadingOverlay"; +import ImageDrop from "../components/file/ImageDrop"; + import MapTiles from "../components/map/MapTiles"; import MapEditBar from "../components/map/MapEditBar"; import SelectMapSelectButton from "../components/map/SelectMapSelectButton"; @@ -198,7 +199,7 @@ function SelectMapModal({ onRequestClose={handleClose} style={{ maxWidth: layout.modalSize, width: "calc(100% - 16px)" }} > - + handleImagesUpload(event.target.files)} type="file" diff --git a/src/modals/SelectTokensModal.js b/src/modals/SelectTokensModal.js index df27357..fb0a90b 100644 --- a/src/modals/SelectTokensModal.js +++ b/src/modals/SelectTokensModal.js @@ -7,9 +7,10 @@ import EditTokenModal from "./EditTokenModal"; import ConfirmModal from "./ConfirmModal"; import Modal from "../components/Modal"; -import ImageDrop from "../components/ImageDrop"; import LoadingOverlay from "../components/LoadingOverlay"; +import ImageDrop from "../components/file/ImageDrop"; + import TokenTiles from "../components/token/TokenTiles"; import TokenEditBar from "../components/token/TokenEditBar"; @@ -199,7 +200,7 @@ function SelectTokensModal({ isOpen, onRequestClose, onMapTokensStateCreate }) { onRequestClose={onRequestClose} style={{ maxWidth: layout.modalSize, width: "calc(100% - 16px)" }} > - + handleImagesUpload(event.target.files)} type="file" diff --git a/src/routes/Game.js b/src/routes/Game.js index aa324c2..113ffae 100644 --- a/src/routes/Game.js +++ b/src/routes/Game.js @@ -8,6 +8,7 @@ import OfflineBanner from "../components/banner/OfflineBanner"; import LoadingOverlay from "../components/LoadingOverlay"; import Link from "../components/Link"; import MapLoadingOverlay from "../components/map/MapLoadingOverlay"; +import GlobalImageDrop from "../components/file/GlobalImageDrop"; import AuthModal from "../modals/AuthModal"; import GameExpiredModal from "../modals/GameExpiredModal"; @@ -114,7 +115,7 @@ function Game() { - + - + setPeerError(null)} From eb9afcc66a2291a7c7b2538d0a4cbf257690e7b8 Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Mon, 7 Jun 2021 21:08:14 +1000 Subject: [PATCH 092/176] Refactor image drop and to a hook and move global image drop --- .../{file => image}/GlobalImageDrop.js | 102 +++++++++++++----- src/components/image/ImageDrop.js | 37 +++++++ .../ImageDrop.js => hooks/useImageDrop.js} | 62 ++++------- src/modals/ImageTypeModal.js | 34 ------ src/modals/SelectMapModal.js | 2 +- src/modals/SelectTokensModal.js | 2 +- src/network/NetworkedMapAndTokens.js | 6 +- src/routes/Game.js | 23 ++-- 8 files changed, 153 insertions(+), 115 deletions(-) rename src/components/{file => image}/GlobalImageDrop.js (56%) create mode 100644 src/components/image/ImageDrop.js rename src/{components/file/ImageDrop.js => hooks/useImageDrop.js} (58%) delete mode 100644 src/modals/ImageTypeModal.js diff --git a/src/components/file/GlobalImageDrop.js b/src/components/image/GlobalImageDrop.js similarity index 56% rename from src/components/file/GlobalImageDrop.js rename to src/components/image/GlobalImageDrop.js index a03578b..5287b72 100644 --- a/src/components/file/GlobalImageDrop.js +++ b/src/components/image/GlobalImageDrop.js @@ -1,12 +1,9 @@ import React, { useState, useRef } from "react"; -import { Box } from "theme-ui"; +import { Flex, Text } from "theme-ui"; import { useToasts } from "react-toast-notifications"; -import ImageDrop from "./ImageDrop"; - import LoadingOverlay from "../LoadingOverlay"; -import ImageTypeModal from "../../modals/ImageTypeModal"; import ConfirmModal from "../../modals/ConfirmModal"; import { createMapFromFile } from "../../helpers/map"; @@ -17,7 +14,9 @@ import { useMapData } from "../../contexts/MapDataContext"; import { useTokenData } from "../../contexts/TokenDataContext"; import { useAssets } from "../../contexts/AssetsContext"; -function GlobalImageDrop({ children }) { +import useImageDrop from "../../hooks/useImageDrop"; + +function GlobalImageDrop({ children, onMapTokensStateCreate }) { const { addToast } = useToasts(); const { userId } = useAuth(); @@ -25,20 +24,24 @@ function GlobalImageDrop({ children }) { const { addToken } = useTokenData(); const { addAssets } = useAssets(); - const [isImageTypeModalOpen, setIsImageTypeModalOpen] = useState(false); const [isLargeImageWarningModalOpen, setShowLargeImageWarning] = useState( false ); const [isLoading, setIsLoading] = useState(false); 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) { // Attempt to enable persistant storage await navigator.storage.persist(); } + dropPositionRef.current = dropPosition; + droppedImagesRef.current = []; for (let file of files) { if (file.size > 5e7) { @@ -54,7 +57,11 @@ function GlobalImageDrop({ children }) { return; } - setIsImageTypeModalOpen(true); + if (droppingType === "maps") { + await handleMaps(); + } else { + await handleTokens(); + } } function handleLargeImageWarningCancel() { @@ -64,11 +71,14 @@ function GlobalImageDrop({ children }) { async function handleLargeImageWarningConfirm() { setShowLargeImageWarning(false); - setIsImageTypeModalOpen(true); + if (droppingType === "maps") { + await handleMaps(); + } else { + await handleTokens(); + } } async function handleMaps() { - setIsImageTypeModalOpen(false); setIsLoading(true); for (let file of droppedImagesRef.current) { const { map, assets } = await createMapFromFile(file, userId); @@ -80,7 +90,6 @@ function GlobalImageDrop({ children }) { } async function handleTokens() { - setIsImageTypeModalOpen(false); setIsLoading(true); for (let file of droppedImagesRef.current) { const { token, assets } = await createTokenFromFile(file, userId); @@ -91,21 +100,66 @@ function GlobalImageDrop({ children }) { droppedImagesRef.current = undefined; } - function handleImageTypeClose() { - droppedImagesRef.current = undefined; - setIsImageTypeModalOpen(false); + function handleMapsOver() { + setDroppingType("maps"); } + function handleTokensOver() { + setDroppingType("tokens"); + } + + const { dragging, containerListeners, overlayListeners } = useImageDrop( + handleDrop + ); + return ( - - {children} - 1} - /> + + {children} + {dragging && ( + + + + {"Drop image to import as a map"} + + + + + {"Drop image to import as a token"} + + + + )} {isLoading && } - + ); } diff --git a/src/components/image/ImageDrop.js b/src/components/image/ImageDrop.js new file mode 100644 index 0000000..f93aa97 --- /dev/null +++ b/src/components/image/ImageDrop.js @@ -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 ( + + {children} + {dragging && ( + + + {dropText || "Drop image to import"} + + + )} + + ); +} + +export default ImageDrop; diff --git a/src/components/file/ImageDrop.js b/src/hooks/useImageDrop.js similarity index 58% rename from src/components/file/ImageDrop.js rename to src/hooks/useImageDrop.js index 85bf8af..bf38044 100644 --- a/src/components/file/ImageDrop.js +++ b/src/hooks/useImageDrop.js @@ -1,26 +1,34 @@ -import React, { useState } from "react"; -import { Box, Flex, Text } from "theme-ui"; +import { useState } from "react"; 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 [dragging, setDragging] = useState(false); - function handleImageDragEnter(event) { + function onDragEnter(event) { event.preventDefault(); event.stopPropagation(); setDragging(true); } - function handleImageDragLeave(event) { + function onDragLeave(event) { event.preventDefault(); event.stopPropagation(); setDragging(false); } - async function handleImageDrop(event) { + function onDragOver(event) { + event.preventDefault(); + event.stopPropagation(); + event.dataTransfer.dropEffect = "copy"; + } + + async function onDrop(event) { event.preventDefault(); event.stopPropagation(); let imageFiles = []; @@ -63,41 +71,15 @@ function ImageDrop({ onDrop, dropText, children }) { addToast(`Unsupported file type for ${file.name}`); } } - onDrop(imageFiles); + const dropPosition = new Vector2(event.clientX, event.clientY); + onImageDrop(imageFiles, dropPosition); setDragging(false); } - return ( - - {children} - {dragging && ( - { - e.preventDefault(); - e.stopPropagation(); - e.dataTransfer.dropEffect = "copy"; - }} - onDrop={handleImageDrop} - > - - {dropText || "Drop image to import"} - - - )} - - ); + const containerListeners = { onDragEnter }; + const overlayListeners = { onDragLeave, onDragOver, onDrop }; + + return { dragging, containerListeners, overlayListeners }; } -export default ImageDrop; +export default useImageDrop; diff --git a/src/modals/ImageTypeModal.js b/src/modals/ImageTypeModal.js deleted file mode 100644 index 7726494..0000000 --- a/src/modals/ImageTypeModal.js +++ /dev/null @@ -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 ( - - - - - - - - - - ); -} - -export default ImageTypeModal; diff --git a/src/modals/SelectMapModal.js b/src/modals/SelectMapModal.js index 32a6532..dab60d0 100644 --- a/src/modals/SelectMapModal.js +++ b/src/modals/SelectMapModal.js @@ -9,7 +9,7 @@ import ConfirmModal from "./ConfirmModal"; import Modal from "../components/Modal"; import LoadingOverlay from "../components/LoadingOverlay"; -import ImageDrop from "../components/file/ImageDrop"; +import ImageDrop from "../components/image/ImageDrop"; import MapTiles from "../components/map/MapTiles"; import MapEditBar from "../components/map/MapEditBar"; diff --git a/src/modals/SelectTokensModal.js b/src/modals/SelectTokensModal.js index fb0a90b..b81d3eb 100644 --- a/src/modals/SelectTokensModal.js +++ b/src/modals/SelectTokensModal.js @@ -9,7 +9,7 @@ import ConfirmModal from "./ConfirmModal"; import Modal from "../components/Modal"; import LoadingOverlay from "../components/LoadingOverlay"; -import ImageDrop from "../components/file/ImageDrop"; +import ImageDrop from "../components/image/ImageDrop"; import TokenTiles from "../components/token/TokenTiles"; import TokenEditBar from "../components/token/TokenEditBar"; diff --git a/src/network/NetworkedMapAndTokens.js b/src/network/NetworkedMapAndTokens.js index 6c258d1..9f587aa 100644 --- a/src/network/NetworkedMapAndTokens.js +++ b/src/network/NetworkedMapAndTokens.js @@ -20,6 +20,8 @@ import Session from "./Session"; import Map from "../components/map/Map"; import TokenBar from "../components/token/TokenBar"; +import GlobalImageDrop from "../components/image/GlobalImageDrop"; + const defaultMapActions = { mapDrawActions: [], mapDrawActionIndex: -1, @@ -457,7 +459,7 @@ function NetworkedMapAndTokens({ session }) { } return ( - <> + - + ); } diff --git a/src/routes/Game.js b/src/routes/Game.js index 113ffae..2e06da5 100644 --- a/src/routes/Game.js +++ b/src/routes/Game.js @@ -8,7 +8,6 @@ import OfflineBanner from "../components/banner/OfflineBanner"; import LoadingOverlay from "../components/LoadingOverlay"; import Link from "../components/Link"; import MapLoadingOverlay from "../components/map/MapLoadingOverlay"; -import GlobalImageDrop from "../components/file/GlobalImageDrop"; import AuthModal from "../modals/AuthModal"; import GameExpiredModal from "../modals/GameExpiredModal"; @@ -115,18 +114,16 @@ function Game() { - - - - - - + + + + setPeerError(null)} From 3ef61cac26dc5344fa4b24612ed4666ae1e8807b Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Mon, 7 Jun 2021 22:25:36 +1000 Subject: [PATCH 093/176] Styled global drop and allow for adding to map on drop --- src/components/image/GlobalImageDrop.js | 73 +++++++++++++++++++++---- src/components/image/ImageDrop.js | 2 +- src/network/NetworkedMapAndTokens.js | 5 +- 3 files changed, 66 insertions(+), 14 deletions(-) diff --git a/src/components/image/GlobalImageDrop.js b/src/components/image/GlobalImageDrop.js index 5287b72..24588fe 100644 --- a/src/components/image/GlobalImageDrop.js +++ b/src/components/image/GlobalImageDrop.js @@ -8,22 +8,30 @@ import ConfirmModal from "../../modals/ConfirmModal"; import { createMapFromFile } from "../../helpers/map"; import { createTokenFromFile } from "../../helpers/token"; +import { + createTokenState, + clientPositionToMapPosition, +} from "../../helpers/token"; +import Vector2 from "../../helpers/Vector2"; import { useAuth } from "../../contexts/AuthContext"; import { useMapData } from "../../contexts/MapDataContext"; import { useTokenData } from "../../contexts/TokenDataContext"; import { useAssets } from "../../contexts/AssetsContext"; +import { useMapStage } from "../../contexts/MapStageContext"; import useImageDrop from "../../hooks/useImageDrop"; -function GlobalImageDrop({ children, onMapTokensStateCreate }) { +function GlobalImageDrop({ children, onMapChange, onMapTokensStateCreate }) { const { addToast } = useToasts(); const { userId } = useAuth(); - const { addMap } = useMapData(); + const { addMap, getMapState } = useMapData(); const { addToken } = useTokenData(); const { addAssets } = useAssets(); + const mapStageRef = useMapStage(); + const [isLargeImageWarningModalOpen, setShowLargeImageWarning] = useState( false ); @@ -80,24 +88,57 @@ function GlobalImageDrop({ children, onMapTokensStateCreate }) { async function handleMaps() { setIsLoading(true); + let maps = []; for (let file of droppedImagesRef.current) { const { map, assets } = await createMapFromFile(file, userId); await addMap(map); await addAssets(assets); + maps.push(map); } + + // Change map if only 1 dropped + if (maps.length === 1) { + const mapState = await getMapState(maps[0].id); + onMapChange(maps[0], mapState); + } + setIsLoading(false); droppedImagesRef.current = undefined; } async function handleTokens() { setIsLoading(true); + // Keep track of tokens so we can add them to the map + let tokens = []; for (let file of droppedImagesRef.current) { const { token, assets } = await createTokenFromFile(file, userId); await addToken(token); await addAssets(assets); + tokens.push(token); } setIsLoading(false); droppedImagesRef.current = undefined; + + const dropPosition = dropPositionRef.current; + const mapStage = mapStageRef.current; + if (mapStage && dropPosition) { + const mapPosition = clientPositionToMapPosition(mapStage, dropPosition); + if (mapPosition) { + let tokenStates = []; + let offset = new Vector2(0, 0); + for (let token of tokens) { + if (token) { + tokenStates.push( + createTokenState(token, Vector2.add(mapPosition, offset), userId) + ); + offset = Vector2.add(offset, 0.01); + } + } + if (tokenStates.length > 0) { + onMapTokensStateCreate(tokenStates); + } + } + } } function handleMapsOver() { @@ -131,31 +172,39 @@ function GlobalImageDrop({ children, onMapTokensStateCreate }) { - - {"Drop image to import as a map"} + + Drop as map - - {"Drop image to import as a token"} + + Drop as token diff --git a/src/components/image/ImageDrop.js b/src/components/image/ImageDrop.js index f93aa97..d46e14f 100644 --- a/src/components/image/ImageDrop.js +++ b/src/components/image/ImageDrop.js @@ -25,7 +25,7 @@ function ImageDrop({ onDrop, dropText, children }) { }} {...overlayListeners} > - + {dropText || "Drop image to import"} diff --git a/src/network/NetworkedMapAndTokens.js b/src/network/NetworkedMapAndTokens.js index 9f587aa..2c65a80 100644 --- a/src/network/NetworkedMapAndTokens.js +++ b/src/network/NetworkedMapAndTokens.js @@ -459,7 +459,10 @@ function NetworkedMapAndTokens({ session }) { } return ( - + Date: Mon, 7 Jun 2021 22:27:28 +1000 Subject: [PATCH 094/176] Fix global image drop zIndex --- src/components/image/GlobalImageDrop.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/image/GlobalImageDrop.js b/src/components/image/GlobalImageDrop.js index 24588fe..b1909e6 100644 --- a/src/components/image/GlobalImageDrop.js +++ b/src/components/image/GlobalImageDrop.js @@ -166,6 +166,7 @@ function GlobalImageDrop({ children, onMapChange, onMapTokensStateCreate }) { bottom: 0, cursor: "copy", flexDirection: "column", + zIndex: 100, }} {...overlayListeners} > From 57cce9346d013363d28d6b3889839e365f41814f Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Tue, 8 Jun 2021 23:46:20 +1000 Subject: [PATCH 095/176] Fix drag and drop add to map bugs with scrolled tile grid --- package.json | 4 +- src/components/tile/TilesAddDroppable.js | 54 -------------- src/components/tile/TilesContainer.js | 12 ++++ src/components/tile/TilesOverlay.js | 67 +++++++++-------- src/components/tile/TilesUngroupDroppable.js | 59 --------------- src/contexts/TileDragContext.js | 75 ++++++++++++++------ src/modals/SelectMapModal.js | 5 +- src/modals/SelectTokensModal.js | 5 +- yarn.lock | 17 +++-- 9 files changed, 115 insertions(+), 183 deletions(-) delete mode 100644 src/components/tile/TilesAddDroppable.js delete mode 100644 src/components/tile/TilesUngroupDroppable.js diff --git a/package.json b/package.json index 602eebd..91aa658 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,8 @@ "dependencies": { "@babylonjs/core": "^4.2.0", "@babylonjs/loaders": "^4.2.0", - "@dnd-kit/core": "3.0.2", - "@dnd-kit/sortable": "^3.0.1", + "@dnd-kit/core": "^3.0.4", + "@dnd-kit/sortable": "^3.1.0", "@mitchemmc/dexie-export-import": "^1.0.1", "@msgpack/msgpack": "^2.4.1", "@sentry/react": "^6.2.2", diff --git a/src/components/tile/TilesAddDroppable.js b/src/components/tile/TilesAddDroppable.js deleted file mode 100644 index 7bd6cde..0000000 --- a/src/components/tile/TilesAddDroppable.js +++ /dev/null @@ -1,54 +0,0 @@ -import React from "react"; -import { createPortal } from "react-dom"; - -import Droppable from "../drag/Droppable"; - -import { ADD_TO_MAP_ID_PREFIX } from "../../contexts/TileDragContext"; - -function TilesAddDroppable({ containerSize }) { - return createPortal( -
- - - - -
, - document.body - ); -} - -export default TilesAddDroppable; diff --git a/src/components/tile/TilesContainer.js b/src/components/tile/TilesContainer.js index 8703ee5..005c48d 100644 --- a/src/components/tile/TilesContainer.js +++ b/src/components/tile/TilesContainer.js @@ -3,9 +3,12 @@ import { Grid, useThemeUI } from "theme-ui"; import SimpleBar from "simplebar-react"; import { useGroup } from "../../contexts/GroupContext"; +import { ADD_TO_MAP_ID } from "../../contexts/TileDragContext"; import useResponsiveLayout from "../../hooks/useResponsiveLayout"; +import Droppable from "../drag/Droppable"; + function TilesContainer({ children }) { const { onGroupSelect } = useGroup(); @@ -28,10 +31,19 @@ function TilesContainer({ children }) { sx={{ borderRadius: "4px", overflow: "hidden", + position: "relative", }} gap={2} columns={`repeat(${layout.tileGridColumns}, 1fr)`} > + {children}
diff --git a/src/components/tile/TilesOverlay.js b/src/components/tile/TilesOverlay.js index 40215bf..0c7a81c 100644 --- a/src/components/tile/TilesOverlay.js +++ b/src/components/tile/TilesOverlay.js @@ -5,8 +5,7 @@ import ReactResizeDetector from "react-resize-detector"; import SimpleBar from "simplebar-react"; import { useGroup } from "../../contexts/GroupContext"; - -import TilesUngroupDroppable from "./TilesUngroupDroppable"; +import { UNGROUP_ID, ADD_TO_MAP_ID } from "../../contexts/TileDragContext"; import useResponsiveLayout from "../../hooks/useResponsiveLayout"; @@ -16,7 +15,9 @@ import GroupNameModal from "../../modals/GroupNameModal"; import { renameGroup } from "../../helpers/group"; -function TilesOverlay({ children }) { +import Droppable from "../drag/Droppable"; + +function TilesOverlay({ modalSize, children }) { const { groups, openGroupId, @@ -41,11 +42,6 @@ function TilesOverlay({ children }) { setContinerSize({ width: size, height: size }); } - const [overlaySize, setOverlaySize] = useState({ width: 0, height: 0 }); - function handleOverlayResize(width, height) { - setOverlaySize({ width, height }); - } - const [isGroupNameModalOpen, setIsGroupNameModalOpen] = useState(false); function handleGroupNameChange(name) { onGroupsChange(renameGroup(groups, openGroupId, name)); @@ -57,28 +53,16 @@ function TilesOverlay({ children }) { return ( <> {openGroupId && ( - )} - {openGroupId && ( - - - - )} + + {children} diff --git a/src/components/tile/TilesUngroupDroppable.js b/src/components/tile/TilesUngroupDroppable.js deleted file mode 100644 index 3363991..0000000 --- a/src/components/tile/TilesUngroupDroppable.js +++ /dev/null @@ -1,59 +0,0 @@ -import React from "react"; -import { createPortal } from "react-dom"; - -import Droppable from "../drag/Droppable"; - -import { UNGROUP_ID_PREFIX } from "../../contexts/TileDragContext"; - -function TilesUngroupDroppable({ outerContainerSize, innerContainerSize }) { - const width = (outerContainerSize.width - innerContainerSize.width) / 2; - const height = (outerContainerSize.height - innerContainerSize.height) / 2; - - return createPortal( -
- - - - -
, - document.body - ); -} - -export default TilesUngroupDroppable; diff --git a/src/contexts/TileDragContext.js b/src/contexts/TileDragContext.js index 95aa1a8..767a213 100644 --- a/src/contexts/TileDragContext.js +++ b/src/contexts/TileDragContext.js @@ -6,7 +6,6 @@ import { useSensor, useSensors, closestCenter, - rectIntersection, } from "@dnd-kit/core"; import { useGroup } from "./GroupContext"; @@ -18,8 +17,26 @@ const TileDragContext = React.createContext(); export const BASE_SORTABLE_ID = "__base__"; export const GROUP_SORTABLE_ID = "__group__"; export const GROUP_ID_PREFIX = "__group__"; -export const UNGROUP_ID_PREFIX = "__ungroup__"; -export const ADD_TO_MAP_ID_PREFIX = "__add__"; +export const UNGROUP_ID = "__ungroup__"; +export const ADD_TO_MAP_ID = "__add__"; + +// Custom rectIntersect that takes a point +function rectIntersection(rects, point) { + for (let rect of rects) { + const [id, bounds] = rect; + if ( + id && + bounds && + point.x > bounds.offsetLeft && + point.x < bounds.offsetLeft + bounds.width && + point.y > bounds.offsetTop && + point.y < bounds.offsetTop + bounds.height + ) { + return id; + } + } + return null; +} export function TileDragProvider({ onDragAdd, children }) { const { @@ -59,11 +76,11 @@ export function TileDragProvider({ onDragAdd, children }) { setOverId(over?.id); if (over) { if ( - over.id.startsWith(UNGROUP_ID_PREFIX) || + over.id.startsWith(UNGROUP_ID) || over.id.startsWith(GROUP_ID_PREFIX) ) { setDragCursor("alias"); - } else if (over.id.startsWith(ADD_TO_MAP_ID_PREFIX)) { + } else if (over.id.startsWith(ADD_TO_MAP_ID)) { setDragCursor(onDragAdd ? "copy" : "no-drop"); } else { setDragCursor("grabbing"); @@ -100,7 +117,7 @@ export function TileDragProvider({ onDragAdd, children }) { moveGroupsInto(activeGroups, overGroupIndex, selectedIndices), openGroupId ); - } else if (over.id.startsWith(UNGROUP_ID_PREFIX)) { + } else if (over.id === UNGROUP_ID) { onGroupSelect(); // Handle tile ungroup const newGroups = ungroup(groups, openGroupId, selectedIndices); @@ -109,7 +126,7 @@ export function TileDragProvider({ onDragAdd, children }) { onGroupClose(); } onGroupsChange(newGroups); - } else if (over.id.startsWith(ADD_TO_MAP_ID_PREFIX)) { + } else if (over.id === ADD_TO_MAP_ID) { onDragAdd && onDragAdd(selectedGroupIds, over.rect); } else if (!filter) { // Hanlde tile move only if we have no filter @@ -124,27 +141,39 @@ export function TileDragProvider({ onDragAdd, children }) { } function customCollisionDetection(rects, rect) { - // Handle group rects - if (openGroupId) { - const ungroupRects = rects.filter(([id]) => - id.startsWith(UNGROUP_ID_PREFIX) - ); - const intersectingGroupRect = rectIntersection(ungroupRects, rect); - if (intersectingGroupRect) { - return intersectingGroupRect; + // Calculate rect bottom taking into account any scroll offset + const rectBottom = rect.top + rect.bottom - rect.offsetTop; + const rectCenter = { + x: rect.left + rect.width / 2, + y: rectBottom - rect.height / 2, + }; + + // Find whether out rect center is outside our add to map rect + const addRect = rects.find(([id]) => id === ADD_TO_MAP_ID); + if (addRect) { + const intersectingAddRect = rectIntersection([addRect], rectCenter); + if (!intersectingAddRect) { + return ADD_TO_MAP_ID; } } - // Handle add to map rects - const addRects = rects.filter(([id]) => - id.startsWith(ADD_TO_MAP_ID_PREFIX) - ); - const intersectingAddRect = rectIntersection(addRects, rect); - if (intersectingAddRect) { - return intersectingAddRect; + // Find whether out rect center is outside our ungroup rect + if (openGroupId) { + const ungroupRect = rects.find(([id]) => id === UNGROUP_ID); + if (ungroupRect) { + const intersectingGroupRect = rectIntersection( + [ungroupRect], + rectCenter + ); + if (!intersectingGroupRect) { + return UNGROUP_ID; + } + } } - const otherRects = rects.filter(([id]) => id !== UNGROUP_ID_PREFIX); + const otherRects = rects.filter( + ([id]) => id !== ADD_TO_MAP_ID && id !== UNGROUP_ID + ); return closestCenter(otherRects, rect); } diff --git a/src/modals/SelectMapModal.js b/src/modals/SelectMapModal.js index dab60d0..c67e14d 100644 --- a/src/modals/SelectMapModal.js +++ b/src/modals/SelectMapModal.js @@ -17,7 +17,6 @@ import SelectMapSelectButton from "../components/map/SelectMapSelectButton"; import TilesOverlay from "../components/tile/TilesOverlay"; import TilesContainer from "../components/tile/TilesContainer"; -import TilesAddDroppable from "../components/tile/TilesAddDroppable"; import TileActionBar from "../components/tile/TileActionBar"; import { findGroup, getItemNames } from "../helpers/group"; @@ -231,7 +230,6 @@ function SelectMapModal({ - - - + - - - + Date: Tue, 8 Jun 2021 23:53:27 +1000 Subject: [PATCH 096/176] Remove tile global position collision work around --- src/contexts/TileDragContext.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/contexts/TileDragContext.js b/src/contexts/TileDragContext.js index 767a213..ab9106c 100644 --- a/src/contexts/TileDragContext.js +++ b/src/contexts/TileDragContext.js @@ -141,11 +141,9 @@ export function TileDragProvider({ onDragAdd, children }) { } function customCollisionDetection(rects, rect) { - // Calculate rect bottom taking into account any scroll offset - const rectBottom = rect.top + rect.bottom - rect.offsetTop; const rectCenter = { x: rect.left + rect.width / 2, - y: rectBottom - rect.height / 2, + y: rect.top + rect.height / 2, }; // Find whether out rect center is outside our add to map rect From 387ecd6fd70075aa8786ce5031d95d59d1dd367e Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Wed, 9 Jun 2021 08:32:58 +1000 Subject: [PATCH 097/176] Fix modal inset for safari --- src/components/Modal.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/Modal.js b/src/components/Modal.js index 43db923..664d7a9 100644 --- a/src/components/Modal.js +++ b/src/components/Modal.js @@ -34,7 +34,10 @@ function StyledModal({ }, content: { backgroundColor: theme.colors.background, - inset: "initial", + top: "initial", + left: "initial", + bottom: "initial", + right: "initial", maxHeight: "100%", ...style, }, From 1ec67c7a0ff3ef4033160811088895ec60b2078e Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Wed, 9 Jun 2021 08:35:53 +1000 Subject: [PATCH 098/176] Fix map tile add to map drag bug --- src/modals/SelectMapModal.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modals/SelectMapModal.js b/src/modals/SelectMapModal.js index c67e14d..8a2cdbc 100644 --- a/src/modals/SelectMapModal.js +++ b/src/modals/SelectMapModal.js @@ -170,11 +170,11 @@ function SelectMapModal({ const [canAddDraggedMap, setCanAddDraggedMap] = useState(false); function handleGroupsSelect(groupIds) { - if (!canAddDraggedMap && groupIds.length === 1) { + if (groupIds.length === 1) { // Only allow adding a map from dragging if there is a single group item selected const group = findGroup(mapGroups, groupIds[0]); setCanAddDraggedMap(group && group.type === "item"); - } else if (canAddDraggedMap) { + } else { setCanAddDraggedMap(false); } } From ee34c599da5ed99debefb776447dffdf3ad5c876 Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Wed, 9 Jun 2021 09:29:32 +1000 Subject: [PATCH 099/176] Add remove group items for maps and tokens --- src/components/map/MapEditBar.js | 2 +- src/components/token/TokenEditBar.js | 2 +- src/contexts/MapDataContext.js | 6 ++++++ src/contexts/TokenDataContext.js | 6 ++++++ src/helpers/group.js | 28 ++++++++++++++++++++++++++++ 5 files changed, 42 insertions(+), 2 deletions(-) diff --git a/src/components/map/MapEditBar.js b/src/components/map/MapEditBar.js index 2e6046c..0b5bb10 100644 --- a/src/components/map/MapEditBar.js +++ b/src/components/map/MapEditBar.js @@ -62,8 +62,8 @@ function MapEditBar({ currentMap, disabled, onMapChange, onMapReset, onLoad }) { setIsMapsRemoveModalOpen(false); const selectedMaps = getSelectedMaps(); const selectedMapIds = selectedMaps.map((map) => map.id); - await removeMaps(selectedMapIds); onGroupSelect(); + await removeMaps(selectedMapIds); // Removed the map from the map screen if needed if (currentMap && selectedMapIds.includes(currentMap.id)) { onMapChange(null, null); diff --git a/src/components/token/TokenEditBar.js b/src/components/token/TokenEditBar.js index f3b85e0..a2cafe7 100644 --- a/src/components/token/TokenEditBar.js +++ b/src/components/token/TokenEditBar.js @@ -44,8 +44,8 @@ function TokenEditBar({ disabled, onLoad }) { setIsTokensRemoveModalOpen(false); const selectedTokens = getSelectedTokens(); const selectedTokenIds = selectedTokens.map((token) => token.id); - await removeTokens(selectedTokenIds); onGroupSelect(); + await removeTokens(selectedTokenIds); onLoad(false); } diff --git a/src/contexts/MapDataContext.js b/src/contexts/MapDataContext.js index c5d3418..b110b18 100644 --- a/src/contexts/MapDataContext.js +++ b/src/contexts/MapDataContext.js @@ -5,6 +5,7 @@ import { useAuth } from "./AuthContext"; import { useDatabase } from "./DatabaseContext"; import { applyObservableChange } from "../helpers/dexie"; +import { removeGroupsItems } from "../helpers/group"; const MapDataContext = React.createContext(); @@ -105,6 +106,11 @@ export function MapDataProvider({ children }) { } } } + + const group = await database.table("groups").get("maps"); + let items = removeGroupsItems(group.items, ids); + await database.table("groups").update("maps", { items }); + await database.table("maps").bulkDelete(ids); await database.table("states").bulkDelete(ids); await database.table("assets").bulkDelete(assetIds); diff --git a/src/contexts/TokenDataContext.js b/src/contexts/TokenDataContext.js index e299a55..9771c31 100644 --- a/src/contexts/TokenDataContext.js +++ b/src/contexts/TokenDataContext.js @@ -5,6 +5,7 @@ import { useAuth } from "./AuthContext"; import { useDatabase } from "./DatabaseContext"; import { applyObservableChange } from "../helpers/dexie"; +import { removeGroupsItems } from "../helpers/group"; const TokenDataContext = React.createContext(); @@ -73,6 +74,11 @@ export function TokenDataProvider({ children }) { assetIds.push(token.thumbnail); } } + + const group = await database.table("groups").get("tokens"); + let items = removeGroupsItems(group.items, ids); + await database.table("groups").update("tokens", { items }); + await database.table("tokens").bulkDelete(ids); await database.table("assets").bulkDelete(assetIds); }, diff --git a/src/helpers/group.js b/src/helpers/group.js index a23313e..ba62aa4 100644 --- a/src/helpers/group.js +++ b/src/helpers/group.js @@ -236,3 +236,31 @@ export function renameGroup(groups, groupId, newName) { } return newGroups; } + +/** + * Remove items from groups including sub groups + * @param {Group[]} groups + * @param {string[]} itemIds + */ +export function removeGroupsItems(groups, itemIds) { + let newGroups = cloneDeep(groups); + + for (let i = newGroups.length - 1; i >= 0; i--) { + const group = newGroups[i]; + if (group.type === "item") { + if (itemIds.includes(group.id)) { + newGroups.splice(i, 1); + } + } else { + const items = group.items; + for (let j = items.length - 1; j >= 0; j--) { + const item = items[j]; + if (itemIds.includes(item.id)) { + newGroups[i].items.splice(j, 1); + } + } + } + } + + return newGroups; +} From 1d2696228bf95dbfe30302fdb289a2b8e8777769 Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Wed, 9 Jun 2021 09:47:39 +1000 Subject: [PATCH 100/176] Standardise settings select margins --- src/components/Select.js | 4 ++++ src/components/map/MapSettings.js | 12 +++++----- src/components/token/TokenSettings.js | 32 +++++++++++++-------------- 3 files changed, 25 insertions(+), 23 deletions(-) diff --git a/src/components/Select.js b/src/components/Select.js index 413df18..e56c418 100644 --- a/src/components/Select.js +++ b/src/components/Select.js @@ -53,6 +53,10 @@ function Select({ creatable, ...props }) { color: theme.colors.text, opacity: state.isDisabled ? 0.5 : 1, }), + container: (provided) => ({ + ...provided, + margin: "4px 0", + }), }} theme={(t) => ({ ...t, diff --git a/src/components/map/MapSettings.js b/src/components/map/MapSettings.js index 9eca0ba..fdfe0c4 100644 --- a/src/components/map/MapSettings.js +++ b/src/components/map/MapSettings.js @@ -185,8 +185,8 @@ function MapSettings({ sx={{ flexDirection: "column" }} > - - + + - + - + - - - - - onSettingsChange("defaultSize", parseFloat(e.target.value)) - } - disabled={tokenEmpty} - min={1} - my={1} - /> - - + + + + onSettingsChange("defaultSize", parseFloat(e.target.value)) + } + disabled={tokenEmpty} + min={1} + my={1} + /> + Date: Wed, 9 Jun 2021 09:51:00 +1000 Subject: [PATCH 101/176] Update TokenSettings.js --- src/components/token/TokenSettings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/token/TokenSettings.js b/src/components/token/TokenSettings.js index ac4a3fd..47e20df 100644 --- a/src/components/token/TokenSettings.js +++ b/src/components/token/TokenSettings.js @@ -54,7 +54,7 @@ function TokenSettings({ token, onSettingsChange }) { my={1} /> - + Date: Wed, 9 Jun 2021 10:33:47 +1000 Subject: [PATCH 102/176] Fix tile drag cancel with modal open --- src/components/token/TokenBar.js | 9 ++- src/contexts/TileDragContext.js | 124 ++++++++++++++++++------------- src/modals/SelectMapModal.js | 17 ++++- src/modals/SelectTokensModal.js | 17 ++++- 4 files changed, 109 insertions(+), 58 deletions(-) diff --git a/src/components/token/TokenBar.js b/src/components/token/TokenBar.js index e591b14..2a9b93e 100644 --- a/src/components/token/TokenBar.js +++ b/src/components/token/TokenBar.js @@ -6,6 +6,7 @@ import { DragOverlay, DndContext, PointerSensor, + KeyboardSensor, useSensor, useSensors, } from "@dnd-kit/core"; @@ -45,7 +46,8 @@ function TokenBar({ onMapTokensStateCreate }) { const pointerSensor = useSensor(PointerSensor, { activationConstraint: { distance: 5 }, }); - const sensors = useSensors(pointerSensor); + const keyboardSensor = useSensor(KeyboardSensor); + const sensors = useSensors(pointerSensor, keyboardSensor); function handleDragStart({ active }) { setDragId(active.id); @@ -93,6 +95,10 @@ function TokenBar({ onMapTokensStateCreate }) { } } + function handleDragCancel() { + setDragId(null); + } + function renderToken(group, draggable = true) { if (group.type === "item") { const token = tokensById[group.id]; @@ -132,6 +138,7 @@ function TokenBar({ onMapTokensStateCreate }) { diff --git a/src/contexts/TileDragContext.js b/src/contexts/TileDragContext.js index ab9106c..6990d4d 100644 --- a/src/contexts/TileDragContext.js +++ b/src/contexts/TileDragContext.js @@ -1,8 +1,8 @@ import React, { useState, useContext } from "react"; import { DndContext, - MouseSensor, - TouchSensor, + PointerSensor, + KeyboardSensor, useSensor, useSensors, closestCenter, @@ -38,7 +38,13 @@ function rectIntersection(rects, point) { return null; } -export function TileDragProvider({ onDragAdd, children }) { +export function TileDragProvider({ + onDragAdd, + onDragStart, + onDragEnd, + onDragCancel, + children, +}) { const { groups, activeGroups, @@ -50,29 +56,32 @@ export function TileDragProvider({ onDragAdd, children }) { filter, } = useGroup(); - const mouseSensor = useSensor(MouseSensor, { - activationConstraint: { delay: 250, tolerance: 5 }, - }); - const touchSensor = useSensor(TouchSensor, { + const pointerSensor = useSensor(PointerSensor, { activationConstraint: { delay: 250, tolerance: 5 }, }); + const keyboardSensor = useSensor(KeyboardSensor); - const sensors = useSensors(mouseSensor, touchSensor); + const sensors = useSensors(pointerSensor, keyboardSensor); const [dragId, setDragId] = useState(); const [overId, setOverId] = useState(); const [dragCursor, setDragCursor] = useState("pointer"); - function handleDragStart({ active, over }) { + function handleDragStart(event) { + const { active, over } = event; setDragId(active.id); setOverId(over?.id); if (!selectedGroupIds.includes(active.id)) { onGroupSelect(active.id); } setDragCursor("grabbing"); + + onDragStart && onDragStart(event); } - function handleDragOver({ over }) { + function handleDragOver(event) { + const { over } = event; + setOverId(over?.id); if (over) { if ( @@ -88,56 +97,64 @@ export function TileDragProvider({ onDragAdd, children }) { } } - function handleDragEnd({ active, over }) { + function handleDragEnd(event) { + const { active, over } = event; + setDragId(); setOverId(); setDragCursor("pointer"); - if (!active || !over || active.id === over.id) { - return; + if (active && over && active.id !== over.id) { + let selectedIndices = selectedGroupIds.map((groupId) => + activeGroups.findIndex((group) => group.id === groupId) + ); + // Maintain current group sorting + selectedIndices = selectedIndices.sort((a, b) => a - b); + + if (over.id.startsWith(GROUP_ID_PREFIX)) { + onGroupSelect(); + // Handle tile group + const overId = over.id.slice(9); + if (overId !== active.id) { + const overGroupIndex = activeGroups.findIndex( + (group) => group.id === overId + ); + onGroupsChange( + moveGroupsInto(activeGroups, overGroupIndex, selectedIndices), + openGroupId + ); + } + } else if (over.id === UNGROUP_ID) { + onGroupSelect(); + // Handle tile ungroup + const newGroups = ungroup(groups, openGroupId, selectedIndices); + // Close group if it was removed + if (!newGroups.find((group) => group.id === openGroupId)) { + onGroupClose(); + } + onGroupsChange(newGroups); + } else if (over.id === ADD_TO_MAP_ID) { + onDragAdd && onDragAdd(selectedGroupIds, over.rect); + } else if (!filter) { + // Hanlde tile move only if we have no filter + const overGroupIndex = activeGroups.findIndex( + (group) => group.id === over.id + ); + onGroupsChange( + moveGroups(activeGroups, overGroupIndex, selectedIndices), + openGroupId + ); + } } - let selectedIndices = selectedGroupIds.map((groupId) => - activeGroups.findIndex((group) => group.id === groupId) - ); - // Maintain current group sorting - selectedIndices = selectedIndices.sort((a, b) => a - b); + onDragEnd && onDragEnd(event); + } - if (over.id.startsWith(GROUP_ID_PREFIX)) { - onGroupSelect(); - // Handle tile group - const overId = over.id.slice(9); - if (overId === active.id) { - return; - } + function handleDragCancel(event) { + setDragId(); + setOverId(); + setDragCursor("pointer"); - const overGroupIndex = activeGroups.findIndex( - (group) => group.id === overId - ); - onGroupsChange( - moveGroupsInto(activeGroups, overGroupIndex, selectedIndices), - openGroupId - ); - } else if (over.id === UNGROUP_ID) { - onGroupSelect(); - // Handle tile ungroup - const newGroups = ungroup(groups, openGroupId, selectedIndices); - // Close group if it was removed - if (!newGroups.find((group) => group.id === openGroupId)) { - onGroupClose(); - } - onGroupsChange(newGroups); - } else if (over.id === ADD_TO_MAP_ID) { - onDragAdd && onDragAdd(selectedGroupIds, over.rect); - } else if (!filter) { - // Hanlde tile move only if we have no filter - const overGroupIndex = activeGroups.findIndex( - (group) => group.id === over.id - ); - onGroupsChange( - moveGroups(activeGroups, overGroupIndex, selectedIndices), - openGroupId - ); - } + onDragCancel && onDragCancel(event); } function customCollisionDetection(rects, rect) { @@ -183,6 +200,7 @@ export function TileDragProvider({ onDragAdd, children }) { onDragStart={handleDragStart} onDragEnd={handleDragEnd} onDragOver={handleDragOver} + onDragCancel={handleDragCancel} sensors={sensors} collisionDetection={customCollisionDetection} > diff --git a/src/modals/SelectMapModal.js b/src/modals/SelectMapModal.js index 8a2cdbc..36b5b16 100644 --- a/src/modals/SelectMapModal.js +++ b/src/modals/SelectMapModal.js @@ -168,6 +168,8 @@ function SelectMapModal({ const [editingMapId, setEditingMapId] = useState(); + const [isDraggingMap, setIsDraggingMap] = useState(false); + const [canAddDraggedMap, setCanAddDraggedMap] = useState(false); function handleGroupsSelect(groupIds) { if (groupIds.length === 1) { @@ -197,6 +199,7 @@ function SelectMapModal({ isOpen={isOpen} onRequestClose={handleClose} style={{ maxWidth: layout.modalSize, width: "calc(100% - 16px)" }} + shouldCloseOnEsc={!isDraggingMap} > - + setIsDraggingMap(true)} + onDragEnd={() => setIsDraggingMap(false)} + onDragCancel={() => setIsDraggingMap(false)} + > - + setIsDraggingMap(true)} + onDragEnd={() => setIsDraggingMap(false)} + onDragCancel={() => setIsDraggingMap(false)} + > - + setIsDraggingToken(true)} + onDragEnd={() => setIsDraggingToken(false)} + onDragCancel={() => setIsDraggingToken(false)} + > - + setIsDraggingToken(true)} + onDragEnd={() => setIsDraggingToken(false)} + onDragCancel={() => setIsDraggingToken(false)} + > Date: Wed, 9 Jun 2021 23:24:24 +1000 Subject: [PATCH 103/176] Fix tile droppable rendering on Safari --- src/components/tile/TilesContainer.js | 5 ++++- src/components/tile/TilesOverlay.js | 6 ++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/components/tile/TilesContainer.js b/src/components/tile/TilesContainer.js index 005c48d..97cd304 100644 --- a/src/components/tile/TilesContainer.js +++ b/src/components/tile/TilesContainer.js @@ -40,7 +40,10 @@ function TilesContainer({ children }) { id={ADD_TO_MAP_ID} style={{ position: "absolute", - inset: 0, + top: 0, + bottom: 0, + left: 0, + right: 0, zIndex: -1, }} /> diff --git a/src/components/tile/TilesOverlay.js b/src/components/tile/TilesOverlay.js index 0c7a81c..0577288 100644 --- a/src/components/tile/TilesOverlay.js +++ b/src/components/tile/TilesOverlay.js @@ -142,7 +142,6 @@ function TilesOverlay({ modalSize, children }) { style={{ position: "absolute", width: modalSize.width, - // height: modalSize.height, height: `calc(100% + ${ modalSize.height - containerSize.height + 48 }px)`, @@ -159,7 +158,10 @@ function TilesOverlay({ modalSize, children }) { id={UNGROUP_ID} style={{ position: "absolute", - inset: 0, + top: 0, + bottom: 0, + left: 0, + right: 0, zIndex: -1, }} /> From 21986231fa7720859a8d1845005e0f0ba545f4eb Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Wed, 9 Jun 2021 23:25:28 +1000 Subject: [PATCH 104/176] Add prevent select helper to stop highlighting UI elements on drag with touch devices --- src/components/token/TokenBar.js | 17 ++++++++++++++--- src/contexts/TileDragContext.js | 20 +++++++++++++++++--- src/hooks/usePreventSelect.js | 13 +++++++++++++ src/index.css | 4 ++++ 4 files changed, 48 insertions(+), 6 deletions(-) create mode 100644 src/hooks/usePreventSelect.js diff --git a/src/components/token/TokenBar.js b/src/components/token/TokenBar.js index 2a9b93e..7f42544 100644 --- a/src/components/token/TokenBar.js +++ b/src/components/token/TokenBar.js @@ -5,7 +5,8 @@ import SimpleBar from "simplebar-react"; import { DragOverlay, DndContext, - PointerSensor, + MouseSensor, + TouchSensor, KeyboardSensor, useSensor, useSensors, @@ -18,6 +19,7 @@ import SelectTokensButton from "./SelectTokensButton"; import Draggable from "../drag/Draggable"; import useSetting from "../../hooks/useSetting"; +import usePreventSelect from "../../hooks/usePreventSelect"; import { useTokenData } from "../../contexts/TokenDataContext"; import { useAuth } from "../../contexts/AuthContext"; @@ -43,14 +45,20 @@ function TokenBar({ onMapTokensStateCreate }) { // https://github.com/clauderic/dnd-kit/issues/238 const dragOverlayRef = useRef(); - const pointerSensor = useSensor(PointerSensor, { + const mouseSensor = useSensor(MouseSensor, { + activationConstraint: { distance: 5 }, + }); + const touchSensor = useSensor(TouchSensor, { activationConstraint: { distance: 5 }, }); const keyboardSensor = useSensor(KeyboardSensor); - const sensors = useSensors(pointerSensor, keyboardSensor); + const sensors = useSensors(mouseSensor, touchSensor, keyboardSensor); + + const [preventSelect, resumeSelect] = usePreventSelect(); function handleDragStart({ active }) { setDragId(active.id); + preventSelect(); } function handleDragEnd({ active }) { @@ -93,10 +101,13 @@ function TokenBar({ onMapTokensStateCreate }) { } } } + + resumeSelect(); } function handleDragCancel() { setDragId(null); + resumeSelect(); } function renderToken(group, draggable = true) { diff --git a/src/contexts/TileDragContext.js b/src/contexts/TileDragContext.js index 6990d4d..2e8c229 100644 --- a/src/contexts/TileDragContext.js +++ b/src/contexts/TileDragContext.js @@ -1,7 +1,8 @@ import React, { useState, useContext } from "react"; import { DndContext, - PointerSensor, + MouseSensor, + TouchSensor, KeyboardSensor, useSensor, useSensors, @@ -12,6 +13,8 @@ import { useGroup } from "./GroupContext"; import { moveGroupsInto, moveGroups, ungroup } from "../helpers/group"; +import usePreventSelect from "../hooks/usePreventSelect"; + const TileDragContext = React.createContext(); export const BASE_SORTABLE_ID = "__base__"; @@ -56,17 +59,22 @@ export function TileDragProvider({ filter, } = useGroup(); - const pointerSensor = useSensor(PointerSensor, { + const mouseSensor = useSensor(MouseSensor, { + activationConstraint: { delay: 250, tolerance: 5 }, + }); + const touchSensor = useSensor(TouchSensor, { activationConstraint: { delay: 250, tolerance: 5 }, }); const keyboardSensor = useSensor(KeyboardSensor); - const sensors = useSensors(pointerSensor, keyboardSensor); + const sensors = useSensors(mouseSensor, touchSensor, keyboardSensor); const [dragId, setDragId] = useState(); const [overId, setOverId] = useState(); const [dragCursor, setDragCursor] = useState("pointer"); + const [preventSelect, resumeSelect] = usePreventSelect(); + function handleDragStart(event) { const { active, over } = event; setDragId(active.id); @@ -77,6 +85,8 @@ export function TileDragProvider({ setDragCursor("grabbing"); onDragStart && onDragStart(event); + + preventSelect(); } function handleDragOver(event) { @@ -146,6 +156,8 @@ export function TileDragProvider({ } } + resumeSelect(); + onDragEnd && onDragEnd(event); } @@ -154,6 +166,8 @@ export function TileDragProvider({ setOverId(); setDragCursor("pointer"); + resumeSelect(); + onDragCancel && onDragCancel(event); } diff --git a/src/hooks/usePreventSelect.js b/src/hooks/usePreventSelect.js new file mode 100644 index 0000000..d20bb40 --- /dev/null +++ b/src/hooks/usePreventSelect.js @@ -0,0 +1,13 @@ +function usePreventSelect() { + function preventSelect() { + document.body.classList.add("no-select"); + } + + function resumeSelect() { + document.body.classList.remove("no-select"); + } + + return [preventSelect, resumeSelect]; +} + +export default usePreventSelect; diff --git a/src/index.css b/src/index.css index fb96b26..38bf6b3 100644 --- a/src/index.css +++ b/src/index.css @@ -13,3 +13,7 @@ html { input[type="checkbox"]:disabled ~ svg { opacity: 0.1; } + +.no-select div { + user-select: none; +} From 3d42b62b74a8056ec3ba5302a042309412132eab Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Thu, 10 Jun 2021 11:02:54 +1000 Subject: [PATCH 105/176] Add shape detection to token outline creation --- src/components/map/MapToken.js | 84 ++++++++++++++++++++++++++-------- src/helpers/Vector2.js | 60 ++++++++++++++++++++++++ src/helpers/image.js | 75 ++++++++++++++++++++++++++++++ src/helpers/token.js | 13 +----- src/tokens/index.js | 2 +- src/upgrade.js | 7 +-- 6 files changed, 204 insertions(+), 37 deletions(-) diff --git a/src/components/map/MapToken.js b/src/components/map/MapToken.js index e30b36c..532e5dd 100644 --- a/src/components/map/MapToken.js +++ b/src/components/map/MapToken.js @@ -246,34 +246,68 @@ function MapToken({ tokenName = tokenName + "-locked"; } - let outline = tokenState.outline; - if (Array.isArray(tokenState.outline)) { - outline = [...outline]; // Copy array so we can edit it imutably - for (let i = 0; i < outline.length; i += 2) { - // Scale outline to the token - outline[i] = (outline[i] / tokenState.width) * tokenWidth; - outline[i + 1] = (outline[i + 1] / tokenState.height) * tokenHeight; + function getScaledOutline() { + let outline = tokenState.outline; + if (outline.type === "rect") { + return { + ...outline, + x: (outline.x / tokenState.width) * tokenWidth, + y: (outline.y / tokenState.height) * tokenHeight, + width: (outline.width / tokenState.width) * tokenWidth, + height: (outline.height / tokenState.height) * tokenHeight, + }; + } else if (outline.type === "circle") { + return { + ...outline, + x: (outline.x / tokenState.width) * tokenWidth, + y: (outline.y / tokenState.height) * tokenHeight, + radius: (outline.radius / tokenState.width) * tokenWidth, + }; + } else { + let points = [...outline.points]; // Copy array so we can edit it imutably + for (let i = 0; i < points.length; i += 2) { + // Scale outline to the token + points[i] = (points[i] / tokenState.width) * tokenWidth; + points[i + 1] = (points[i + 1] / tokenState.height) * tokenHeight; + } + return { ...outline, points }; } } function renderOutline() { + const outline = getScaledOutline(); const sharedProps = { fill: colors.black, - width: tokenWidth, - height: tokenHeight, - x: 0, - y: 0, - rotation: tokenState.rotation, - offsetX: tokenWidth / 2, - offsetY: tokenHeight / 2, opacity: 0.8, }; - if (outline === "rect") { - return ; - } else if (outline === "circle") { - return ; + if (outline.type === "rect") { + return ( + + ); + } else if (outline.type === "circle") { + return ( + + ); } else { - return ; + return ( + + ); } } @@ -299,7 +333,17 @@ function MapToken({ id={tokenState.id} > {!tokenImage ? ( - renderOutline() + + {renderOutline()} + ) : ( 0) { + center = { x: center.x / points.length, y: center.y / points.length }; + } + return center; + } + + /** + * Determine whether given points are rectangular + * @param {Vector2[]} points + * @returns {boolean} + */ + static rectangular(points) { + if (points.length !== 4) { + return false; + } + // Check whether distance to the center is the same for all four points + const centroid = this.centroid(points); + let prevDist; + for (let point of points) { + const dist = this.distance(point, centroid); + if (prevDist && dist !== prevDist) { + return false; + } else { + prevDist = dist; + } + } + return true; + } + + /** + * Determine whether given points are circular + * @param {Vector2[]} points + * @returns {boolean} + */ + static circular(points, threshold = 0.1) { + const centroid = this.centroid(points); + let distances = []; + for (let point of points) { + distances.push(this.distance(point, centroid)); + } + if (distances.length > 0) { + const maxDistance = Math.max(...distances); + const minDistance = Math.min(...distances); + return maxDistance - minDistance < threshold; + } else { + return false; + } + } } export default Vector2; diff --git a/src/helpers/image.js b/src/helpers/image.js index 00f012c..64f9765 100644 --- a/src/helpers/image.js +++ b/src/helpers/image.js @@ -1,4 +1,7 @@ +import imageOutline from "image-outline"; + import blobToBuffer from "./blobToBuffer"; +import Vector2 from "./Vector2"; const lightnessDetectionOffset = 0.1; @@ -152,3 +155,75 @@ export async function createThumbnail(image, type, size = 300, quality = 0.5) { mime: type, }; } + +/** + * @typedef CircleOutline + * @property {"circle"} type + * @property {number} x - Center X of the circle + * @property {number} y - Center Y of the circle + * @property {number} radius + */ + +/** + * @typedef RectOutline + * @property {"rect"} type + * @property {number} width + * @property {number} height + * @property {number} x - Leftmost X position of the rect + * @property {number} y - Topmost Y position of the rect + */ + +/** + * @typedef PathOutline + * @property {"path"} type + * @property {number[]} points - Alternating x, y coordinates zipped together + */ + +/** + * @typedef {CircleOutline|RectOutline|PathOutline} Outline + */ + +/** + * Get the outline of an image + * @param {HTMLImageElement} image + * @returns {Outline} + */ +export function getImageOutline(image, maxPoints = 100) { + let baseOutline = imageOutline(image); + + if (baseOutline) { + if (baseOutline.length > maxPoints) { + baseOutline = Vector2.resample(baseOutline, maxPoints); + } + const bounds = Vector2.getBoundingBox(baseOutline); + if (Vector2.rectangular(baseOutline)) { + return { + type: "rect", + x: Math.round(bounds.min.x), + y: Math.round(bounds.min.y), + width: Math.round(bounds.width), + height: Math.round(bounds.height), + }; + } else if ( + Vector2.circular( + baseOutline, + Math.max(bounds.width / 10, bounds.height / 10) + ) + ) { + return { + type: "circle", + x: Math.round(bounds.center.x), + y: Math.round(bounds.center.y), + radius: Math.round(Math.min(bounds.width, bounds.height) / 2), + }; + } else { + // Flatten and round outline to save on storage size + const points = baseOutline + .map(({ x, y }) => [Math.round(x), Math.round(y)]) + .flat(); + return { type: "path", points }; + } + } else { + return { type: "rect", x: 0, y: 0, width: 1, height: 1 }; + } +} diff --git a/src/helpers/token.js b/src/helpers/token.js index 5c8a137..9319ad2 100644 --- a/src/helpers/token.js +++ b/src/helpers/token.js @@ -1,10 +1,8 @@ import { v4 as uuid } from "uuid"; import Case from "case"; -import imageOutline from "image-outline"; import blobToBuffer from "./blobToBuffer"; -import { createThumbnail } from "./image"; -import Vector2 from "./Vector2"; +import { createThumbnail, getImageOutline } from "./image"; export function createTokenState(token, position, userId) { let tokenState = { @@ -76,14 +74,7 @@ export async function createTokenFromFile(file, userId) { }; assets.push(fileAsset); - let outline = imageOutline(image); - if (outline.length > 100) { - outline = Vector2.resample(outline, 100); - } - // Flatten and round outline to save on storage size - outline = outline - .map(({ x, y }) => [Math.round(x), Math.round(y)]) - .flat(); + const outline = getImageOutline(image); const token = { name, diff --git a/src/tokens/index.js b/src/tokens/index.js index 7826052..baf607d 100644 --- a/src/tokens/index.js +++ b/src/tokens/index.js @@ -97,7 +97,7 @@ export function getDefaultTokens(userId) { hideInSidebar: false, width: 256, height: 256, - outline: "circle", + outline: { type: "circle", x: 128, y: 128, radius: 128 }, owner: userId, created: tokenKeys.length - i, lastModified: Date.now(), diff --git a/src/upgrade.js b/src/upgrade.js index 941e9e7..521c5c9 100644 --- a/src/upgrade.js +++ b/src/upgrade.js @@ -555,11 +555,8 @@ export const versions = { token.defaultCategory = token.category; delete token.category; token.defaultLabel = ""; - if (token.width === token.height) { - token.outline = "circle"; - } else { - token.outline = "rect"; - } + // TODO: move to outline detection + token.outline = { type: "circle", x: 256, y: 256, radius: 256 }; delete token.lastUsed; }); }); From 4b67071919218e8692f27bc92710b0edacc1fba3 Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Thu, 10 Jun 2021 11:16:47 +1000 Subject: [PATCH 106/176] Replace token image cache with token outline hit function Saves 70% perf on scroll then zoom with 100+ tokens --- src/components/map/MapToken.js | 93 ++++++++++------------------------ 1 file changed, 26 insertions(+), 67 deletions(-) diff --git a/src/components/map/MapToken.js b/src/components/map/MapToken.js index 532e5dd..2025ca9 100644 --- a/src/components/map/MapToken.js +++ b/src/components/map/MapToken.js @@ -1,10 +1,9 @@ -import React, { useState, useEffect, useRef } from "react"; +import React, { useState, useRef } from "react"; import { Image as KonvaImage, Group, Line, Rect, Circle } from "react-konva"; import { useSpring, animated } from "react-spring/konva"; import useImage from "use-image"; import Konva from "konva"; -import useDebounce from "../../hooks/useDebounce"; import usePrevious from "../../hooks/usePrevious"; import useGridSnapping from "../../hooks/useGridSnapping"; @@ -13,7 +12,6 @@ import { useSetPreventMapInteraction, useMapWidth, useMapHeight, - useDebouncedStageScale, } from "../../contexts/MapInteractionContext"; import { useGridCellPixelSize } from "../../contexts/GridContext"; import { useDataURL } from "../../contexts/AssetsContext"; @@ -37,7 +35,6 @@ function MapToken({ }) { const { userId } = useAuth(); - const stageScale = useDebouncedStageScale(); const mapWidth = useMapWidth(); const mapHeight = useMapHeight(); const setPreventMapInteraction = useSetPreventMapInteraction(); @@ -45,7 +42,7 @@ function MapToken({ const gridCellPixelSize = useGridCellPixelSize(); const tokenURL = useDataURL(tokenState, tokenSources); - const [tokenImage, tokenImageStatus] = useImage(tokenURL); + const [tokenImage] = useImage(tokenURL); const tokenAspectRatio = tokenState.width / tokenState.height; @@ -181,43 +178,7 @@ function MapToken({ const tokenWidth = minCellSize * tokenState.size; const tokenHeight = (minCellSize / tokenAspectRatio) * tokenState.size; - const debouncedStageScale = useDebounce(stageScale, 50); const imageRef = useRef(); - useEffect(() => { - const image = imageRef.current; - if (!image) { - return; - } - - const canvas = image.getCanvas(); - const pixelRatio = canvas.pixelRatio || 1; - - if ( - tokenImageStatus === "loaded" && - tokenWidth > 0 && - tokenHeight > 0 && - tokenImage - ) { - const maxImageSize = Math.max(tokenImage.width, tokenImage.height); - const maxTokenSize = Math.max(tokenWidth, tokenHeight); - // Constrain image buffer to original image size - const maxRatio = maxImageSize / maxTokenSize; - - image.cache({ - pixelRatio: Math.min( - Math.max(debouncedStageScale * pixelRatio, 1), - maxRatio - ), - }); - image.drawHitFromCache(); - } - }, [ - debouncedStageScale, - tokenWidth, - tokenHeight, - tokenImageStatus, - tokenImage, - ]); // Animate to new token positions if edited by others const tokenX = tokenState.x * mapWidth; @@ -278,7 +239,7 @@ function MapToken({ const outline = getScaledOutline(); const sharedProps = { fill: colors.black, - opacity: 0.8, + opacity: tokenImage ? 0 : 0.8, }; if (outline.type === "rect") { return ( @@ -332,31 +293,29 @@ function MapToken({ name={tokenName} id={tokenState.id} > - {!tokenImage ? ( - - {renderOutline()} - - ) : ( - - )} + + {renderOutline()} + + {}} + /> Date: Thu, 10 Jun 2021 15:06:11 +1000 Subject: [PATCH 107/176] Add token outline generation to db versions --- src/upgrade.js | 99 ++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 80 insertions(+), 19 deletions(-) diff --git a/src/upgrade.js b/src/upgrade.js index 521c5c9..1a7a800 100644 --- a/src/upgrade.js +++ b/src/upgrade.js @@ -6,7 +6,7 @@ import Case from "case"; import blobToBuffer from "./helpers/blobToBuffer"; import { getGridDefaultInset } from "./helpers/grid"; -import { createThumbnail } from "./helpers/image"; +import { createThumbnail, getImageOutline } from "./helpers/image"; import { AddShapeAction, EditShapeAction, @@ -419,8 +419,35 @@ export const versions = { }); }); }, - // v1.9.0 - Move map assets into new table + // v1.9.0 - Add outlines to tokens 23(v) { + v.stores({}).upgrade(async (tx) => { + const tokens = await Dexie.waitFor(tx.table("tokens").toArray()); + const tokenOutlines = await Dexie.waitFor( + Promise.all(tokens.map(createDataOutline)) + ); + + return tx + .table("tokens") + .toCollection() + .modify((token) => { + const tokenOutline = tokenOutlines.find((el) => el.id === token.id); + if (tokenOutline) { + token.outline = tokenOutline.outline; + } else { + token.outline = { + type: "rect", + width: token.width, + height: token.height, + x: 0, + y: 0, + }; + } + }); + }); + }, + // v1.9.0 - Move map assets into new table + 24(v) { v.stores({ assets: "id, owner" }).upgrade((tx) => { tx.table("maps").each((map) => { let assets = []; @@ -463,7 +490,7 @@ export const versions = { }); }, // v1.9.0 - Move token assets into new table - 24(v) { + 25(v) { v.stores({}).upgrade((tx) => { tx.table("tokens").each((token) => { let assets = []; @@ -490,7 +517,7 @@ export const versions = { }); }, // v1.9.0 - Create foreign keys for assets - 25(v) { + 26(v) { v.stores({}).upgrade((tx) => { tx.table("assets").each((asset) => { if (asset.prevType === "map") { @@ -515,7 +542,7 @@ export const versions = { }); }, // v1.9.0 - Remove asset migration helpers - 26(v) { + 27(v) { v.stores({}).upgrade((tx) => { tx.table("assets") .toCollection() @@ -529,7 +556,7 @@ export const versions = { }); }, // v1.9.0 - Remap map resolution assets - 27(v) { + 28(v) { v.stores({}).upgrade((tx) => { tx.table("maps") .toCollection() @@ -546,8 +573,8 @@ export const versions = { }); }); }, - // v1.9.0 - Move tokens to use more defaults and add token outline to tokens - 28(v) { + // v1.9.0 - Move tokens to use more defaults + 29(v) { v.stores({}).upgrade((tx) => { tx.table("tokens") .toCollection() @@ -555,14 +582,12 @@ export const versions = { token.defaultCategory = token.category; delete token.category; token.defaultLabel = ""; - // TODO: move to outline detection - token.outline = { type: "circle", x: 256, y: 256, radius: 256 }; delete token.lastUsed; }); }); }, // v1.9.0 - Move tokens to use more defaults and add token outline to token states - 29(v) { + 30(v) { v.stores({}).upgrade(async (tx) => { const tokens = await Dexie.waitFor(tx.table("tokens").toArray()); tx.table("states") @@ -584,7 +609,13 @@ export const versions = { state.tokens[id].category = "character"; state.tokens[id].type = "file"; state.tokens[id].file = ""; - state.tokens[id].outline = "rect"; + state.tokens[id].outline = { + type: "rect", + width: 256, + height: 256, + x: 0, + y: 0, + }; state.tokens[id].width = 256; state.tokens[id].height = 256; } @@ -594,7 +625,12 @@ export const versions = { state.tokens[id].key = Case.camel( state.tokens[id].tokenId.slice(10) ); - state.tokens[id].outline = "circle"; + state.tokens[id].outline = { + type: "circle", + x: 128, + y: 128, + radius: 128, + }; state.tokens[id].width = 256; state.tokens[id].height = 256; } @@ -603,7 +639,7 @@ export const versions = { }); }, // v1.9.0 - Remove maps not owned by user as cache is now done on the asset level - 30(v) { + 31(v) { v.stores({}).upgrade(async (tx) => { const userId = (await Dexie.waitFor(tx.table("user").get("userId"))) ?.value; @@ -613,7 +649,7 @@ export const versions = { }); }, // v1.9.0 - Remove tokens not owned by user as cache is now done on the asset level - 31(v) { + 32(v) { v.stores({}).upgrade(async (tx) => { const userId = (await Dexie.waitFor(tx.table("user").get("userId"))) ?.value; @@ -623,7 +659,7 @@ export const versions = { }); }, // v1.9.0 - Store default maps and tokens in db - 32(v) { + 33(v) { v.stores({}).upgrade(async (tx) => { const userId = (await Dexie.waitFor(tx.table("user").get("userId"))) ?.value; @@ -637,7 +673,7 @@ export const versions = { }); }, // v1.9.0 - Add new group table - 33(v) { + 34(v) { v.stores({ groups: "id" }).upgrade(async (tx) => { function groupItems(items) { let groups = []; @@ -672,7 +708,7 @@ export const versions = { }); }, // v1.9.0 - Remove map and token group in respective tables - 34(v) { + 35(v) { v.stores({}).upgrade((tx) => { tx.table("maps") .toCollection() @@ -688,7 +724,7 @@ export const versions = { }, }; -export const latestVersion = 34; +export const latestVersion = 35; /** * Load versions onto a database up to a specific version number @@ -764,3 +800,28 @@ async function createDataThumbnail(data) { 60000 * 10 // 10 minute timeout ); } + +async function createDataOutline(data) { + const url = URL.createObjectURL(new Blob([data.file])); + return await Dexie.waitFor( + new Promise((resolve) => { + let image = new Image(); + image.onload = async () => { + resolve({ id: data.id, outline: getImageOutline(image) }); + }; + image.onerror = () => { + resolve({ + id: data.id, + outline: { + type: "rect", + width: data.width, + height: data.height, + x: 0, + y: 0, + }, + }); + }; + image.src = url; + }) + ); +} From d996a81c96950b01d17a79b20d89bccef7b71d6f Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Thu, 10 Jun 2021 16:03:17 +1000 Subject: [PATCH 108/176] Fix asset db upgrade not having owners --- src/upgrade.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/upgrade.js b/src/upgrade.js index 1a7a800..9fd7720 100644 --- a/src/upgrade.js +++ b/src/upgrade.js @@ -453,6 +453,7 @@ export const versions = { let assets = []; assets.push({ id: uuid(), + owner: map.owner, file: map.file, width: map.width, height: map.height, @@ -465,6 +466,7 @@ export const versions = { const mapRes = map.resolutions[resolution]; assets.push({ id: uuid(), + owner: map.owner, file: mapRes.file, width: mapRes.width, height: mapRes.height, @@ -477,6 +479,7 @@ export const versions = { assets.push({ id: uuid(), + owner: map.owner, file: map.thumbnail.file, width: map.thumbnail.width, height: map.thumbnail.height, @@ -496,6 +499,7 @@ export const versions = { let assets = []; assets.push({ id: uuid(), + owner: token.owner, file: token.file, width: token.width, height: token.height, @@ -505,6 +509,7 @@ export const versions = { }); assets.push({ id: uuid(), + owner: token.owner, file: token.thumbnail.file, width: token.thumbnail.width, height: token.thumbnail.height, From 78461cb62fa50495e08796fb01c485cc41ab959e Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Thu, 10 Jun 2021 16:12:32 +1000 Subject: [PATCH 109/176] Fix bug with asset manifest not including other users custom assets --- src/network/NetworkedMapAndTokens.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/network/NetworkedMapAndTokens.js b/src/network/NetworkedMapAndTokens.js index 2c65a80..be0c8d2 100644 --- a/src/network/NetworkedMapAndTokens.js +++ b/src/network/NetworkedMapAndTokens.js @@ -69,13 +69,12 @@ function NetworkedMapAndTokens({ session }) { const { owner } = map; let processedTokens = new Set(); for (let tokenState of Object.values(mapState.tokens)) { - if ( - tokenState.file && - !processedTokens.has(tokenState.file) && - tokenState.owner === owner - ) { + if (tokenState.file && !processedTokens.has(tokenState.file)) { processedTokens.add(tokenState.file); - assets[tokenState.file] = { id: tokenState.file, owner }; + assets[tokenState.file] = { + id: tokenState.file, + owner: tokenState.owner, + }; } } if (map.type === "file") { From 5727bade368fa01d3cb752f8f71a8ae1d45f6a66 Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Thu, 10 Jun 2021 16:13:19 +1000 Subject: [PATCH 110/176] Fix bug with pre v1.9.0 import not changing token state ownership --- src/upgrade.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/upgrade.js b/src/upgrade.js index 9fd7720..0cddfd9 100644 --- a/src/upgrade.js +++ b/src/upgrade.js @@ -610,6 +610,7 @@ export const versions = { state.tokens[id].outline = token.outline; state.tokens[id].width = token.width; state.tokens[id].height = token.height; + state.tokens[id].owner = token.owner; } else { state.tokens[id].category = "character"; state.tokens[id].type = "file"; From b75db97c26b82a343e17e7614fd84ef40db94683 Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Thu, 10 Jun 2021 18:04:32 +1000 Subject: [PATCH 111/176] Add custom drag context wrapper to inject the overlay rect on drag end --- src/components/token/TokenBar.js | 34 ++++----------- src/contexts/DragContext.js | 75 ++++++++++++++++++++++++++++++++ src/contexts/TileDragContext.js | 13 +++--- src/modals/SelectTokensModal.js | 4 +- 4 files changed, 94 insertions(+), 32 deletions(-) create mode 100644 src/contexts/DragContext.js diff --git a/src/components/token/TokenBar.js b/src/components/token/TokenBar.js index 7f42544..ea4ef85 100644 --- a/src/components/token/TokenBar.js +++ b/src/components/token/TokenBar.js @@ -1,10 +1,9 @@ -import React, { useState, useRef } from "react"; +import React, { useState } from "react"; import { createPortal } from "react-dom"; import { Box, Flex } from "theme-ui"; import SimpleBar from "simplebar-react"; import { DragOverlay, - DndContext, MouseSensor, TouchSensor, KeyboardSensor, @@ -24,6 +23,7 @@ import usePreventSelect from "../../hooks/usePreventSelect"; import { useTokenData } from "../../contexts/TokenDataContext"; import { useAuth } from "../../contexts/AuthContext"; import { useMapStage } from "../../contexts/MapStageContext"; +import DragContext from "../../contexts/DragContext"; import { createTokenState, @@ -40,10 +40,6 @@ function TokenBar({ onMapTokensStateCreate }) { const [dragId, setDragId] = useState(); const mapStageRef = useMapStage(); - // Use a ref to the drag overlay to get it's position on dragEnd - // TODO: use active.rect when dnd-kit bug is fixed - // https://github.com/clauderic/dnd-kit/issues/238 - const dragOverlayRef = useRef(); const mouseSensor = useSensor(MouseSensor, { activationConstraint: { distance: 5 }, @@ -61,13 +57,12 @@ function TokenBar({ onMapTokensStateCreate }) { preventSelect(); } - function handleDragEnd({ active }) { + function handleDragEnd({ active, overlayNodeClientRect }) { setDragId(null); const mapStage = mapStageRef.current; - const dragOverlay = dragOverlayRef.current; - if (mapStage && dragOverlay) { - const dragRect = dragOverlay.getBoundingClientRect(); + if (mapStage) { + const dragRect = overlayNodeClientRect; const dragPosition = { x: dragRect.left + dragRect.width / 2, y: dragRect.top + dragRect.height / 2, @@ -146,7 +141,7 @@ function TokenBar({ onMapTokensStateCreate }) { } return ( - {createPortal( - - {dragId && ( -
- {renderToken(findGroup(tokenGroups, dragId), false)} -
- )} + + {dragId && renderToken(findGroup(tokenGroups, dragId), false)} , document.body )}
- + ); } diff --git a/src/contexts/DragContext.js b/src/contexts/DragContext.js new file mode 100644 index 0000000..2614b0f --- /dev/null +++ b/src/contexts/DragContext.js @@ -0,0 +1,75 @@ +// eslint-disable-next-line no-unused-vars +import React, { useRef, ReactNode } from "react"; +import { + DndContext, + useDndContext, + useDndMonitor, + // eslint-disable-next-line no-unused-vars + DragEndEvent, +} from "@dnd-kit/core"; + +/** + * Wrap a dnd-kit DndContext with a position monitor to get the + * active drag element on drag end + * TODO: use look into fixing this upstream + * Related: https://github.com/clauderic/dnd-kit/issues/238 + */ + +/** + * @typedef DragEndOverlayEvent + * @property {DOMRect} overlayNodeClientRect + * + * @typedef {DragEndEvent & DragEndOverlayEvent} DragEndWithOverlayProps + */ + +/** + * @callback DragEndWithOverlayEvent + * @param {DragEndWithOverlayProps} props + */ + +/** + * @typedef CustomDragProps + * @property {DragEndWithOverlayEvent=} onDragEnd + * @property {ReactNode} children + */ + +/** + * @param {CustomDragProps} props + */ +function DragPositionMonitor({ children, onDragEnd }) { + const { overlayNode } = useDndContext(); + + const overlayNodeClientRectRef = useRef(); + function handleDragMove() { + if (overlayNode?.nodeRef?.current) { + overlayNodeClientRectRef.current = overlayNode.nodeRef.current.getBoundingClientRect(); + } + } + + function handleDragEnd(props) { + onDragEnd && + onDragEnd({ + ...props, + overlayNodeClientRect: overlayNodeClientRectRef.current, + }); + } + useDndMonitor({ onDragEnd: handleDragEnd, onDragMove: handleDragMove }); + + return children; +} + +/** + * TODO: Import Props interface from dnd-kit with conversion to Typescript + * @param {CustomDragProps} props + */ +function DragContext({ children, onDragEnd, ...props }) { + return ( + + + {children} + + + ); +} + +export default DragContext; diff --git a/src/contexts/TileDragContext.js b/src/contexts/TileDragContext.js index 2e8c229..6f6644e 100644 --- a/src/contexts/TileDragContext.js +++ b/src/contexts/TileDragContext.js @@ -1,6 +1,5 @@ import React, { useState, useContext } from "react"; import { - DndContext, MouseSensor, TouchSensor, KeyboardSensor, @@ -9,6 +8,8 @@ import { closestCenter, } from "@dnd-kit/core"; +import DragContext from "./DragContext"; + import { useGroup } from "./GroupContext"; import { moveGroupsInto, moveGroups, ungroup } from "../helpers/group"; @@ -108,7 +109,7 @@ export function TileDragProvider({ } function handleDragEnd(event) { - const { active, over } = event; + const { active, over, overlayNodeClientRect } = event; setDragId(); setOverId(); @@ -143,7 +144,9 @@ export function TileDragProvider({ } onGroupsChange(newGroups); } else if (over.id === ADD_TO_MAP_ID) { - onDragAdd && onDragAdd(selectedGroupIds, over.rect); + onDragAdd && + overlayNodeClientRect && + onDragAdd(selectedGroupIds, overlayNodeClientRect); } else if (!filter) { // Hanlde tile move only if we have no filter const overGroupIndex = activeGroups.findIndex( @@ -210,7 +213,7 @@ export function TileDragProvider({ const value = { dragId, overId, dragCursor }; return ( - {children} - + ); } diff --git a/src/modals/SelectTokensModal.js b/src/modals/SelectTokensModal.js index 42606b4..421e0e9 100644 --- a/src/modals/SelectTokensModal.js +++ b/src/modals/SelectTokensModal.js @@ -144,8 +144,8 @@ function SelectTokensModal({ isOpen, onRequestClose, onMapTokensStateCreate }) { const mapStageRef = useMapStage(); function handleTokensAddToMap(groupIds, rect) { let clientPosition = new Vector2( - rect.width / 2 + rect.offsetLeft, - rect.height / 2 + rect.offsetTop + rect.width / 2 + rect.left, + rect.height / 2 + rect.top ); const mapStage = mapStageRef.current; if (!mapStage) { From fa8d08107943e3af9372b3002051e4e70c167bb6 Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Thu, 10 Jun 2021 20:07:40 +1000 Subject: [PATCH 112/176] Hide tile overlay immediately when closed --- src/components/tile/TilesOverlay.js | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/components/tile/TilesOverlay.js b/src/components/tile/TilesOverlay.js index 0577288..9d024b0 100644 --- a/src/components/tile/TilesOverlay.js +++ b/src/components/tile/TilesOverlay.js @@ -50,19 +50,21 @@ function TilesOverlay({ modalSize, children }) { const group = groups.find((group) => group.id === openGroupId); + if (!openGroupId) { + return null; + } + return ( <> - {openGroupId && ( - - )} + Date: Thu, 10 Jun 2021 20:08:11 +1000 Subject: [PATCH 113/176] Optimisation to tile rendering --- src/components/map/MapTiles.js | 6 +++--- src/components/token/TokenTiles.js | 8 +++----- src/contexts/MapDataContext.js | 11 +++++++++++ src/contexts/TokenDataContext.js | 13 +++++++++---- src/modals/SelectMapModal.js | 6 ++++-- src/modals/SelectTokensModal.js | 5 +++-- 6 files changed, 33 insertions(+), 16 deletions(-) diff --git a/src/components/map/MapTiles.js b/src/components/map/MapTiles.js index 294661d..3adeba6 100644 --- a/src/components/map/MapTiles.js +++ b/src/components/map/MapTiles.js @@ -9,7 +9,7 @@ import { getGroupItems } from "../../helpers/group"; import { useGroup } from "../../contexts/GroupContext"; -function MapTiles({ maps, onMapEdit, onMapSelect, subgroup }) { +function MapTiles({ mapsById, onMapEdit, onMapSelect, subgroup }) { const { selectedGroupIds, selectMode, @@ -19,7 +19,7 @@ function MapTiles({ maps, onMapEdit, onMapSelect, subgroup }) { function renderTile(group) { if (group.type === "item") { - const map = maps.find((map) => map.id === group.id); + const map = mapsById[group.id]; const isSelected = selectedGroupIds.includes(group.id); const canEdit = isSelected && selectMode === "single" && selectedGroupIds.length === 1; @@ -44,7 +44,7 @@ function MapTiles({ maps, onMapEdit, onMapSelect, subgroup }) { maps.find((map) => map.id === item.id))} + maps={items.map((item) => mapsById[item.id])} isSelected={isSelected} onSelect={onGroupSelect} onDoubleClick={() => canOpen && onGroupOpen(group.id)} diff --git a/src/components/token/TokenTiles.js b/src/components/token/TokenTiles.js index 363eeb9..02d5059 100644 --- a/src/components/token/TokenTiles.js +++ b/src/components/token/TokenTiles.js @@ -10,7 +10,7 @@ import { getGroupItems } from "../../helpers/group"; import { useGroup } from "../../contexts/GroupContext"; -function TokenTiles({ tokens, onTokenEdit, subgroup }) { +function TokenTiles({ tokensById, onTokenEdit, subgroup }) { const { selectedGroupIds, selectMode, @@ -20,7 +20,7 @@ function TokenTiles({ tokens, onTokenEdit, subgroup }) { function renderTile(group) { if (group.type === "item") { - const token = tokens.find((token) => token.id === group.id); + const token = tokensById[group.id]; const isSelected = selectedGroupIds.includes(group.id); const canEdit = isSelected && selectMode === "single" && selectedGroupIds.length === 1; @@ -48,9 +48,7 @@ function TokenTiles({ tokens, onTokenEdit, subgroup }) { - tokens.find((token) => token.id === item.id) - )} + tokens={items.map((item) => tokensById[item.id])} isSelected={isSelected} onSelect={onGroupSelect} onDoubleClick={() => canOpen && onGroupOpen(group.id)} diff --git a/src/contexts/MapDataContext.js b/src/contexts/MapDataContext.js index b110b18..f29c713 100644 --- a/src/contexts/MapDataContext.js +++ b/src/contexts/MapDataContext.js @@ -221,6 +221,16 @@ export function MapDataProvider({ children }) { }; }, [database, databaseStatus]); + const [mapsById, setMapsById] = useState({}); + useEffect(() => { + setMapsById( + maps.reduce((obj, map) => { + obj[map.id] = map; + return obj; + }, {}) + ); + }, [maps]); + const value = { maps, mapStates, @@ -234,6 +244,7 @@ export function MapDataProvider({ children }) { mapsLoading, getMapState, updateMapGroups, + mapsById, }; return ( {children} diff --git a/src/contexts/TokenDataContext.js b/src/contexts/TokenDataContext.js index 9771c31..a72722a 100644 --- a/src/contexts/TokenDataContext.js +++ b/src/contexts/TokenDataContext.js @@ -169,10 +169,15 @@ export function TokenDataProvider({ children }) { }; }, [database, databaseStatus]); - const tokensById = tokens.reduce((obj, token) => { - obj[token.id] = token; - return obj; - }, {}); + const [tokensById, setTokensById] = useState({}); + useEffect(() => { + setTokensById( + tokens.reduce((obj, token) => { + obj[token.id] = token; + return obj; + }, {}) + ); + }, [tokens]); const value = { tokens, diff --git a/src/modals/SelectMapModal.js b/src/modals/SelectMapModal.js index 36b5b16..be7dfb0 100644 --- a/src/modals/SelectMapModal.js +++ b/src/modals/SelectMapModal.js @@ -52,6 +52,7 @@ function SelectMapModal({ updateMapGroups, updateMap, updateMapState, + mapsById, } = useMapData(); const { addAssets } = useAssets(); @@ -214,6 +215,7 @@ function SelectMapModal({ handleWidth handleHeight onResize={handleModalResize} + refreshMode="debounce" > @@ -254,7 +256,7 @@ function SelectMapModal({ > @@ -256,7 +257,7 @@ function SelectTokensModal({ isOpen, onRequestClose, onMapTokensStateCreate }) { > From 183c7db1a615001f3a64c8c43a2dca48cff4152f Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Fri, 11 Jun 2021 12:20:22 +1000 Subject: [PATCH 114/176] Add group support to SelectDataModal --- src/modals/SelectDataModal.js | 220 +++++++++++++++++++++++----------- 1 file changed, 149 insertions(+), 71 deletions(-) diff --git a/src/modals/SelectDataModal.js b/src/modals/SelectDataModal.js index 5cd94ff..e0dd3fa 100644 --- a/src/modals/SelectDataModal.js +++ b/src/modals/SelectDataModal.js @@ -1,9 +1,10 @@ import React, { useEffect, useState } from "react"; -import { Box, Label, Flex, Button, Text, Checkbox, Divider } from "theme-ui"; +import { Box, Label, Flex, Button, Text, Checkbox } from "theme-ui"; import SimpleBar from "simplebar-react"; import Modal from "../components/Modal"; import LoadingOverlay from "../components/LoadingOverlay"; +import Divider from "../components/Divider"; import { getDatabase } from "../database"; @@ -17,8 +18,10 @@ function SelectDataModal({ filter, }) { const [maps, setMaps] = useState({}); + const [mapGroups, setMapGroups] = useState([]); const [tokensByMap, setTokensByMap] = useState({}); const [tokens, setTokens] = useState({}); + const [tokenGroups, setTokenGroups] = useState([]); const [isLoading, setIsLoading] = useState(false); const hasMaps = Object.values(maps).length > 0; @@ -36,7 +39,12 @@ function SelectDataModal({ .table("maps") .filter((map) => filter("maps", map, map.id)) .each((map) => { - loadedMaps[map.id] = { name: map.name, id: map.id, checked: true }; + loadedMaps[map.id] = { + name: map.name, + id: map.id, + type: map.type, + checked: true, + }; }); await db .table("states") @@ -56,17 +64,27 @@ function SelectDataModal({ loadedTokens[token.id] = { name: token.name, id: token.id, + type: token.type, checked: true, }; }); + + const mapGroup = await db.table("groups").get("maps"); + const tokenGroup = await db.table("groups").get("tokens"); + db.close(); setMaps(loadedMaps); + setMapGroups(mapGroup.items); setTokensByMap(loadedTokensByMap); + setTokenGroups(tokenGroup.items); setTokens(loadedTokens); setIsLoading(false); } else { setMaps({}); setTokens({}); + setTokenGroups([]); + setMapGroups([]); + setTokensByMap({}); } } loadData(); @@ -92,7 +110,7 @@ function SelectDataModal({ setTokens((prevTokens) => { let newTokens = { ...prevTokens }; for (let id in newTokens) { - if (id in tokensUsed) { + if (id in tokensUsed && newTokens[id].type !== "default") { newTokens[id].checked = true; } } @@ -106,11 +124,11 @@ function SelectDataModal({ onConfirm(checkedMaps, checkedTokens); } - function handleSelectMapsChanged(event) { + function handleMapsChanged(event, maps) { setMaps((prevMaps) => { let newMaps = { ...prevMaps }; - for (let id in newMaps) { - newMaps[id].checked = event.target.checked; + for (let map of maps) { + newMaps[map.id].checked = event.target.checked; } return newMaps; }); @@ -118,26 +136,17 @@ function SelectDataModal({ if (!event.target.checked && !tokensSelectChecked) { setTokens((prevTokens) => { let newTokens = { ...prevTokens }; + let tempUsedCount = { ...tokenUsedCount }; for (let id in newTokens) { - newTokens[id].checked = false; - } - return newTokens; - }); - } - } - - function handleMapChange(event, map) { - setMaps((prevMaps) => ({ - ...prevMaps, - [map.id]: { ...map, checked: event.target.checked }, - })); - // If all token select is unchecked then ensure tokens assosiated to this map are unchecked - if (!event.target.checked && !tokensSelectChecked) { - setTokens((prevTokens) => { - let newTokens = { ...prevTokens }; - for (let id in newTokens) { - if (tokensByMap[map.id].has(id) && tokenUsedCount[id] === 1) { - newTokens[id].checked = false; + for (let map of maps) { + if (tokensByMap[map.id].has(id)) { + if (tempUsedCount[id] > 1) { + tempUsedCount[id] -= 1; + } else if (tempUsedCount[id] === 1) { + tempUsedCount[id] = 0; + newTokens[id].checked = false; + } + } } } return newTokens; @@ -145,31 +154,126 @@ function SelectDataModal({ } } - function handleSelectTokensChange(event) { + function handleTokensChanged(event, tokens) { setTokens((prevTokens) => { let newTokens = { ...prevTokens }; - for (let id in newTokens) { - if (!(id in tokenUsedCount)) { - newTokens[id].checked = event.target.checked; + for (let token of tokens) { + if (!(token.id in tokenUsedCount) || token.type === "default") { + newTokens[token.id].checked = event.target.checked; } } return newTokens; }); } - function handleTokenChange(event, token) { - setTokens((prevTokens) => ({ - ...prevTokens, - [token.id]: { ...token, checked: event.target.checked }, - })); - } - // Some tokens are checked not by maps or all tokens are checked by maps const tokensSelectChecked = Object.values(tokens).some( (token) => !(token.id in tokenUsedCount) && token.checked ) || Object.values(tokens).every((token) => token.id in tokenUsedCount); + function renderGroupContainer(group, checked, renderItem, onGroupChange) { + return ( + + + + {group.items.map(renderItem)} + + + + + + ); + } + + function renderMapGroup(group) { + if (group.type === "item") { + const map = maps[group.id]; + return ( + + ); + } else { + return renderGroupContainer( + group, + group.items.some((item) => maps[item.id].checked), + renderMapGroup, + (e, group) => + handleMapsChanged( + e, + group.items.map((group) => maps[group.id]) + ) + ); + } + } + + function renderTokenGroup(group) { + if (group.type === "item") { + const token = tokens[group.id]; + return ( + + + {token.id in tokenUsedCount && token.type !== "default" && ( + + Token used in {tokenUsedCount[token.id]} selected map + {tokenUsedCount[token.id] > 1 && "s"} + + )} + + ); + } else { + const checked = + group.items.some( + (item) => !(item.id in tokenUsedCount) && tokens[item.id].checked + ) || group.items.every((item) => item.id in tokenUsedCount); + return renderGroupContainer( + group, + checked, + renderTokenGroup, + (e, group) => + handleTokensChanged( + e, + group.items.map((group) => tokens[group.id]) + ) + ); + } + } + return ( map.checked)} - onChange={handleSelectMapsChanged} + onChange={(e) => + handleMapsChanged(e, Object.values(maps)) + } /> Maps - {Object.values(maps).map((map) => ( - - ))} + {mapGroups.map(renderMapGroup)} )} - {hasMaps && hasTokens && } + {hasMaps && hasTokens && } {hasTokens && ( <> - {Object.values(tokens).map((token) => ( - - - {token.id in tokenUsedCount && ( - - Token used in {tokenUsedCount[token.id]} selected map - {tokenUsedCount[token.id] > 1 && "s"} - - )} - - ))} + {tokenGroups.map(renderTokenGroup)} )} From dab655935fbf162e3dee25d6195a23fdaac33527 Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Fri, 11 Jun 2021 15:21:08 +1000 Subject: [PATCH 115/176] Add group support for import/export data --- src/modals/ImportExportModal.js | 152 +++++++++++++++++++++++--------- src/modals/SelectDataModal.js | 27 +++++- src/workers/DatabaseWorker.js | 21 +++-- 3 files changed, 151 insertions(+), 49 deletions(-) diff --git a/src/modals/ImportExportModal.js b/src/modals/ImportExportModal.js index f0b265c..2d24255 100644 --- a/src/modals/ImportExportModal.js +++ b/src/modals/ImportExportModal.js @@ -122,7 +122,12 @@ function ImportExportModal({ isOpen, onRequestClose }) { setShowImportSelector(false); } - async function handleImportSelectorConfirm(checkedMaps, checkedTokens) { + async function handleImportSelectorConfirm( + checkedMaps, + checkedTokens, + checkedMapGroups, + checkedTokenGroups + ) { setIsLoading(true); backgroundTaskRunningRef.current = true; setShowImportSelector(false); @@ -135,6 +140,8 @@ function ImportExportModal({ isOpen, onRequestClose }) { let newTokenIds = {}; // Mapping of old asset ids to new asset ids let newAssetIds = {}; + // Mapping of old maps ids to new map ids + let newMapIds = {}; if (checkedTokens.length > 0) { const tokenIds = checkedTokens.map((token) => token.id); const tokensToAdd = await importDB.table("tokens").bulkGet(tokenIds); @@ -143,19 +150,24 @@ function ImportExportModal({ isOpen, onRequestClose }) { // Generate new ids const newId = uuid(); newTokenIds[token.id] = newId; - const newFileId = uuid(); - const newThumbnailId = uuid(); - newAssetIds[token.file] = newFileId; - newAssetIds[token.thumbnail] = newThumbnailId; - // Change ids and owner - newTokens.push({ - ...token, - id: newId, - owner: userId, - file: newFileId, - thumbnail: newThumbnailId, - }); + if (token.type === "default") { + newTokens.push({ ...token, id: newId, owner: userId }); + } else { + const newFileId = uuid(); + const newThumbnailId = uuid(); + newAssetIds[token.file] = newFileId; + newAssetIds[token.thumbnail] = newThumbnailId; + + // Change ids and owner + newTokens.push({ + ...token, + id: newId, + owner: userId, + file: newFileId, + thumbnail: newThumbnailId, + }); + } } await db.table("tokens").bulkAdd(newTokens); } @@ -173,35 +185,48 @@ function ImportExportModal({ isOpen, onRequestClose }) { state.tokens[tokenState.id].tokenId = newTokenIds[tokenState.tokenId]; } + // Change token state file asset id if (tokenState.type === "file" && tokenState.file in newAssetIds) { state.tokens[tokenState.id].file = newAssetIds[tokenState.file]; } + // Change token state owner if owned by the user of the map + if (tokenState.owner === map.owner) { + state.tokens[tokenState.id].owner = userId; + } } // Generate new ids const newId = uuid(); - const newFileId = uuid(); - const newThumbnailId = uuid(); - newAssetIds[map.file] = newFileId; - newAssetIds[map.thumbnail] = newThumbnailId; - const newResolutionIds = {}; - for (let res of Object.keys(map.resolutions)) { - newResolutionIds[res] = uuid(); - newAssetIds[map.resolutions[res]] = newResolutionIds[res]; + newMapIds[map.id] = newId; + + if (map.type === "default") { + newMaps.push({ ...map, id: newId, owner: userId }); + } else { + const newFileId = uuid(); + const newThumbnailId = uuid(); + newAssetIds[map.file] = newFileId; + newAssetIds[map.thumbnail] = newThumbnailId; + const newResolutionIds = {}; + for (let res of Object.keys(map.resolutions)) { + newResolutionIds[res] = uuid(); + newAssetIds[map.resolutions[res]] = newResolutionIds[res]; + } + // Change ids and owner + newMaps.push({ + ...map, + id: newId, + owner: userId, + file: newFileId, + thumbnail: newThumbnailId, + resolutions: newResolutionIds, + }); } - // Change ids and owner - newMaps.push({ - ...map, - id: newId, - owner: userId, - file: newFileId, - thumbnail: newThumbnailId, - resolutions: newResolutionIds, - }); + newStates.push({ ...state, mapId: newId }); } await db.table("maps").bulkAdd(newMaps); await db.table("states").bulkAdd(newStates); } + // Add assets with new ids const assetsToAdd = await importDB .table("assets") @@ -211,6 +236,53 @@ function ImportExportModal({ isOpen, onRequestClose }) { assets.push({ ...asset, id: newAssetIds[asset.id] }); } await db.table("assets").bulkAdd(assets); + + // Add map groups with new ids + if (checkedMapGroups.length > 0) { + const mapGroup = await db.table("groups").get("maps"); + let newMapGroups = []; + for (let group of checkedMapGroups) { + if (group.type === "item") { + newMapGroups.push({ ...group, id: newMapIds[group.id] }); + } else { + newMapGroups.push({ + ...group, + id: uuid(), + items: group.items.map((item) => ({ + ...item, + id: newMapIds[item.id], + })), + }); + } + } + await db + .table("groups") + .update("maps", { items: [...newMapGroups, ...mapGroup.items] }); + } + + // Add token groups with new ids + if (checkedTokenGroups.length > 0) { + const tokenGroup = await db.table("groups").get("tokens"); + let newTokenGroups = []; + for (let group of checkedTokenGroups) { + if (group.type === "item") { + newTokenGroups.push({ ...group, id: newTokenIds[group.id] }); + } else { + newTokenGroups.push({ + ...group, + id: uuid(), + items: group.items.map((item) => ({ + ...item, + id: newTokenIds[item.id], + })), + }); + } + } + await db.table("groups").update("tokens", { + items: [...newTokenGroups, ...tokenGroup.items], + }); + } + addSuccessToast("Imported", checkedMaps, checkedTokens); } catch (e) { console.error(e); @@ -223,18 +295,13 @@ function ImportExportModal({ isOpen, onRequestClose }) { backgroundTaskRunningRef.current = false; } - function exportSelectorFilter(table, value) { - // Only show owned maps and tokens - if (table === "maps" || table === "tokens") { - if (value.owner === userId) { - return true; - } - } - // Allow all states so tokens can be checked against maps - if (table === "states") { - return true; - } - return false; + function exportSelectorFilter(table) { + return ( + table === "maps" || + table === "tokens" || + table === "states" || + table === "groups" + ); } async function handleExportSelectorClose() { @@ -259,6 +326,7 @@ function ImportExportModal({ isOpen, onRequestClose }) { saveAs(blob, `${shortid.generate()}.owlbear`); addSuccessToast("Exported", checkedMaps, checkedTokens); } catch (e) { + console.error(e); setError(e); } setIsLoading(false); diff --git a/src/modals/SelectDataModal.js b/src/modals/SelectDataModal.js index e0dd3fa..bfdd306 100644 --- a/src/modals/SelectDataModal.js +++ b/src/modals/SelectDataModal.js @@ -118,10 +118,35 @@ function SelectDataModal({ }); }, [maps, tokensByMap]); + function getCheckedGroups(groups, data) { + let checkedGroups = []; + for (let group of groups) { + if (group.type === "item") { + if (data[group.id] && data[group.id].checked) { + checkedGroups.push(group); + } + } else { + let items = []; + for (let item of group.items) { + if (data[item.id] && data[item.id].checked) { + items.push(item); + } + } + if (items.length > 0) { + checkedGroups.push({ ...group, items }); + } + } + } + return checkedGroups; + } + function handleConfirm() { let checkedMaps = Object.values(maps).filter((map) => map.checked); let checkedTokens = Object.values(tokens).filter((token) => token.checked); - onConfirm(checkedMaps, checkedTokens); + let checkedMapGroups = getCheckedGroups(mapGroups, maps); + let checkedTokenGroups = getCheckedGroups(tokenGroups, tokens); + + onConfirm(checkedMaps, checkedTokens, checkedMapGroups, checkedTokenGroups); } function handleMapsChanged(event, maps) { diff --git a/src/workers/DatabaseWorker.js b/src/workers/DatabaseWorker.js index d49384f..8b53785 100644 --- a/src/workers/DatabaseWorker.js +++ b/src/workers/DatabaseWorker.js @@ -73,15 +73,19 @@ let service = { .toArray(); const assetIds = []; for (let map of maps) { - assetIds.push(map.file); - assetIds.push(map.thumbnail); - for (let res of Object.values(map.resolutions)) { - assetIds.push(res); + if (map.type === "file") { + assetIds.push(map.file); + assetIds.push(map.thumbnail); + for (let res of Object.values(map.resolutions)) { + assetIds.push(res); + } } } for (let token of tokens) { - assetIds.push(token.file); - assetIds.push(token.thumbnail); + if (token.type === "file") { + assetIds.push(token.file); + assetIds.push(token.thumbnail); + } } const filter = (table, value) => { @@ -97,6 +101,11 @@ let service = { if (table === "assets") { return assetIds.includes(value.id); } + // Always include groups table + if (table === "groups") { + return true; + } + return false; }; From 159627072d4d8f666042c3d8c2768fd2e6952183 Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Fri, 11 Jun 2021 15:33:14 +1000 Subject: [PATCH 116/176] Remove delete restriction for default maps and tokens --- src/components/map/MapEditBar.js | 15 ++------------- src/components/token/TokenEditBar.js | 11 +---------- 2 files changed, 3 insertions(+), 23 deletions(-) diff --git a/src/components/map/MapEditBar.js b/src/components/map/MapEditBar.js index 0b5bb10..c1ec6a4 100644 --- a/src/components/map/MapEditBar.js +++ b/src/components/map/MapEditBar.js @@ -16,7 +16,6 @@ import shortcuts from "../../shortcuts"; function MapEditBar({ currentMap, disabled, onMapChange, onMapReset, onLoad }) { const [hasMapState, setHasMapState] = useState(false); - const [hasSelectedDefaultMap, setHasSelectedDefaultMap] = useState(false); const { maps, mapStates, removeMaps, resetMap } = useMapData(); @@ -24,17 +23,12 @@ function MapEditBar({ currentMap, disabled, onMapChange, onMapReset, onLoad }) { useEffect(() => { const selectedGroups = groupsFromIds(selectedGroupIds, activeGroups); - const selectedMaps = itemsFromGroups(selectedGroups, maps); const selectedMapStates = itemsFromGroups( selectedGroups, mapStates, "mapId" ); - setHasSelectedDefaultMap( - selectedMaps.some((map) => map.type === "default") - ); - let _hasMapState = false; for (let state of selectedMapStates) { if ( @@ -49,7 +43,7 @@ function MapEditBar({ currentMap, disabled, onMapChange, onMapReset, onLoad }) { } setHasMapState(_hasMapState); - }, [selectedGroupIds, maps, mapStates, activeGroups]); + }, [selectedGroupIds, mapStates, activeGroups]); function getSelectedMaps() { const selectedGroups = groupsFromIds(selectedGroupIds, activeGroups); @@ -96,11 +90,7 @@ function MapEditBar({ currentMap, disabled, onMapChange, onMapReset, onLoad }) { } if (shortcuts.delete(event)) { const selectedMaps = getSelectedMaps(); - // Selected maps and none are default - if ( - selectedMaps.length > 0 && - !selectedMaps.some((map) => map.type === "default") - ) { + if (selectedMaps.length > 0) { setIsMapsResetModalOpen(false); setIsMapsRemoveModalOpen(true); } @@ -142,7 +132,6 @@ function MapEditBar({ currentMap, disabled, onMapChange, onMapReset, onLoad }) { aria-label="Remove Selected Map(s)" title="Remove Selected Map(s)" onClick={() => setIsMapsRemoveModalOpen(true)} - disabled={hasSelectedDefaultMap} > diff --git a/src/components/token/TokenEditBar.js b/src/components/token/TokenEditBar.js index a2cafe7..a759739 100644 --- a/src/components/token/TokenEditBar.js +++ b/src/components/token/TokenEditBar.js @@ -20,16 +20,12 @@ function TokenEditBar({ disabled, onLoad }) { const { activeGroups, selectedGroupIds, onGroupSelect } = useGroup(); - const [hasSelectedDefaultToken, setHasSelectedDefaultToken] = useState(false); const [allTokensVisible, setAllTokensVisisble] = useState(false); useEffect(() => { const selectedGroups = groupsFromIds(selectedGroupIds, activeGroups); const selectedTokens = itemsFromGroups(selectedGroups, tokens); - setHasSelectedDefaultToken( - selectedTokens.some((token) => token.type === "default") - ); setAllTokensVisisble(selectedTokens.every((token) => !token.hideInSidebar)); }, [selectedGroupIds, tokens, activeGroups]); @@ -64,11 +60,7 @@ function TokenEditBar({ disabled, onLoad }) { } if (shortcuts.delete(event)) { const selectedTokens = getSelectedTokens(); - // Selected tokens and none are default - if ( - selectedTokens.length > 0 && - !selectedTokens.some((token) => token.type === "default") - ) { + if (selectedTokens.length > 0) { // Ensure all other modals are closed setIsTokensRemoveModalOpen(true); } @@ -116,7 +108,6 @@ function TokenEditBar({ disabled, onLoad }) { aria-label="Remove Selected Token(s)" title="Remove Selected Token(s)" onClick={() => handleTokensRemove()} - disabled={hasSelectedDefaultToken} > From 52fad1171a030746993bcd9584d9476c9ea6cc7d Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Fri, 11 Jun 2021 15:38:23 +1000 Subject: [PATCH 117/176] Remove worker loading from map and token data contexts --- src/contexts/MapDataContext.js | 18 +++--------------- src/contexts/TokenDataContext.js | 17 +++-------------- 2 files changed, 6 insertions(+), 29 deletions(-) diff --git a/src/contexts/MapDataContext.js b/src/contexts/MapDataContext.js index f29c713..9b4e636 100644 --- a/src/contexts/MapDataContext.js +++ b/src/contexts/MapDataContext.js @@ -1,5 +1,4 @@ import React, { useEffect, useState, useContext, useCallback } from "react"; -import { decode } from "@msgpack/msgpack"; import { useAuth } from "./AuthContext"; import { useDatabase } from "./DatabaseContext"; @@ -19,7 +18,7 @@ const defaultMapState = { }; export function MapDataProvider({ children }) { - const { database, databaseStatus, worker } = useDatabase(); + const { database, databaseStatus } = useDatabase(); const { userId } = useAuth(); const [maps, setMaps] = useState([]); @@ -34,18 +33,7 @@ export function MapDataProvider({ children }) { } async function loadMaps() { - let storedMaps = []; - // Try to load maps with worker, fallback to database if failed - const packedMaps = await worker.loadData("maps"); - // let packedMaps; - if (packedMaps) { - storedMaps = decode(packedMaps); - } else { - console.warn("Unable to load maps with worker, loading may be slow"); - await database.table("maps").each((map) => { - storedMaps.push(map); - }); - } + const storedMaps = await database.table("maps").toArray(); setMaps(storedMaps); const storedStates = await database.table("states").toArray(); setMapStates(storedStates); @@ -56,7 +44,7 @@ export function MapDataProvider({ children }) { } loadMaps(); - }, [userId, database, databaseStatus, worker]); + }, [userId, database, databaseStatus]); const getMap = useCallback( async (mapId) => { diff --git a/src/contexts/TokenDataContext.js b/src/contexts/TokenDataContext.js index a72722a..9c98490 100644 --- a/src/contexts/TokenDataContext.js +++ b/src/contexts/TokenDataContext.js @@ -1,5 +1,4 @@ import React, { useEffect, useState, useContext, useCallback } from "react"; -import { decode } from "@msgpack/msgpack"; import { useAuth } from "./AuthContext"; import { useDatabase } from "./DatabaseContext"; @@ -10,7 +9,7 @@ import { removeGroupsItems } from "../helpers/group"; const TokenDataContext = React.createContext(); export function TokenDataProvider({ children }) { - const { database, databaseStatus, worker } = useDatabase(); + const { database, databaseStatus } = useDatabase(); const { userId } = useAuth(); const [tokens, setTokens] = useState([]); @@ -23,17 +22,7 @@ export function TokenDataProvider({ children }) { } async function loadTokens() { - let storedTokens = []; - // Try to load tokens with worker, fallback to database if failed - const packedTokens = await worker.loadData("tokens"); - if (packedTokens) { - storedTokens = decode(packedTokens); - } else { - console.warn("Unable to load tokens with worker, loading may be slow"); - await database.table("tokens").each((token) => { - storedTokens.push(token); - }); - } + const storedTokens = await database.table("tokens").toArray(); setTokens(storedTokens); const group = await database.table("groups").get("tokens"); const storedGroups = group.items; @@ -42,7 +31,7 @@ export function TokenDataProvider({ children }) { } loadTokens(); - }, [userId, database, databaseStatus, worker]); + }, [userId, database, databaseStatus]); const getToken = useCallback( async (tokenId) => { From bd3c868364c83751669b75322b483924e0fff733 Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Fri, 11 Jun 2021 15:47:16 +1000 Subject: [PATCH 118/176] Bump version to 1.9.0 (test) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 91aa658..71bab11 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "owlbear-rodeo", - "version": "1.8.1", + "version": "1.9.0 (test)", "private": true, "dependencies": { "@babylonjs/core": "^4.2.0", From 44aa4d694a372175cbd87b80898fbf7264de1c65 Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Fri, 11 Jun 2021 17:15:50 +1000 Subject: [PATCH 119/176] Add 1.9.0 Release Notes --- src/docs/releaseNotes/v1.9.0.md | 44 +++++++++++++++++++++++++++++++++ src/routes/ReleaseNotes.js | 10 ++++++-- 2 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 src/docs/releaseNotes/v1.9.0.md diff --git a/src/docs/releaseNotes/v1.9.0.md b/src/docs/releaseNotes/v1.9.0.md new file mode 100644 index 0000000..9d24208 --- /dev/null +++ b/src/docs/releaseNotes/v1.9.0.md @@ -0,0 +1,44 @@ +## Major Changes + +This release focuses on improving the workflow for managing assets as well as increasing performance of fog and tokens. This release also brings a lot of under the hood changes to how data is structured, stored and saved. + +### Drag and Drop Asset Management + +All assets (tokens and maps) now offer a lot more flexibility for organisation with a new drag and drop interface for rearrangement, grouping and adding to a game. + +To access these features simply long press on any asset you have selected. You can then drag to the edge of another asset to re-order the element, drag over another asset to create a new group or drag out of the select screen to add that asset to the game. + +With this feature we now also support the ability to drag and drop images from your computer directly into a game without the need to first add the asset into the appropriate asset import screen. To use this you can simply drag an image file into a game. Two new import boxes will then be shown where you can drag the images into the top box to import them as a map or the bottom box to import them as a token. When importing a single map this way the game will automatically switch to show this map in the shared scene. When importing tokens this way they will automatically be placed onto a shared map if one is shown. + +### Improved Group Support + +Grouped assets now have a more structured interface with a separate group view as well as full support for replicating token groups in the token sidebar. + +Once a group has been created you can open it by double clicking the group in the select screen or by single clicking in the token sidebar. + +Groups also support the new drag and drop interface which also includes dragging a token group into your game to add all tokens to a map at once. This is also supported with groups in the token sidebar. + +### Token Previews + +The generic token missing or loading image has been replaced by per-token loading previews. +These manifest as a transparent black shape that represents a simplified version of the token that is loading. + +This helps when using large tokens such as an initiative tracker which will no longer take over the screen as it loads. +This also helps greatly to optimise our token rendering. Specifically these simplified shapes are now used for our collision detection for detecting whether you have selected a token. Moving to this method means we no longer need to render a token multiple times to work out whether you are interacting with it. In our tests token rendering is now up to 70% faster when using over 100 tokens on screen. This change should also help fix token selection issues in browsers like Brave that modifier Canvas data. + +A downside to this method however is token selection will now be less accurate for transparent tokens with holes such as spell effects. We plan to keep an eye on this however to see whether this has adverse effects on real world usage. + +### New Rendering Technique for Fog Editing + +While in edit mode we now render fog much more efficiently. In testing we have seen just shy of a 70% decrease in rendering time when using 50+ fog shapes. While this will help with the usability of the tool on more complex maps, this should also fix issues with browsers like Firefox and Safari which would stop updating token movement if fog rendering was taking too long. + +## Minor Changes + +- The progress loading bar will now pool consecutive assets together to avoid showing each asset separately. +- All default maps and tokens are now fully customisable. This includes adjusting settings, import/export and even deleting. +- Modals have a new transition animation to match the new group UI. +- Cursors now better represent drag and drop actions. +- Tokens in the select token screen now show an indicator for whether they are hidden in the sidebar. +- Added a new default label setting to tokens. +- Fixed bug with the fog brush tool not working properly on maps with smaller grid sizes. +- Added better file type handling for image import screens with more informative error notifications. diff --git a/src/routes/ReleaseNotes.js b/src/routes/ReleaseNotes.js index da984ba..54633f1 100644 --- a/src/routes/ReleaseNotes.js +++ b/src/routes/ReleaseNotes.js @@ -26,6 +26,7 @@ const v162 = raw("../docs/releaseNotes/v1.6.2.md"); const v170 = raw("../docs/releaseNotes/v1.7.0.md"); const v180 = raw("../docs/releaseNotes/v1.8.0.md"); const v181 = raw("../docs/releaseNotes/v1.8.1.md"); +const v190 = raw("../docs/releaseNotes/v1.9.0.md"); function ReleaseNotes() { const location = useLocation(); @@ -50,13 +51,18 @@ function ReleaseNotes() { Release Notes +
+ + + +
- +
- +
From 0bf911675c9f5d4075e353c206c5b9547adee542 Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Fri, 11 Jun 2021 17:30:10 +1000 Subject: [PATCH 120/176] Add more vertical margin to Markdown rendering and change embed to image tag --- src/components/Markdown.js | 14 ++++++-------- src/docs/howTo/overview.md | 2 +- src/docs/howTo/sharingMaps.md | 2 +- src/docs/howTo/usingDice.md | 2 +- src/docs/howTo/usingDrawing.md | 2 +- src/docs/howTo/usingFog.md | 2 +- src/docs/howTo/usingTokens.md | 2 +- src/docs/releaseNotes/v1.1.0.md | 2 +- src/docs/releaseNotes/v1.2.0.md | 2 +- src/docs/releaseNotes/v1.3.0.md | 2 +- src/docs/releaseNotes/v1.4.0.md | 2 +- src/docs/releaseNotes/v1.5.0.md | 2 +- src/docs/releaseNotes/v1.6.0.md | 2 +- src/docs/releaseNotes/v1.7.0.md | 2 +- src/docs/releaseNotes/v1.8.0.md | 2 +- 15 files changed, 20 insertions(+), 22 deletions(-) diff --git a/src/components/Markdown.js b/src/components/Markdown.js index 52d67fe..388ea77 100644 --- a/src/components/Markdown.js +++ b/src/components/Markdown.js @@ -9,7 +9,7 @@ import { import ReactMarkdown from "react-markdown"; function Paragraph(props) { - return ; + return ; } function Heading({ level, ...props }) { @@ -27,6 +27,9 @@ function Heading({ level, ...props }) { } function Image(props) { + if (props.alt === "embed:") { + return ; + } if (props.src.endsWith(".mp4")) { return (
- {gridChanged && ( + {gridChanged && gridValid && ( Date: Sat, 12 Jun 2021 14:30:54 +1000 Subject: [PATCH 124/176] Move map and token settings to match the group overlay visual --- src/components/Select.js | 2 +- src/components/map/MapSettings.js | 306 +++++++++++++----------------- src/modals/EditMapModal.js | 53 ++++-- src/modals/EditTokenModal.js | 50 +++-- 4 files changed, 201 insertions(+), 210 deletions(-) diff --git a/src/components/Select.js b/src/components/Select.js index e56c418..5bc43fc 100644 --- a/src/components/Select.js +++ b/src/components/Select.js @@ -24,7 +24,7 @@ function Select({ creatable, ...props }) { }), control: (provided, state) => ({ ...provided, - backgroundColor: theme.colors.background, + backgroundColor: "transparent", color: theme.colors.text, borderColor: theme.colors.text, opacity: state.isDisabled ? 0.5 : 1, diff --git a/src/components/map/MapSettings.js b/src/components/map/MapSettings.js index fdfe0c4..8f05c3b 100644 --- a/src/components/map/MapSettings.js +++ b/src/components/map/MapSettings.js @@ -1,7 +1,5 @@ import React, { useEffect, useState } from "react"; -import { Flex, Box, Label, Input, Checkbox, IconButton } from "theme-ui"; - -import ExpandMoreIcon from "../../icons/ExpandMoreIcon"; +import { Flex, Box, Label, Input, Checkbox } from "theme-ui"; import { isEmpty } from "../../helpers/shared"; import { getGridUpdatedInset } from "../../helpers/grid"; @@ -43,8 +41,6 @@ function MapSettings({ mapState, onSettingsChange, onStateSettingsChange, - showMore, - onShowMoreChange, }) { function handleFlagChange(event, flag) { if (event.target.checked) { @@ -177,172 +173,142 @@ function MapSettings({ my={1} />
- {showMore && ( - <> - - - - - s.value === map.grid.measurement.type - ) - } - onChange={handleGridMeasurementTypeChange} - isSearchable={false} - /> - - - - - - - - {!mapEmpty && map.type !== "default" && ( - - - - s.value === map.grid.type) + } + onChange={handleGridTypeChange} + isSearchable={false} + /> + + + + + + + + + + + + + + {!mapEmpty && map.type !== "default" && ( + + + +