Update maps and tokens to have thumbnails and removed loading of all files on load

This commit is contained in:
Mitchell McCaffrey 2021-02-08 16:53:56 +11:00
parent b9993e1a0b
commit 24e64f9d32
16 changed files with 418 additions and 137 deletions

View File

@ -3,7 +3,7 @@ import { Group } from "react-konva";
import MapControls from "./MapControls";
import MapInteraction from "./MapInteraction";
import MapToken from "./MapToken";
import MapTokens from "./MapTokens";
import MapDrawing from "./MapDrawing";
import MapFog from "./MapFog";
import MapGrid from "./MapGrid";
@ -175,91 +175,17 @@ function Map({
setIsTokenMenuOpen(true);
}
function getMapTokenCategoryWeight(category) {
switch (category) {
case "character":
return 0;
case "vehicle":
return 1;
case "prop":
return 2;
default:
return 0;
}
}
// 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) {
return 1;
} else if (tokenB) {
return -1;
} else {
return 0;
}
}
const mapTokens = map && mapState && (
<Group>
{Object.values(mapState.tokens)
.sort((a, b) => sortMapTokenStates(a, b, tokenDraggingOptions))
.map((tokenState) => (
<MapToken
key={tokenState.id}
token={tokensById[tokenState.tokenId]}
tokenState={tokenState}
onTokenStateChange={onMapTokenStateChange}
onTokenMenuOpen={handleTokenMenuOpen}
onTokenDragStart={(e) =>
setTokenDraggingOptions({
dragging: true,
tokenState,
tokenGroup: e.target,
})
}
onTokenDragEnd={() =>
setTokenDraggingOptions({
...tokenDraggingOptions,
dragging: false,
})
}
draggable={
selectedToolId === "pan" &&
!(tokenState.id in disabledTokens) &&
!tokenState.locked
}
mapState={mapState}
fadeOnHover={selectedToolId === "drawing"}
map={map}
/>
))}
</Group>
<MapTokens
map={map}
mapState={mapState}
tokenDraggingOptions={tokenDraggingOptions}
setTokenDraggingOptions={setTokenDraggingOptions}
onMapTokenStateChange={onMapTokenStateChange}
handleTokenMenuOpen={handleTokenMenuOpen}
selectedToolId={selectedToolId}
disabledTokens={disabledTokens}
/>
);
const tokenMenu = (

View File

@ -16,6 +16,9 @@ import { MapStageProvider, useMapStage } from "../../contexts/MapStageContext";
import AuthContext, { useAuth } from "../../contexts/AuthContext";
import SettingsContext, { useSettings } from "../../contexts/SettingsContext";
import KeyboardContext from "../../contexts/KeyboardContext";
import TokenDataContext, {
useTokenData,
} from "../../contexts/TokenDataContext";
import { GridProvider } from "../../contexts/GridContext";
import { useKeyboard } from "../../contexts/KeyboardContext";
@ -178,6 +181,7 @@ function MapInteraction({
const auth = useAuth();
const settings = useSettings();
const tokenData = useTokenData();
const mapInteraction = {
stageScale,
@ -227,7 +231,9 @@ function MapInteraction({
height={mapHeight}
>
<MapStageProvider value={mapStageRef}>
{mapLoaded && children}
<TokenDataContext.Provider value={tokenData}>
{mapLoaded && children}
</TokenDataContext.Provider>
</MapStageProvider>
</GridProvider>
</MapInteractionProvider>

View File

@ -17,11 +17,7 @@ function MapTile({
}) {
const isDefault = map.type === "default";
const mapSource = useDataSource(
isDefault
? map
: map.resolutions && map.resolutions.low
? map.resolutions.low
: map,
isDefault ? map : map.thumbnail,
defaultMapSources,
unknownSource
);

View File

@ -0,0 +1,131 @@
import React, { useEffect } from "react";
import { Group } from "react-konva";
import MapToken from "./MapToken";
import { useTokenData } from "../../contexts/TokenDataContext";
function MapTokens({
map,
mapState,
tokenDraggingOptions,
setTokenDraggingOptions,
onMapTokenStateChange,
handleTokenMenuOpen,
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":
return 0;
case "vehicle":
return 1;
case "prop":
return 2;
default:
return 0;
}
}
// 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) {
return 1;
} else if (tokenB) {
return -1;
} else {
return 0;
}
}
return (
<Group>
{Object.values(mapState.tokens)
.sort((a, b) => sortMapTokenStates(a, b, tokenDraggingOptions))
.map((tokenState) => (
<MapToken
key={tokenState.id}
token={tokensById[tokenState.tokenId]}
tokenState={tokenState}
onTokenStateChange={onMapTokenStateChange}
onTokenMenuOpen={handleTokenMenuOpen}
onTokenDragStart={(e) =>
setTokenDraggingOptions({
dragging: true,
tokenState,
tokenGroup: e.target,
})
}
onTokenDragEnd={() =>
setTokenDraggingOptions({
...tokenDraggingOptions,
dragging: false,
})
}
draggable={
selectedToolId === "pan" &&
!(tokenState.id in disabledTokens) &&
!tokenState.locked
}
mapState={mapState}
fadeOnHover={selectedToolId === "drawing"}
map={map}
/>
))}
</Group>
);
}
export default MapTokens;

View File

@ -7,7 +7,12 @@ import useDataSource from "../../hooks/useDataSource";
import { tokenSources, unknownSource } from "../../tokens";
function ListToken({ token, className }) {
const imageSource = useDataSource(token, tokenSources, unknownSource);
const isDefault = token.type === "default";
const tokenSource = useDataSource(
isDefault ? token : token.thumbnail,
tokenSources,
unknownSource
);
const imageRef = useRef();
// Stop touch to prevent 3d touch gesutre on iOS
@ -16,7 +21,7 @@ function ListToken({ token, className }) {
return (
<Box my={2} mx={3} sx={{ width: "48px", height: "48px" }}>
<Image
src={imageSource}
src={tokenSource}
ref={imageRef}
className={className}
sx={{

View File

@ -17,7 +17,12 @@ function TokenTile({
canEdit,
badges,
}) {
const tokenSource = useDataSource(token, defaultTokenSources, unknownSource);
const isDefault = token.type === "default";
const tokenSource = useDataSource(
isDefault ? token : token.thumbnail,
defaultTokenSources,
unknownSource
);
return (
<Tile

View File

@ -69,6 +69,7 @@ export function MapDataProvider({ children }) {
return defaultMapsWithIds;
}
// Loads maps without the file data to save memory
async function loadMaps() {
let storedMaps = [];
// Try to load maps with worker, fallback to database if failed
@ -77,7 +78,10 @@ export function MapDataProvider({ children }) {
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));
await database.table("maps").each((map) => {
const { file, resolutions, ...rest } = map;
storedMaps.push(rest);
});
}
const sortedMaps = storedMaps.sort((a, b) => b.created - a.created);
const defaultMapsWithIds = await getDefaultMaps();

View File

@ -25,6 +25,10 @@ export function TokenDataProvider({ children }) {
const { database, databaseStatus } = 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);
@ -32,7 +36,7 @@ export function TokenDataProvider({ children }) {
if (!userId || !database || databaseStatus === "loading") {
return;
}
function getDefaultTokes() {
function getDefaultTokens() {
const defaultTokensWithIds = [];
for (let defaultToken of defaultTokens) {
defaultTokensWithIds.push({
@ -45,6 +49,7 @@ 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
@ -53,12 +58,13 @@ export function TokenDataProvider({ children }) {
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));
await database.table("tokens").each((token) => {
const { file, resolutions, ...rest } = token;
storedTokens.push(rest);
});
}
const sortedTokens = storedTokens.sort((a, b) => b.created - a.created);
const defaultTokensWithIds = getDefaultTokes();
const defaultTokensWithIds = getDefaultTokens();
const allTokens = [...sortedTokens, ...defaultTokensWithIds];
setTokens(allTokens);
setTokensLoading(false);
@ -195,6 +201,35 @@ 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;
});
});
}, []);
const ownedTokens = tokens.filter((token) => token.owner === userId);
const tokensById = tokens.reduce((obj, token) => {
@ -215,6 +250,8 @@ export function TokenDataProvider({ children }) {
tokensById,
tokensLoading,
getTokenFromDB,
loadTokens,
unloadTokens,
};
return (

View File

@ -3,6 +3,7 @@ import Dexie from "dexie";
import blobToBuffer from "./helpers/blobToBuffer";
import { getGridDefaultInset } from "./helpers/grid";
import { convertOldActionsToShapes } from "./actions";
import { createThumbnail } from "./helpers/image";
function loadVersions(db) {
// v1.2.0
@ -332,6 +333,52 @@ function loadVersions(db) {
delete state.fogDrawActionIndex;
});
});
async function createDataThumbnail(data) {
const url = URL.createObjectURL(new Blob([data.file]));
return await Dexie.waitFor(
new Promise((resolve) => {
let image = new Image();
image.onload = async () => {
const thumbnail = await createThumbnail(image);
resolve(thumbnail);
};
image.src = url;
})
);
}
db.version(19)
.stores({})
.upgrade(async (tx) => {
const maps = await Dexie.waitFor(tx.table("maps").toArray());
const thumbnails = {};
for (let map of maps) {
thumbnails[map.id] = await createDataThumbnail(map);
}
return tx
.table("maps")
.toCollection()
.modify((map) => {
map.thumbnail = thumbnails[map.id];
});
});
db.version(20)
.stores({})
.upgrade(async (tx) => {
const tokens = await Dexie.waitFor(tx.table("tokens").toArray());
const thumbnails = {};
for (let token of tokens) {
thumbnails[token.id] = await createDataThumbnail(token);
}
return tx
.table("tokens")
.toCollection()
.modify((token) => {
token.thumbnail = thumbnails[token.id];
});
});
}
// Get the dexie database used in DatabaseContext

View File

@ -1,3 +1,5 @@
import blobToBuffer from "./blobToBuffer";
const lightnessDetectionOffset = 0.1;
/**
@ -35,11 +37,19 @@ export function getImageLightness(image) {
return norm + lightnessDetectionOffset >= 0;
}
/**
* @typedef ResizedImage
* @property {Blob} blob
* @property {number} width
* @property {number} height
*/
/**
* @param {HTMLImageElement} image the image to resize
* @param {number} size the size of the longest edge of the new image
* @param {string} type the mime type of the image
* @param {number} quality if image is a jpeg or webp this is the quality setting
* @returns {Promise<ResizedImage>}
*/
export async function resizeImage(image, size, type, quality) {
const width = image.width;
@ -66,3 +76,25 @@ export async function resizeImage(image, size, type, quality) {
);
});
}
export async function createThumbnail(
image,
type,
resolution = 300,
quality = 0.5
) {
const thumbnailImage = await resizeImage(
image,
Math.min(resolution, image.width, image.height),
type,
quality
);
const thumbnailBuffer = await blobToBuffer(thumbnailImage.blob);
return {
file: thumbnailBuffer,
width: thumbnailImage.width,
height: thumbnailImage.height,
type: "file",
id: "thumbnail",
};
}

View File

@ -1,9 +1,10 @@
import React, { useState } from "react";
import React, { useEffect, useState } from "react";
import { Button, Flex, Label } from "theme-ui";
import Modal from "../components/Modal";
import MapSettings from "../components/map/MapSettings";
import MapEditor from "../components/map/MapEditor";
import LoadingOverlay from "../components/LoadingOverlay";
import { useMapData } from "../contexts/MapDataContext";
@ -12,8 +13,28 @@ import { getGridDefaultInset } from "../helpers/grid";
import useResponsiveLayout from "../hooks/useResponsiveLayout";
function EditMapModal({ isOpen, onDone, map, mapState }) {
const { updateMap, updateMapState } = useMapData();
function EditMapModal({ isOpen, onDone, mapId }) {
const { updateMap, updateMapState, getMapFromDB, mapStates } = useMapData();
const [isLoading, setIsLoading] = useState(true);
const [map, setMap] = useState();
const [mapState, setMapState] = useState();
// Load full map when modal is opened
useEffect(() => {
async function loadMap() {
setIsLoading(true);
setMap(await getMapFromDB(mapId));
setMapState(mapStates.find((state) => state.mapId === mapId));
setIsLoading(false);
}
if (isOpen && mapId) {
loadMap();
} else {
setMap();
setMapState();
}
}, [isOpen, mapId, getMapFromDB, mapStates]);
function handleClose() {
setMapSettingChanges({});
@ -114,10 +135,23 @@ function EditMapModal({ isOpen, onDone, map, mapState }) {
<Label pt={2} pb={1}>
Edit map
</Label>
<MapEditor
map={selectedMapWithChanges}
onSettingsChange={handleMapSettingsChange}
/>
{isLoading ? (
<Flex
sx={{
width: "100%",
height: layout.screenSize === "large" ? "500px" : "300px",
position: "relative",
}}
bg="muted"
>
<LoadingOverlay />
</Flex>
) : (
<MapEditor
map={selectedMapWithChanges}
onSettingsChange={handleMapSettingsChange}
/>
)}
<MapSettings
map={selectedMapWithChanges}
mapState={selectedMapStateWithChanges}

View File

@ -1,9 +1,10 @@
import React, { useState } from "react";
import React, { useState, useEffect } from "react";
import { Button, Flex, Label } from "theme-ui";
import Modal from "../components/Modal";
import TokenSettings from "../components/token/TokenSettings";
import TokenPreview from "../components/token/TokenPreview";
import LoadingOverlay from "../components/LoadingOverlay";
import { useTokenData } from "../contexts/TokenDataContext";
@ -11,8 +12,24 @@ import { isEmpty } from "../helpers/shared";
import useResponsiveLayout from "../hooks/useResponsiveLayout";
function EditTokenModal({ isOpen, onDone, token }) {
const { updateToken } = useTokenData();
function EditTokenModal({ isOpen, onDone, tokenId }) {
const { updateToken, getTokenFromDB } = useTokenData();
const [isLoading, setIsLoading] = useState(true);
const [token, setToken] = useState();
useEffect(() => {
async function loadToken() {
setIsLoading(true);
setToken(await getTokenFromDB(tokenId));
setIsLoading(false);
}
if (isOpen && tokenId) {
loadToken();
} else {
setToken();
}
}, [isOpen, tokenId, getTokenFromDB]);
function handleClose() {
setTokenSettingChanges({});
@ -67,7 +84,20 @@ function EditTokenModal({ isOpen, onDone, token }) {
<Label pt={2} pb={1}>
Edit token
</Label>
<TokenPreview token={selectedTokenWithChanges} />
{isLoading ? (
<Flex
sx={{
width: "100%",
height: layout.screenSize === "large" ? "500px" : "300px",
position: "relative",
}}
bg="muted"
>
<LoadingOverlay />
</Flex>
) : (
<TokenPreview token={selectedTokenWithChanges} />
)}
<TokenSettings
token={selectedTokenWithChanges}
onSettingsChange={handleTokenSettingsChange}

View File

@ -13,7 +13,7 @@ import ImageDrop from "../components/ImageDrop";
import LoadingOverlay from "../components/LoadingOverlay";
import blobToBuffer from "../helpers/blobToBuffer";
import { resizeImage } from "../helpers/image";
import { resizeImage, createThumbnail } from "../helpers/image";
import { useSearch, useGroup, handleItemSelect } from "../helpers/select";
import {
getGridDefaultInset,
@ -64,6 +64,7 @@ function SelectMapModal({
updateMap,
updateMaps,
mapsLoading,
getMapFromDB,
} = useMapData();
/**
@ -82,8 +83,10 @@ function SelectMapModal({
const [isGroupModalOpen, setIsGroupModalOpen] = useState(false);
async function handleMapsGroup(group) {
setIsLoading(true);
setIsGroupModalOpen(false);
updateMaps(selectedMapIds, { group });
await updateMaps(selectedMapIds, { group });
setIsLoading(false);
}
const [mapsByGroup, mapGroups] = useGroup(
@ -98,7 +101,7 @@ function SelectMapModal({
*/
const fileInputRef = useRef();
const [imageLoading, setImageLoading] = useState(false);
const [isLoading, setIsLoading] = useState(false);
async function handleImagesUpload(files) {
if (navigator.storage) {
@ -120,7 +123,7 @@ function SelectMapModal({
return Promise.reject();
}
let image = new Image();
setImageLoading(true);
setIsLoading(true);
const buffer = await blobToBuffer(file);
// Copy file to avoid permissions issues
@ -197,11 +200,14 @@ function SelectMapModal({
};
}
}
// Create thumbnail
const thumbnail = await createThumbnail(image, file.type);
handleMapAdd({
// Save as a buffer to send with msgpack
file: buffer,
resolutions,
thumbnail,
name,
type: "file",
grid: {
@ -222,7 +228,7 @@ function SelectMapModal({
owner: userId,
...defaultMapProps,
});
setImageLoading(false);
setIsLoading(false);
URL.revokeObjectURL(url);
resolve();
};
@ -302,14 +308,20 @@ function SelectMapModal({
}
async function handleDone() {
if (imageLoading) {
if (isLoading) {
return;
}
if (selectedMapIds.length === 1) {
// Update last used for cache invalidation
const lastUsed = Date.now();
await updateMap(selectedMapIds[0], { lastUsed });
onMapChange({ ...selectedMaps[0], lastUsed }, selectedMapStates[0]);
const map = selectedMaps[0];
if (map.type === "file") {
await updateMap(map.id, { lastUsed });
const updatedMap = await getMapFromDB(map.id);
onMapChange(updatedMap, selectedMapStates[0]);
} else {
onMapChange(map, selectedMapStates[0]);
}
} else {
onMapChange(null, null);
}
@ -414,7 +426,7 @@ function SelectMapModal({
/>
<Button
variant="primary"
disabled={imageLoading || selectedMapIds.length !== 1}
disabled={isLoading || selectedMapIds.length !== 1}
onClick={handleDone}
mt={2}
>
@ -422,12 +434,11 @@ function SelectMapModal({
</Button>
</Flex>
</ImageDrop>
{(imageLoading || mapsLoading) && <LoadingOverlay bg="overlay" />}
{(isLoading || mapsLoading) && <LoadingOverlay bg="overlay" />}
<EditMapModal
isOpen={isEditModalOpen}
onDone={() => setIsEditModalOpen(false)}
map={selectedMaps.length === 1 && selectedMaps[0]}
mapState={selectedMapStates.length === 1 && selectedMapStates[0]}
mapId={selectedMaps.length === 1 && selectedMaps[0].id}
/>
<EditGroupModal
isOpen={isGroupModalOpen}

View File

@ -14,6 +14,7 @@ import LoadingOverlay from "../components/LoadingOverlay";
import blobToBuffer from "../helpers/blobToBuffer";
import { useSearch, useGroup, handleItemSelect } from "../helpers/select";
import { createThumbnail } from "../helpers/image";
import useResponsiveLayout from "../hooks/useResponsiveLayout";
@ -21,6 +22,9 @@ import { useTokenData } from "../contexts/TokenDataContext";
import { useAuth } from "../contexts/AuthContext";
import { useKeyboard } from "../contexts/KeyboardContext";
// 300 pixels total
const thumbnailResolution = { size: 300, quality: 0.5 };
function SelectTokensModal({ isOpen, onRequestClose }) {
const { userId } = useAuth();
const {
@ -47,8 +51,10 @@ function SelectTokensModal({ isOpen, onRequestClose }) {
const [isGroupModalOpen, setIsGroupModalOpen] = useState(false);
async function handleTokensGroup(group) {
setIsLoading(true);
setIsGroupModalOpen(false);
await updateTokens(selectedTokenIds, { group });
setIsLoading(false);
}
const [tokensByGroup, tokenGroups] = useGroup(
@ -63,7 +69,7 @@ function SelectTokensModal({ isOpen, onRequestClose }) {
*/
const fileInputRef = useRef();
const [imageLoading, setImageLoading] = useState(false);
const [isLoading, setIsLoading] = useState(false);
function openImageDialog() {
if (fileInputRef.current) {
@ -100,7 +106,7 @@ function SelectTokensModal({ isOpen, onRequestClose }) {
name = Case.capital(name);
}
let image = new Image();
setImageLoading(true);
setIsLoading(true);
const buffer = await blobToBuffer(file);
// Copy file to avoid permissions issues
@ -109,9 +115,12 @@ function SelectTokensModal({ isOpen, onRequestClose }) {
const url = URL.createObjectURL(blob);
return new Promise((resolve, reject) => {
image.onload = function () {
image.onload = async function () {
const thumbnail = await createThumbnail(image, file.type);
handleTokenAdd({
file: buffer,
thumbnail,
name,
id: shortid.generate(),
type: "file",
@ -126,7 +135,7 @@ function SelectTokensModal({ isOpen, onRequestClose }) {
width: image.width,
height: image.height,
});
setImageLoading(false);
setIsLoading(false);
resolve();
};
image.onerror = reject;
@ -268,18 +277,18 @@ function SelectTokensModal({ isOpen, onRequestClose }) {
/>
<Button
variant="primary"
disabled={imageLoading}
disabled={isLoading}
onClick={onRequestClose}
>
Done
</Button>
</Flex>
</ImageDrop>
{tokensLoading && <LoadingOverlay bg="overlay" />}
{(isLoading || tokensLoading) && <LoadingOverlay bg="overlay" />}
<EditTokenModal
isOpen={isEditModalOpen}
onDone={() => setIsEditModalOpen(false)}
token={selectedTokens.length === 1 && selectedTokens[0]}
tokenId={selectedTokens.length === 1 && selectedTokens[0].id}
/>
<EditGroupModal
isOpen={isGroupModalOpen}

View File

@ -208,7 +208,7 @@ function NetworkedMapAndTokens({ session }) {
}
}, [currentMap, debouncedMapState, userId, database, updateMapState]);
function handleMapChange(newMap, newMapState) {
async function handleMapChange(newMap, newMapState) {
// Clear map before sending new one
setCurrentMap(null);
session.socket?.emit("map", null);
@ -217,17 +217,16 @@ function NetworkedMapAndTokens({ session }) {
setCurrentMap(newMap);
if (newMap && newMap.type === "file") {
const { file, resolutions, ...rest } = newMap;
const { file, resolutions, thumbnail, ...rest } = newMap;
session.socket?.emit("map", rest);
} else {
session.socket?.emit("map", newMap);
}
if (!newMap || !newMapState) {
return;
}
loadAssetManifestFromMap(newMap, newMapState);
await loadAssetManifestFromMap(newMap, newMapState);
}
function handleMapStateChange(newMapState) {
@ -406,6 +405,7 @@ function NetworkedMapAndTokens({ session }) {
...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,

View File

@ -9,9 +9,10 @@ let service = {
/**
* Load either a whole table or individual item from the DB
* @param {string} table Table to load from
* @param {string|undefined} key Optional database key to load, if undefined whole table will be loaded
* @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) {
async loadData(table, key, excludeFiles = true) {
try {
let db = getDatabase({});
if (key) {
@ -21,7 +22,14 @@ let service = {
// Load entire table
let items = [];
// Use a cursor instead of toArray to prevent IPC max size error
await db.table(table).each((item) => items.push(item));
await db.table(table).each((item) => {
if (excludeFiles) {
const { file, resolutions, ...rest } = item;
items.push(rest);
} else {
items.push(item);
}
});
// Pack data with msgpack so we can use transfer to avoid memory issues
const packed = encode(items);