Move tokenState to work without backing token, add asset sync
This commit is contained in:
parent
a023ef61ed
commit
245a9cee43
@ -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";
|
||||
|
@ -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,13 +28,10 @@ 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);
|
||||
if (b.category !== a.category) {
|
||||
const aWeight = getMapTokenCategoryWeight(a.category);
|
||||
const bWeight = getMapTokenCategoryWeight(b.category);
|
||||
return bWeight - aWeight;
|
||||
} else if (
|
||||
tokenDraggingOptions &&
|
||||
@ -81,13 +51,6 @@ function MapTokens({
|
||||
// Else sort so last modified is on top
|
||||
return a.lastModified - b.lastModified;
|
||||
}
|
||||
} else if (tokenA) {
|
||||
return 1;
|
||||
} else if (tokenB) {
|
||||
return -1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
@ -97,7 +60,6 @@ function MapTokens({
|
||||
.map((tokenState) => (
|
||||
<MapToken
|
||||
key={tokenState.id}
|
||||
token={tokensById[tokenState.tokenId]}
|
||||
tokenState={tokenState}
|
||||
onTokenStateChange={onMapTokenStateChange}
|
||||
onTokenMenuOpen={handleTokenMenuOpen}
|
||||
|
@ -26,15 +26,17 @@ function TokenSettings({ token, onSettingsChange }) {
|
||||
/>
|
||||
</Box>
|
||||
<Box mt={2}>
|
||||
<Label mb={1}>Category</Label>
|
||||
<Label mb={1}>Default Category</Label>
|
||||
<Select
|
||||
options={categorySettings}
|
||||
value={
|
||||
!tokenEmpty &&
|
||||
categorySettings.find((s) => s.value === token.category)
|
||||
categorySettings.find((s) => s.value === token.defaultCategory)
|
||||
}
|
||||
isDisabled={tokenEmpty || token.type === "default"}
|
||||
onChange={(option) => onSettingsChange("category", option.value)}
|
||||
onChange={(option) =>
|
||||
onSettingsChange("defaultCategory", option.value)
|
||||
}
|
||||
isSearchable={false}
|
||||
/>
|
||||
</Box>
|
||||
|
@ -25,12 +25,13 @@ function Tokens({ onMapTokenStateCreate }) {
|
||||
function handleProxyDragEnd(isOnMap, token) {
|
||||
if (isOnMap && onMapTokenStateCreate) {
|
||||
// Create a token state from the dragged token
|
||||
onMapTokenStateCreate({
|
||||
let tokenState = {
|
||||
id: shortid.generate(),
|
||||
tokenId: token.id,
|
||||
owner: userId,
|
||||
size: token.defaultSize,
|
||||
label: "",
|
||||
category: token.defaultCategory,
|
||||
label: token.defaultLabel,
|
||||
statuses: [],
|
||||
x: token.x,
|
||||
y: token.y,
|
||||
@ -39,7 +40,15 @@ function Tokens({ onMapTokenStateCreate }) {
|
||||
rotation: 0,
|
||||
locked: false,
|
||||
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
|
||||
// Keep last modified the same
|
||||
updateToken(token.id, {
|
||||
|
@ -12,6 +12,7 @@ import { omit } from "../helpers/shared";
|
||||
* @property {number} height
|
||||
* @property {Uint8Array} file
|
||||
* @property {string} mime
|
||||
* @property {string} owner
|
||||
*/
|
||||
|
||||
/**
|
||||
@ -25,10 +26,16 @@ import { omit } from "../helpers/shared";
|
||||
* @param {Asset[]} assets
|
||||
*/
|
||||
|
||||
/**
|
||||
* @callback putAsset
|
||||
* @param {Asset} asset
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef AssetsContext
|
||||
* @property {getAsset} getAsset
|
||||
* @property {addAssets} addAssets
|
||||
* @property {putAsset} putAsset
|
||||
*/
|
||||
|
||||
/**
|
||||
@ -54,9 +61,17 @@ export function AssetsProvider({ children }) {
|
||||
[database]
|
||||
);
|
||||
|
||||
const putAsset = useCallback(
|
||||
async (asset) => {
|
||||
return database.table("assets").put(asset);
|
||||
},
|
||||
[database]
|
||||
);
|
||||
|
||||
const value = {
|
||||
getAsset,
|
||||
addAssets,
|
||||
putAsset,
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -20,10 +20,6 @@ export function TokenDataProvider({ children }) {
|
||||
const { database, databaseStatus, worker } = useDatabase();
|
||||
const { userId } = useAuth();
|
||||
|
||||
/**
|
||||
* Contains all tokens without any file data,
|
||||
* to ensure file data is present call loadTokens
|
||||
*/
|
||||
const [tokens, setTokens] = useState([]);
|
||||
const [tokensLoading, setTokensLoading] = useState(true);
|
||||
|
||||
@ -44,7 +40,6 @@ export function TokenDataProvider({ children }) {
|
||||
return defaultTokensWithIds;
|
||||
}
|
||||
|
||||
// Loads tokens without the file data to save memory
|
||||
async function loadTokens() {
|
||||
let storedTokens = [];
|
||||
// Try to load tokens with worker, fallback to database if failed
|
||||
@ -156,35 +151,6 @@ export function TokenDataProvider({ children }) {
|
||||
[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
|
||||
useEffect(() => {
|
||||
if (!database || databaseStatus === "loading") {
|
||||
@ -248,8 +214,6 @@ export function TokenDataProvider({ children }) {
|
||||
tokensById,
|
||||
tokensLoading,
|
||||
getTokenFromDB,
|
||||
loadTokens,
|
||||
unloadTokens,
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -3,6 +3,7 @@ import Dexie, { Version, DexieOptions } from "dexie";
|
||||
import "dexie-observable";
|
||||
import shortid from "shortid";
|
||||
import { v4 as uuid } from "uuid";
|
||||
import Case from "case";
|
||||
|
||||
import blobToBuffer from "./helpers/blobToBuffer";
|
||||
import { getGridDefaultInset } from "./helpers/grid";
|
||||
@ -434,7 +435,7 @@ const versions = {
|
||||
},
|
||||
// v1.9.0 - Move map assets into new table
|
||||
23(v) {
|
||||
v.stores({ assets: "id" }).upgrade((tx) => {
|
||||
v.stores({ assets: "id, owner" }).upgrade((tx) => {
|
||||
tx.table("maps").each((map) => {
|
||||
let assets = [];
|
||||
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
|
||||
|
@ -1,5 +1,3 @@
|
||||
import { v4 as uuid } from "uuid";
|
||||
|
||||
import blobToBuffer from "./blobToBuffer";
|
||||
|
||||
const lightnessDetectionOffset = 0.1;
|
||||
@ -90,8 +88,7 @@ export async function resizeImage(image, size, type, quality) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef Asset
|
||||
* @property {string} id
|
||||
* @typedef ImageAsset
|
||||
* @property {number} width
|
||||
* @property {number} height
|
||||
* @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 {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<Asset>}
|
||||
* @returns {Promise<ImageAsset>}
|
||||
*/
|
||||
export async function createThumbnail(image, type, size = 300, quality = 0.5) {
|
||||
let canvas = document.createElement("canvas");
|
||||
@ -153,6 +150,5 @@ export async function createThumbnail(image, type, size = 300, quality = 0.5) {
|
||||
width: thumbnailImage.width,
|
||||
height: thumbnailImage.height,
|
||||
mime: type,
|
||||
id: uuid(),
|
||||
};
|
||||
}
|
||||
|
27
src/helpers/map.js
Normal file
27
src/helpers/map.js
Normal 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;
|
||||
}
|
||||
}
|
@ -252,13 +252,15 @@ function SelectMapModal({
|
||||
height: resized.height,
|
||||
id: assetId,
|
||||
mime: file.type,
|
||||
owner: userId,
|
||||
};
|
||||
assets.push(asset);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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);
|
||||
|
||||
const fileAsset = {
|
||||
@ -267,6 +269,7 @@ function SelectMapModal({
|
||||
width: image.width,
|
||||
height: image.height,
|
||||
mime: file.type,
|
||||
owner: userId,
|
||||
};
|
||||
assets.push(fileAsset);
|
||||
|
||||
|
@ -163,7 +163,8 @@ function SelectTokensModal({ isOpen, onRequestClose }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
image.onload = async function () {
|
||||
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);
|
||||
|
||||
const fileAsset = {
|
||||
@ -172,6 +173,7 @@ function SelectTokensModal({ isOpen, onRequestClose }) {
|
||||
width: image.width,
|
||||
height: image.height,
|
||||
mime: file.type,
|
||||
owner: userId,
|
||||
};
|
||||
assets.push(fileAsset);
|
||||
|
||||
@ -186,7 +188,8 @@ function SelectTokensModal({ isOpen, onRequestClose }) {
|
||||
lastUsed: Date.now(),
|
||||
owner: userId,
|
||||
defaultSize: 1,
|
||||
category: "character",
|
||||
defaultCategory: "character",
|
||||
defaultLabel: "",
|
||||
hideInSidebar: false,
|
||||
group: "",
|
||||
width: image.width,
|
||||
|
@ -1,14 +1,15 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { useToasts } from "react-toast-notifications";
|
||||
|
||||
import { useTokenData } from "../contexts/TokenDataContext";
|
||||
import { useMapData } from "../contexts/MapDataContext";
|
||||
import { useMapLoading } from "../contexts/MapLoadingContext";
|
||||
import { useAuth } from "../contexts/AuthContext";
|
||||
import { useDatabase } from "../contexts/DatabaseContext";
|
||||
import { useParty } from "../contexts/PartyContext";
|
||||
import { useAssets } from "../contexts/AssetsContext";
|
||||
|
||||
import { omit } from "../helpers/shared";
|
||||
import { getMapPreviewAsset } from "../helpers/map";
|
||||
|
||||
import useDebounce from "../hooks/useDebounce";
|
||||
import useNetworkedState from "../hooks/useNetworkedState";
|
||||
@ -46,8 +47,8 @@ function NetworkedMapAndTokens({ session }) {
|
||||
isLoading,
|
||||
} = useMapLoading();
|
||||
|
||||
const { putToken, getTokenFromDB } = useTokenData();
|
||||
const { putMap, updateMap, getMapFromDB, updateMapState } = useMapData();
|
||||
const { updateMapState } = useMapData();
|
||||
const { getAsset, putAsset } = useAssets();
|
||||
|
||||
const [currentMap, setCurrentMap] = useState(null);
|
||||
const [currentMapState, setCurrentMapState] = useNetworkedState(
|
||||
@ -69,48 +70,39 @@ function NetworkedMapAndTokens({ session }) {
|
||||
|
||||
async function loadAssetManifestFromMap(map, mapState) {
|
||||
const assets = {};
|
||||
const { owner } = map;
|
||||
if (map.type === "file") {
|
||||
const { id, lastModified, owner } = map;
|
||||
assets[`map-${id}`] = { type: "map", id, lastModified, owner };
|
||||
const previewId = getMapPreviewAsset(map);
|
||||
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();
|
||||
for (let tokenState of Object.values(mapState.tokens)) {
|
||||
const token = await getTokenFromDB(tokenState.tokenId);
|
||||
if (
|
||||
token &&
|
||||
token.type === "file" &&
|
||||
!processedTokens.has(tokenState.tokenId)
|
||||
tokenState.file &&
|
||||
!processedTokens.has(tokenState.file) &&
|
||||
tokenState.owner === owner
|
||||
) {
|
||||
processedTokens.add(tokenState.tokenId);
|
||||
// Omit file from token peer will request file if needed
|
||||
const { id, lastModified, owner } = token;
|
||||
assets[`token-${id}`] = { type: "token", id, lastModified, owner };
|
||||
processedTokens.add(tokenState.file);
|
||||
assets[tokenState.file] = { id: tokenState.file, owner };
|
||||
}
|
||||
}
|
||||
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) {
|
||||
setAssetManifest((prevManifest) => {
|
||||
if (prevManifest?.assets) {
|
||||
const id =
|
||||
asset.type === "map" ? `map-${asset.id}` : `token-${asset.id}`;
|
||||
const id = asset.id;
|
||||
const exists = id in prevManifest.assets;
|
||||
const needsUpdate =
|
||||
exists && assetNeedsUpdate(prevManifest.assets[id], asset);
|
||||
if (!exists || needsUpdate) {
|
||||
if (!exists) {
|
||||
return {
|
||||
...prevManifest,
|
||||
assets: {
|
||||
@ -145,48 +137,28 @@ function NetworkedMapAndTokens({ session }) {
|
||||
(player) => player.userId === asset.owner
|
||||
);
|
||||
|
||||
const cachedAsset = await getAsset(asset.id);
|
||||
if (!owner) {
|
||||
// Add no owner toast if asset is a map and we don't have it in out cache
|
||||
if (asset.type === "map") {
|
||||
const cachedMap = await getMapFromDB(asset.id);
|
||||
if (!cachedMap) {
|
||||
addToast("Unable to find owner for map");
|
||||
}
|
||||
// Add no owner toast if we don't have asset in out cache
|
||||
if (!cachedAsset) {
|
||||
// TODO: Stop toast from appearing multiple times
|
||||
addToast("Unable to find owner for asset");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
requestingAssetsRef.current.add(asset.id);
|
||||
|
||||
if (asset.type === "map") {
|
||||
const cachedMap = await getMapFromDB(asset.id);
|
||||
if (cachedMap && cachedMap.lastModified === asset.lastModified) {
|
||||
if (cachedAsset) {
|
||||
requestingAssetsRef.current.delete(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);
|
||||
}
|
||||
session.sendTo(owner.sessionId, "assetRequest", asset.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
requestAssetsIfNeeded();
|
||||
}, [
|
||||
assetManifest,
|
||||
partyState,
|
||||
session,
|
||||
getMapFromDB,
|
||||
getTokenFromDB,
|
||||
updateMap,
|
||||
userId,
|
||||
addToast,
|
||||
]);
|
||||
}, [assetManifest, partyState, session, userId, addToast, getAsset]);
|
||||
|
||||
/**
|
||||
* Map state
|
||||
@ -215,12 +187,8 @@ function NetworkedMapAndTokens({ session }) {
|
||||
setCurrentMapState(newMapState, true, true);
|
||||
setCurrentMap(newMap);
|
||||
|
||||
if (newMap && newMap.type === "file") {
|
||||
const { file, resolutions, thumbnail, ...rest } = newMap;
|
||||
session.socket?.emit("map", rest);
|
||||
} else {
|
||||
session.socket?.emit("map", newMap);
|
||||
}
|
||||
|
||||
if (!newMap || !newMapState) {
|
||||
setAssetManifest(null, true, true);
|
||||
return;
|
||||
@ -371,11 +339,8 @@ function NetworkedMapAndTokens({ session }) {
|
||||
if (!currentMap || !currentMapState) {
|
||||
return;
|
||||
}
|
||||
// If file type token send the token to the other peers
|
||||
const token = await getTokenFromDB(tokenState.tokenId);
|
||||
if (token && token.type === "file") {
|
||||
const { id, lastModified, owner } = token;
|
||||
addAssetIfNeeded({ type: "token", id, lastModified, owner });
|
||||
if (tokenState.file) {
|
||||
addAssetIfNeeded({ id: tokenState.file, owner: tokenState.owner });
|
||||
}
|
||||
setCurrentMapState((prevMapState) => ({
|
||||
...prevMapState,
|
||||
@ -414,101 +379,21 @@ function NetworkedMapAndTokens({ session }) {
|
||||
|
||||
useEffect(() => {
|
||||
async function handlePeerData({ id, data, reply }) {
|
||||
if (id === "mapRequest") {
|
||||
const map = await getMapFromDB(data);
|
||||
function replyWithMap(preview, resolution) {
|
||||
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");
|
||||
if (id === "assetRequest") {
|
||||
const asset = await getAsset(data);
|
||||
reply("assetResponse", asset);
|
||||
}
|
||||
|
||||
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") {
|
||||
const newMap = data;
|
||||
if (newMap?.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);
|
||||
}
|
||||
if (id === "assetResponse") {
|
||||
await putAsset(data);
|
||||
requestingAssetsRef.current.delete(data.id);
|
||||
assetLoadFinish();
|
||||
}
|
||||
}
|
||||
|
||||
function handlePeerDataProgress({ id, total, count }) {
|
||||
if (count === 1) {
|
||||
// Corresponding asset load finished called in token and map response
|
||||
// Corresponding asset load finished called in asset response
|
||||
assetLoadStart();
|
||||
}
|
||||
assetProgressUpdate({ id, total, count });
|
||||
@ -516,12 +401,7 @@ function NetworkedMapAndTokens({ session }) {
|
||||
|
||||
async function handleSocketMap(map) {
|
||||
if (map) {
|
||||
if (map.type === "file") {
|
||||
const fullMap = await getMapFromDB(map.id);
|
||||
setCurrentMap(fullMap || map);
|
||||
} else {
|
||||
setCurrentMap(map);
|
||||
}
|
||||
} else {
|
||||
setCurrentMap(null);
|
||||
}
|
||||
|
@ -85,7 +85,8 @@ export const tokens = Object.keys(tokenSources).map((key) => ({
|
||||
name: Case.capital(key),
|
||||
type: "default",
|
||||
defaultSize: getDefaultTokenSize(key),
|
||||
category: "character",
|
||||
defaultLabel: "",
|
||||
defaultCategory: "character",
|
||||
hideInSidebar: false,
|
||||
width: 256,
|
||||
height: 256,
|
||||
|
Loading…
Reference in New Issue
Block a user