Move tokenState to work without backing token, add asset sync

This commit is contained in:
Mitchell McCaffrey 2021-04-23 17:10:10 +10:00
parent a023ef61ed
commit 245a9cee43
13 changed files with 198 additions and 285 deletions

View File

@ -24,7 +24,6 @@ import TokenLabel from "../token/TokenLabel";
import { tokenSources, unknownSource } from "../../tokens"; import { tokenSources, unknownSource } from "../../tokens";
function MapToken({ function MapToken({
token,
tokenState, tokenState,
onTokenStateChange, onTokenStateChange,
onTokenMenuOpen, onTokenMenuOpen,
@ -43,7 +42,7 @@ function MapToken({
const gridCellPixelSize = useGridCellPixelSize(); const gridCellPixelSize = useGridCellPixelSize();
const tokenSource = useDataURL(token, tokenSources, unknownSource); const tokenSource = useDataURL(tokenState, tokenSources, unknownSource);
const [tokenSourceImage, tokenSourceStatus] = useImage(tokenSource); const [tokenSourceImage, tokenSourceStatus] = useImage(tokenSource);
const [tokenAspectRatio, setTokenAspectRatio] = useState(1); const [tokenAspectRatio, setTokenAspectRatio] = useState(1);
@ -59,7 +58,7 @@ function MapToken({
const tokenGroup = event.target; const tokenGroup = event.target;
const tokenImage = imageRef.current; const tokenImage = imageRef.current;
if (token && token.category === "vehicle") { if (tokenState.category === "vehicle") {
// Enable hit detection for .intersects() function // Enable hit detection for .intersects() function
Konva.hitOnDragEnabled = true; Konva.hitOnDragEnabled = true;
@ -99,7 +98,7 @@ function MapToken({
const tokenGroup = event.target; const tokenGroup = event.target;
const mountChanges = {}; const mountChanges = {};
if (token && token.category === "vehicle") { if (tokenState.category === "vehicle") {
Konva.hitOnDragEnabled = false; Konva.hitOnDragEnabled = false;
const parent = tokenGroup.getParent(); const parent = tokenGroup.getParent();
@ -196,8 +195,16 @@ function MapToken({
const canvas = image.getCanvas(); const canvas = image.getCanvas();
const pixelRatio = canvas.pixelRatio || 1; const pixelRatio = canvas.pixelRatio || 1;
if (tokenSourceStatus === "loaded" && tokenWidth > 0 && tokenHeight > 0) { if (
const maxImageSize = token ? Math.max(token.width, token.height) : 512; // Default to 512px tokenSourceStatus === "loaded" &&
tokenWidth > 0 &&
tokenHeight > 0 &&
tokenSourceImage
) {
const maxImageSize = Math.max(
tokenSourceImage.width,
tokenSourceImage.height
);
const maxTokenSize = Math.max(tokenWidth, tokenHeight); const maxTokenSize = Math.max(tokenWidth, tokenHeight);
// Constrain image buffer to original image size // Constrain image buffer to original image size
const maxRatio = maxImageSize / maxTokenSize; const maxRatio = maxImageSize / maxTokenSize;
@ -210,7 +217,13 @@ function MapToken({
}); });
image.drawHitFromCache(); image.drawHitFromCache();
} }
}, [debouncedStageScale, tokenWidth, tokenHeight, tokenSourceStatus, token]); }, [
debouncedStageScale,
tokenWidth,
tokenHeight,
tokenSourceStatus,
tokenSourceImage,
]);
// Animate to new token positions if edited by others // Animate to new token positions if edited by others
const tokenX = tokenState.x * mapWidth; 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 // Token name is used by on click to find whether a token is a vehicle or prop
let tokenName = ""; let tokenName = "";
if (token) { if (tokenState) {
tokenName = token.category; tokenName = tokenState.category;
} }
if (tokenState && tokenState.locked) { if (tokenState && tokenState.locked) {
tokenName = tokenName + "-locked"; tokenName = tokenName + "-locked";

View File

@ -1,10 +1,8 @@
import React, { useEffect } from "react"; import React from "react";
import { Group } from "react-konva"; import { Group } from "react-konva";
import MapToken from "./MapToken"; import MapToken from "./MapToken";
import { useTokenData } from "../../contexts/TokenDataContext";
function MapTokens({ function MapTokens({
map, map,
mapState, mapState,
@ -15,31 +13,6 @@ function MapTokens({
selectedToolId, selectedToolId,
disabledTokens, 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) { function getMapTokenCategoryWeight(category) {
switch (category) { switch (category) {
case "character": case "character":
@ -55,38 +28,28 @@ function MapTokens({
// Sort so vehicles render below other tokens // Sort so vehicles render below other tokens
function sortMapTokenStates(a, b, tokenDraggingOptions) { function sortMapTokenStates(a, b, tokenDraggingOptions) {
const tokenA = tokensById[a.tokenId]; // If categories are different sort in order "prop", "vehicle", "character"
const tokenB = tokensById[b.tokenId]; if (b.category !== a.category) {
if (tokenA && tokenB) { const aWeight = getMapTokenCategoryWeight(a.category);
// If categories are different sort in order "prop", "vehicle", "character" const bWeight = getMapTokenCategoryWeight(b.category);
if (tokenB.category !== tokenA.category) { return bWeight - aWeight;
const aWeight = getMapTokenCategoryWeight(tokenA.category); } else if (
const bWeight = getMapTokenCategoryWeight(tokenB.category); tokenDraggingOptions &&
return bWeight - aWeight; tokenDraggingOptions.dragging &&
} else if ( tokenDraggingOptions.tokenState.id === a.id
tokenDraggingOptions && ) {
tokenDraggingOptions.dragging && // If dragging token a move above
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) {
return 1; return 1;
} else if (tokenB) { } else if (
tokenDraggingOptions &&
tokenDraggingOptions.dragging &&
tokenDraggingOptions.tokenState.id === b.id
) {
// If dragging token b move above
return -1; return -1;
} else { } else {
return 0; // Else sort so last modified is on top
return a.lastModified - b.lastModified;
} }
} }
@ -97,7 +60,6 @@ function MapTokens({
.map((tokenState) => ( .map((tokenState) => (
<MapToken <MapToken
key={tokenState.id} key={tokenState.id}
token={tokensById[tokenState.tokenId]}
tokenState={tokenState} tokenState={tokenState}
onTokenStateChange={onMapTokenStateChange} onTokenStateChange={onMapTokenStateChange}
onTokenMenuOpen={handleTokenMenuOpen} onTokenMenuOpen={handleTokenMenuOpen}

View File

@ -26,15 +26,17 @@ function TokenSettings({ token, onSettingsChange }) {
/> />
</Box> </Box>
<Box mt={2}> <Box mt={2}>
<Label mb={1}>Category</Label> <Label mb={1}>Default Category</Label>
<Select <Select
options={categorySettings} options={categorySettings}
value={ value={
!tokenEmpty && !tokenEmpty &&
categorySettings.find((s) => s.value === token.category) categorySettings.find((s) => s.value === token.defaultCategory)
} }
isDisabled={tokenEmpty || token.type === "default"} isDisabled={tokenEmpty || token.type === "default"}
onChange={(option) => onSettingsChange("category", option.value)} onChange={(option) =>
onSettingsChange("defaultCategory", option.value)
}
isSearchable={false} isSearchable={false}
/> />
</Box> </Box>

View File

@ -25,12 +25,13 @@ function Tokens({ onMapTokenStateCreate }) {
function handleProxyDragEnd(isOnMap, token) { function handleProxyDragEnd(isOnMap, token) {
if (isOnMap && onMapTokenStateCreate) { if (isOnMap && onMapTokenStateCreate) {
// Create a token state from the dragged token // Create a token state from the dragged token
onMapTokenStateCreate({ let tokenState = {
id: shortid.generate(), id: shortid.generate(),
tokenId: token.id, tokenId: token.id,
owner: userId, owner: userId,
size: token.defaultSize, size: token.defaultSize,
label: "", category: token.defaultCategory,
label: token.defaultLabel,
statuses: [], statuses: [],
x: token.x, x: token.x,
y: token.y, y: token.y,
@ -39,7 +40,15 @@ function Tokens({ onMapTokenStateCreate }) {
rotation: 0, rotation: 0,
locked: false, locked: false,
visible: true, visible: true,
}); type: token.type,
};
if (token.type === "file") {
tokenState.file = token.file;
} else if (token.type === "default") {
tokenState.key = token.key;
}
onMapTokenStateCreate(tokenState);
// TODO: Remove when cache is moved to assets
// Update last used for cache invalidation // Update last used for cache invalidation
// Keep last modified the same // Keep last modified the same
updateToken(token.id, { updateToken(token.id, {

View File

@ -12,6 +12,7 @@ import { omit } from "../helpers/shared";
* @property {number} height * @property {number} height
* @property {Uint8Array} file * @property {Uint8Array} file
* @property {string} mime * @property {string} mime
* @property {string} owner
*/ */
/** /**
@ -25,10 +26,16 @@ import { omit } from "../helpers/shared";
* @param {Asset[]} assets * @param {Asset[]} assets
*/ */
/**
* @callback putAsset
* @param {Asset} asset
*/
/** /**
* @typedef AssetsContext * @typedef AssetsContext
* @property {getAsset} getAsset * @property {getAsset} getAsset
* @property {addAssets} addAssets * @property {addAssets} addAssets
* @property {putAsset} putAsset
*/ */
/** /**
@ -54,9 +61,17 @@ export function AssetsProvider({ children }) {
[database] [database]
); );
const putAsset = useCallback(
async (asset) => {
return database.table("assets").put(asset);
},
[database]
);
const value = { const value = {
getAsset, getAsset,
addAssets, addAssets,
putAsset,
}; };
return ( return (

View File

@ -20,10 +20,6 @@ export function TokenDataProvider({ children }) {
const { database, databaseStatus, worker } = useDatabase(); const { database, databaseStatus, worker } = useDatabase();
const { userId } = useAuth(); const { userId } = useAuth();
/**
* Contains all tokens without any file data,
* to ensure file data is present call loadTokens
*/
const [tokens, setTokens] = useState([]); const [tokens, setTokens] = useState([]);
const [tokensLoading, setTokensLoading] = useState(true); const [tokensLoading, setTokensLoading] = useState(true);
@ -44,7 +40,6 @@ export function TokenDataProvider({ children }) {
return defaultTokensWithIds; return defaultTokensWithIds;
} }
// Loads tokens without the file data to save memory
async function loadTokens() { async function loadTokens() {
let storedTokens = []; let storedTokens = [];
// Try to load tokens with worker, fallback to database if failed // Try to load tokens with worker, fallback to database if failed
@ -156,35 +151,6 @@ export function TokenDataProvider({ children }) {
[database, updateCache, userId] [database, updateCache, userId]
); );
const loadTokens = useCallback(
async (tokenIds) => {
const loadedTokens = await database.table("tokens").bulkGet(tokenIds);
const loadedTokensById = loadedTokens.reduce((obj, token) => {
obj[token.id] = token;
return obj;
}, {});
setTokens((prevTokens) => {
return prevTokens.map((prevToken) => {
if (prevToken.id in loadedTokensById) {
return loadedTokensById[prevToken.id];
} else {
return prevToken;
}
});
});
},
[database]
);
const unloadTokens = useCallback(async () => {
setTokens((prevTokens) => {
return prevTokens.map((prevToken) => {
const { file, ...rest } = prevToken;
return rest;
});
});
}, []);
// Create DB observable to sync creating and deleting // Create DB observable to sync creating and deleting
useEffect(() => { useEffect(() => {
if (!database || databaseStatus === "loading") { if (!database || databaseStatus === "loading") {
@ -248,8 +214,6 @@ export function TokenDataProvider({ children }) {
tokensById, tokensById,
tokensLoading, tokensLoading,
getTokenFromDB, getTokenFromDB,
loadTokens,
unloadTokens,
}; };
return ( return (

View File

@ -3,6 +3,7 @@ import Dexie, { Version, DexieOptions } from "dexie";
import "dexie-observable"; import "dexie-observable";
import shortid from "shortid"; import shortid from "shortid";
import { v4 as uuid } from "uuid"; import { v4 as uuid } from "uuid";
import Case from "case";
import blobToBuffer from "./helpers/blobToBuffer"; import blobToBuffer from "./helpers/blobToBuffer";
import { getGridDefaultInset } from "./helpers/grid"; import { getGridDefaultInset } from "./helpers/grid";
@ -434,7 +435,7 @@ const versions = {
}, },
// v1.9.0 - Move map assets into new table // v1.9.0 - Move map assets into new table
23(v) { 23(v) {
v.stores({ assets: "id" }).upgrade((tx) => { v.stores({ assets: "id, owner" }).upgrade((tx) => {
tx.table("maps").each((map) => { tx.table("maps").each((map) => {
let assets = []; let assets = [];
assets.push({ assets.push({
@ -558,9 +559,46 @@ const versions = {
}); });
}); });
}, },
28(v) {
v.stores().upgrade((tx) => {
tx.table("tokens")
.toCollection()
.modify((token) => {
token.defaultCategory = token.category;
delete token.category;
token.defaultLabel = "";
});
});
},
29(v) {
v.stores().upgrade((tx) => {
tx.table("states")
.toCollection()
.modify(async (state) => {
for (let tokenState of Object.values(state.tokens)) {
if (!tokenState.tokenId.startsWith("__default")) {
const token = await tx.table("tokens").get(tokenState.tokenId);
if (token) {
tokenState.category = token.defaultCategory;
tokenState.file = token.file;
tokenState.type = "file";
} else {
tokenState.category = "character";
tokenState.type = "file";
tokenState.file = "";
}
} else {
tokenState.category = "character";
tokenState.type = "default";
tokenState.key = Case.camel(tokenState.tokenId.slice(10));
}
}
});
});
},
}; };
const latestVersion = 27; const latestVersion = 29;
/** /**
* Load versions onto a database up to a specific version number * Load versions onto a database up to a specific version number

View File

@ -1,5 +1,3 @@
import { v4 as uuid } from "uuid";
import blobToBuffer from "./blobToBuffer"; import blobToBuffer from "./blobToBuffer";
const lightnessDetectionOffset = 0.1; const lightnessDetectionOffset = 0.1;
@ -90,8 +88,7 @@ export async function resizeImage(image, size, type, quality) {
} }
/** /**
* @typedef Asset * @typedef ImageAsset
* @property {string} id
* @property {number} width * @property {number} width
* @property {number} height * @property {number} height
* @property {Uint8Array} file * @property {Uint8Array} file
@ -104,7 +101,7 @@ export async function resizeImage(image, size, type, quality) {
* @param {string} type the mime type of the image * @param {string} type the mime type of the image
* @param {number} size the width and height of the thumbnail * @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 * @param {number} quality if image is a jpeg or webp this is the quality setting
* @returns {Promise<Asset>} * @returns {Promise<ImageAsset>}
*/ */
export async function createThumbnail(image, type, size = 300, quality = 0.5) { export async function createThumbnail(image, type, size = 300, quality = 0.5) {
let canvas = document.createElement("canvas"); let canvas = document.createElement("canvas");
@ -153,6 +150,5 @@ export async function createThumbnail(image, type, size = 300, quality = 0.5) {
width: thumbnailImage.width, width: thumbnailImage.width,
height: thumbnailImage.height, height: thumbnailImage.height,
mime: type, mime: type,
id: uuid(),
}; };
} }

27
src/helpers/map.js Normal file
View File

@ -0,0 +1,27 @@
/**
* Get the asset id of the preview file to send for a map
* @param {any} map
* @returns {undefined|string}
*/
export function getMapPreviewAsset(map) {
const res = map.resolutions;
switch (map.quality) {
case "low":
return;
case "medium":
return res.low;
case "high":
return res.medium;
case "ultra":
return res.medium;
case "original":
if (res.medium) {
return res.medium;
} else if (res.low) {
return res.low;
}
return;
default:
return;
}
}

View File

@ -252,13 +252,15 @@ function SelectMapModal({
height: resized.height, height: resized.height,
id: assetId, id: assetId,
mime: file.type, mime: file.type,
owner: userId,
}; };
assets.push(asset); assets.push(asset);
} }
} }
} }
// Create thumbnail // Create thumbnail
const thumbnail = await createThumbnail(image, file.type); const thumbnailImage = await createThumbnail(image, file.type);
const thumbnail = { ...thumbnailImage, id: uuid(), owner: userId };
assets.push(thumbnail); assets.push(thumbnail);
const fileAsset = { const fileAsset = {
@ -267,6 +269,7 @@ function SelectMapModal({
width: image.width, width: image.width,
height: image.height, height: image.height,
mime: file.type, mime: file.type,
owner: userId,
}; };
assets.push(fileAsset); assets.push(fileAsset);

View File

@ -163,7 +163,8 @@ function SelectTokensModal({ isOpen, onRequestClose }) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
image.onload = async function () { image.onload = async function () {
let assets = []; let assets = [];
const thumbnail = await createThumbnail(image, file.type); const thumbnailImage = await createThumbnail(image, file.type);
const thumbnail = { ...thumbnailImage, id: uuid(), owner: userId };
assets.push(thumbnail); assets.push(thumbnail);
const fileAsset = { const fileAsset = {
@ -172,6 +173,7 @@ function SelectTokensModal({ isOpen, onRequestClose }) {
width: image.width, width: image.width,
height: image.height, height: image.height,
mime: file.type, mime: file.type,
owner: userId,
}; };
assets.push(fileAsset); assets.push(fileAsset);
@ -186,7 +188,8 @@ function SelectTokensModal({ isOpen, onRequestClose }) {
lastUsed: Date.now(), lastUsed: Date.now(),
owner: userId, owner: userId,
defaultSize: 1, defaultSize: 1,
category: "character", defaultCategory: "character",
defaultLabel: "",
hideInSidebar: false, hideInSidebar: false,
group: "", group: "",
width: image.width, width: image.width,

View File

@ -1,14 +1,15 @@
import React, { useState, useEffect, useRef } from "react"; import React, { useState, useEffect, useRef } from "react";
import { useToasts } from "react-toast-notifications"; import { useToasts } from "react-toast-notifications";
import { useTokenData } from "../contexts/TokenDataContext";
import { useMapData } from "../contexts/MapDataContext"; import { useMapData } from "../contexts/MapDataContext";
import { useMapLoading } from "../contexts/MapLoadingContext"; import { useMapLoading } from "../contexts/MapLoadingContext";
import { useAuth } from "../contexts/AuthContext"; import { useAuth } from "../contexts/AuthContext";
import { useDatabase } from "../contexts/DatabaseContext"; import { useDatabase } from "../contexts/DatabaseContext";
import { useParty } from "../contexts/PartyContext"; import { useParty } from "../contexts/PartyContext";
import { useAssets } from "../contexts/AssetsContext";
import { omit } from "../helpers/shared"; import { omit } from "../helpers/shared";
import { getMapPreviewAsset } from "../helpers/map";
import useDebounce from "../hooks/useDebounce"; import useDebounce from "../hooks/useDebounce";
import useNetworkedState from "../hooks/useNetworkedState"; import useNetworkedState from "../hooks/useNetworkedState";
@ -46,8 +47,8 @@ function NetworkedMapAndTokens({ session }) {
isLoading, isLoading,
} = useMapLoading(); } = useMapLoading();
const { putToken, getTokenFromDB } = useTokenData(); const { updateMapState } = useMapData();
const { putMap, updateMap, getMapFromDB, updateMapState } = useMapData(); const { getAsset, putAsset } = useAssets();
const [currentMap, setCurrentMap] = useState(null); const [currentMap, setCurrentMap] = useState(null);
const [currentMapState, setCurrentMapState] = useNetworkedState( const [currentMapState, setCurrentMapState] = useNetworkedState(
@ -69,48 +70,39 @@ function NetworkedMapAndTokens({ session }) {
async function loadAssetManifestFromMap(map, mapState) { async function loadAssetManifestFromMap(map, mapState) {
const assets = {}; const assets = {};
const { owner } = map;
if (map.type === "file") { if (map.type === "file") {
const { id, lastModified, owner } = map; const previewId = getMapPreviewAsset(map);
assets[`map-${id}`] = { type: "map", id, lastModified, owner }; if (previewId) {
assets[previewId] = { id: previewId, owner };
}
const qualityId = map.resolutions[map.quality];
if (qualityId) {
assets[qualityId] = { id: qualityId, owner };
} else {
assets[map.file] = { id: map.file, owner };
}
} }
let processedTokens = new Set(); let processedTokens = new Set();
for (let tokenState of Object.values(mapState.tokens)) { for (let tokenState of Object.values(mapState.tokens)) {
const token = await getTokenFromDB(tokenState.tokenId);
if ( if (
token && tokenState.file &&
token.type === "file" && !processedTokens.has(tokenState.file) &&
!processedTokens.has(tokenState.tokenId) tokenState.owner === owner
) { ) {
processedTokens.add(tokenState.tokenId); processedTokens.add(tokenState.file);
// Omit file from token peer will request file if needed assets[tokenState.file] = { id: tokenState.file, owner };
const { id, lastModified, owner } = token;
assets[`token-${id}`] = { type: "token", id, lastModified, owner };
} }
} }
setAssetManifest({ mapId: map.id, assets }, true, true); setAssetManifest({ mapId: map.id, assets }, true, true);
} }
function compareAssets(a, b) {
return a.type === b.type && a.id === b.id;
}
// Return true if an asset is out of date
function assetNeedsUpdate(oldAsset, newAsset) {
return (
compareAssets(oldAsset, newAsset) &&
oldAsset.lastModified < newAsset.lastModified
);
}
function addAssetIfNeeded(asset) { function addAssetIfNeeded(asset) {
setAssetManifest((prevManifest) => { setAssetManifest((prevManifest) => {
if (prevManifest?.assets) { if (prevManifest?.assets) {
const id = const id = asset.id;
asset.type === "map" ? `map-${asset.id}` : `token-${asset.id}`;
const exists = id in prevManifest.assets; const exists = id in prevManifest.assets;
const needsUpdate = if (!exists) {
exists && assetNeedsUpdate(prevManifest.assets[id], asset);
if (!exists || needsUpdate) {
return { return {
...prevManifest, ...prevManifest,
assets: { assets: {
@ -145,48 +137,28 @@ function NetworkedMapAndTokens({ session }) {
(player) => player.userId === asset.owner (player) => player.userId === asset.owner
); );
const cachedAsset = await getAsset(asset.id);
if (!owner) { if (!owner) {
// Add no owner toast if asset is a map and we don't have it in out cache // Add no owner toast if we don't have asset in out cache
if (asset.type === "map") { if (!cachedAsset) {
const cachedMap = await getMapFromDB(asset.id); // TODO: Stop toast from appearing multiple times
if (!cachedMap) { addToast("Unable to find owner for asset");
addToast("Unable to find owner for map");
}
} }
continue; continue;
} }
requestingAssetsRef.current.add(asset.id); requestingAssetsRef.current.add(asset.id);
if (asset.type === "map") { if (cachedAsset) {
const cachedMap = await getMapFromDB(asset.id); requestingAssetsRef.current.delete(asset.id);
if (cachedMap && cachedMap.lastModified === asset.lastModified) { } else {
requestingAssetsRef.current.delete(asset.id); session.sendTo(owner.sessionId, "assetRequest", asset.id);
} else {
session.sendTo(owner.sessionId, "mapRequest", asset.id);
}
} else if (asset.type === "token") {
const cachedToken = await getTokenFromDB(asset.id);
if (cachedToken && cachedToken.lastModified === asset.lastModified) {
requestingAssetsRef.current.delete(asset.id);
} else {
session.sendTo(owner.sessionId, "tokenRequest", asset.id);
}
} }
} }
} }
requestAssetsIfNeeded(); requestAssetsIfNeeded();
}, [ }, [assetManifest, partyState, session, userId, addToast, getAsset]);
assetManifest,
partyState,
session,
getMapFromDB,
getTokenFromDB,
updateMap,
userId,
addToast,
]);
/** /**
* Map state * Map state
@ -215,12 +187,8 @@ function NetworkedMapAndTokens({ session }) {
setCurrentMapState(newMapState, true, true); setCurrentMapState(newMapState, true, true);
setCurrentMap(newMap); setCurrentMap(newMap);
if (newMap && newMap.type === "file") { session.socket?.emit("map", newMap);
const { file, resolutions, thumbnail, ...rest } = newMap;
session.socket?.emit("map", rest);
} else {
session.socket?.emit("map", newMap);
}
if (!newMap || !newMapState) { if (!newMap || !newMapState) {
setAssetManifest(null, true, true); setAssetManifest(null, true, true);
return; return;
@ -371,11 +339,8 @@ function NetworkedMapAndTokens({ session }) {
if (!currentMap || !currentMapState) { if (!currentMap || !currentMapState) {
return; return;
} }
// If file type token send the token to the other peers if (tokenState.file) {
const token = await getTokenFromDB(tokenState.tokenId); addAssetIfNeeded({ id: tokenState.file, owner: tokenState.owner });
if (token && token.type === "file") {
const { id, lastModified, owner } = token;
addAssetIfNeeded({ type: "token", id, lastModified, owner });
} }
setCurrentMapState((prevMapState) => ({ setCurrentMapState((prevMapState) => ({
...prevMapState, ...prevMapState,
@ -414,101 +379,21 @@ function NetworkedMapAndTokens({ session }) {
useEffect(() => { useEffect(() => {
async function handlePeerData({ id, data, reply }) { async function handlePeerData({ id, data, reply }) {
if (id === "mapRequest") { if (id === "assetRequest") {
const map = await getMapFromDB(data); const asset = await getAsset(data);
function replyWithMap(preview, resolution) { reply("assetResponse", asset);
let response = {
...map,
resolutions: undefined,
file: undefined,
thumbnail: undefined,
// Remove last modified so if there is an error
// during the map request the cache is invalid
lastModified: 0,
// Add last used for cache invalidation
lastUsed: Date.now(),
};
// Send preview if available
if (map.resolutions[preview]) {
response.resolutions = { [preview]: map.resolutions[preview] };
reply("mapResponse", response, "map");
}
// Send full map at the desired resolution if available
if (map.resolutions[resolution]) {
response.file = map.resolutions[resolution].file;
} else if (map.file) {
// The resolution might not exist for other users so send the file instead
response.file = map.file;
} else {
return;
}
// Add last modified back to file to set cache as valid
response.lastModified = map.lastModified;
reply("mapResponse", response, "map");
}
switch (map.quality) {
case "low":
replyWithMap(undefined, "low");
break;
case "medium":
replyWithMap("low", "medium");
break;
case "high":
replyWithMap("medium", "high");
break;
case "ultra":
replyWithMap("medium", "ultra");
break;
case "original":
if (map.resolutions) {
if (map.resolutions.medium) {
replyWithMap("medium");
} else if (map.resolutions.low) {
replyWithMap("low");
} else {
replyWithMap();
}
} else {
replyWithMap();
}
break;
default:
replyWithMap();
}
} }
if (id === "mapResponse") { if (id === "assetResponse") {
const newMap = data; await putAsset(data);
if (newMap?.id) { requestingAssetsRef.current.delete(data.id);
setCurrentMap(newMap);
await putMap(newMap);
// If we have the final map resolution
if (newMap.lastModified > 0) {
requestingAssetsRef.current.delete(newMap.id);
}
}
assetLoadFinish();
}
if (id === "tokenRequest") {
const token = await getTokenFromDB(data);
// Add a last used property for cache invalidation
reply("tokenResponse", { ...token, lastUsed: Date.now() }, "token");
}
if (id === "tokenResponse") {
const newToken = data;
if (newToken?.id) {
await putToken(newToken);
requestingAssetsRef.current.delete(newToken.id);
}
assetLoadFinish(); assetLoadFinish();
} }
} }
function handlePeerDataProgress({ id, total, count }) { function handlePeerDataProgress({ id, total, count }) {
if (count === 1) { if (count === 1) {
// Corresponding asset load finished called in token and map response // Corresponding asset load finished called in asset response
assetLoadStart(); assetLoadStart();
} }
assetProgressUpdate({ id, total, count }); assetProgressUpdate({ id, total, count });
@ -516,12 +401,7 @@ function NetworkedMapAndTokens({ session }) {
async function handleSocketMap(map) { async function handleSocketMap(map) {
if (map) { if (map) {
if (map.type === "file") { setCurrentMap(map);
const fullMap = await getMapFromDB(map.id);
setCurrentMap(fullMap || map);
} else {
setCurrentMap(map);
}
} else { } else {
setCurrentMap(null); setCurrentMap(null);
} }

View File

@ -85,7 +85,8 @@ export const tokens = Object.keys(tokenSources).map((key) => ({
name: Case.capital(key), name: Case.capital(key),
type: "default", type: "default",
defaultSize: getDefaultTokenSize(key), defaultSize: getDefaultTokenSize(key),
category: "character", defaultLabel: "",
defaultCategory: "character",
hideInSidebar: false, hideInSidebar: false,
width: 256, width: 256,
height: 256, height: 256,