Update maps and tokens to have thumbnails and removed loading of all files on load
This commit is contained in:
parent
b9993e1a0b
commit
24e64f9d32
@ -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 = (
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
);
|
||||
|
131
src/components/map/MapTokens.js
Normal file
131
src/components/map/MapTokens.js
Normal 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;
|
@ -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={{
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
|
@ -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 (
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
};
|
||||
}
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user