Fix token and map editing and viewing

This commit is contained in:
Mitchell McCaffrey 2021-04-23 11:48:24 +10:00
parent 6b9665ffe8
commit a023ef61ed
6 changed files with 155 additions and 98 deletions

View File

@ -20,9 +20,15 @@ import { omit } from "../helpers/shared";
* @returns {Promise<Asset|undefined>} * @returns {Promise<Asset|undefined>}
*/ */
/**
* @callback addAssets
* @param {Asset[]} assets
*/
/** /**
* @typedef AssetsContext * @typedef AssetsContext
* @property {getAsset} getAsset * @property {getAsset} getAsset
* @property {addAssets} addAssets
*/ */
/** /**
@ -31,7 +37,7 @@ import { omit } from "../helpers/shared";
const AssetsContext = React.createContext(); const AssetsContext = React.createContext();
export function AssetsProvider({ children }) { export function AssetsProvider({ children }) {
const { worker } = useDatabase(); const { worker, database } = useDatabase();
const getAsset = useCallback( const getAsset = useCallback(
async (assetId) => { async (assetId) => {
@ -41,10 +47,20 @@ export function AssetsProvider({ children }) {
[worker] [worker]
); );
const addAssets = useCallback(
async (assets) => {
return database.table("assets").bulkAdd(assets);
},
[database]
);
const value = {
getAsset,
addAssets,
};
return ( return (
<AssetsContext.Provider value={{ getAsset }}> <AssetsContext.Provider value={value}>{children}</AssetsContext.Provider>
{children}
</AssetsContext.Provider>
); );
} }
@ -107,10 +123,10 @@ export function AssetURLsProvider({ children }) {
* @param {string} assetId * @param {string} assetId
* @param {"file"|"default"} type * @param {"file"|"default"} type
* @param {Object.<string, string>} defaultSources * @param {Object.<string, string>} defaultSources
* @param {string} unknownSource * @param {string|undefined} unknownSource
* @returns {string} * @returns {string|undefined}
*/ */
export function useAssetURL(assetId, type, defaultSources, unknownSource = "") { export function useAssetURL(assetId, type, defaultSources, unknownSource) {
const assetURLs = useContext(AssetURLsStateContext); const assetURLs = useContext(AssetURLsStateContext);
if (assetURLs === undefined) { if (assetURLs === undefined) {
throw new Error("useAssetURL must be used within a AssetURLsProvider"); throw new Error("useAssetURL must be used within a AssetURLsProvider");
@ -183,7 +199,7 @@ export function useAssetURL(assetId, type, defaultSources, unknownSource = "") {
} }
if (type === "file") { if (type === "file") {
return assetURLs[assetId]?.url; return assetURLs[assetId]?.url || unknownSource;
} }
return unknownSource; return unknownSource;
@ -210,14 +226,14 @@ const dataResolutions = ["ultra", "high", "medium", "low"];
* Load a map or token into a URL taking into account a thumbnail and multiple resolutions * Load a map or token into a URL taking into account a thumbnail and multiple resolutions
* @param {FileData|DefaultData} data * @param {FileData|DefaultData} data
* @param {Object.<string, string>} defaultSources * @param {Object.<string, string>} defaultSources
* @param {string} unknownSource * @param {string|undefined} unknownSource
* @param {boolean} thumbnail * @param {boolean} thumbnail
* @returns {string} * @returns {string|undefined}
*/ */
export function useDataURL( export function useDataURL(
data, data,
defaultSources, defaultSources,
unknownSource = "", unknownSource,
thumbnail = false thumbnail = false
) { ) {
const { database } = useDatabase(); const { database } = useDatabase();

View File

@ -509,14 +509,10 @@ const versions = {
if (asset.prevType === "map") { if (asset.prevType === "map") {
tx.table("maps").update(asset.prevId, { tx.table("maps").update(asset.prevId, {
file: asset.id, file: asset.id,
width: undefined,
height: undefined,
}); });
} else if (asset.prevType === "token") { } else if (asset.prevType === "token") {
tx.table("tokens").update(asset.prevId, { tx.table("tokens").update(asset.prevId, {
file: asset.id, file: asset.id,
width: undefined,
height: undefined,
}); });
} else if (asset.prevType === "mapThumbnail") { } else if (asset.prevType === "mapThumbnail") {
tx.table("maps").update(asset.prevId, { thumbnail: asset.id }); tx.table("maps").update(asset.prevId, { thumbnail: asset.id });

View File

@ -43,6 +43,7 @@ import {
GridStrokeWidthContext, GridStrokeWidthContext,
GridCellPixelOffsetContext, GridCellPixelOffsetContext,
} from "../contexts/GridContext"; } from "../contexts/GridContext";
import DatabaseContext, { useDatabase } from "../contexts/DatabaseContext";
/** /**
* Provide a bridge for konva that forwards our contexts * Provide a bridge for konva that forwards our contexts
@ -74,72 +75,78 @@ function KonvaBridge({ stageRender, children }) {
const gridCellPixelOffset = useGridCellPixelOffset(); const gridCellPixelOffset = useGridCellPixelOffset();
const gridOffset = useGridOffset(); const gridOffset = useGridOffset();
const database = useDatabase();
return stageRender( return stageRender(
<AuthContext.Provider value={auth}> <DatabaseContext.Provider value={database}>
<SettingsContext.Provider value={settings}> <AuthContext.Provider value={auth}>
<KeyboardContext.Provider value={keyboardValue}> <SettingsContext.Provider value={settings}>
<MapStageProvider value={mapStageRef}> <KeyboardContext.Provider value={keyboardValue}>
<AssetsContext.Provider value={assets}> <MapStageProvider value={mapStageRef}>
<AssetURLsStateContext.Provider value={assetURLs}> <AssetsContext.Provider value={assets}>
<AssetURLsUpdaterContext.Provider value={setAssetURLs}> <AssetURLsStateContext.Provider value={assetURLs}>
<TokenDataContext.Provider value={tokenData}> <AssetURLsUpdaterContext.Provider value={setAssetURLs}>
<InteractionEmitterContext.Provider <TokenDataContext.Provider value={tokenData}>
value={interactionEmitter} <InteractionEmitterContext.Provider
> value={interactionEmitter}
<SetPreventMapInteractionContext.Provider
value={setPreventMapInteraction}
> >
<StageWidthContext.Provider value={stageWidth}> <SetPreventMapInteractionContext.Provider
<StageHeightContext.Provider value={stageHeight}> value={setPreventMapInteraction}
<MapWidthContext.Provider value={mapWidth}> >
<MapHeightContext.Provider value={mapHeight}> <StageWidthContext.Provider value={stageWidth}>
<StageScaleContext.Provider value={stageScale}> <StageHeightContext.Provider value={stageHeight}>
<DebouncedStageScaleContext.Provider <MapWidthContext.Provider value={mapWidth}>
value={debouncedStageScale} <MapHeightContext.Provider value={mapHeight}>
<StageScaleContext.Provider
value={stageScale}
> >
<GridContext.Provider value={grid}> <DebouncedStageScaleContext.Provider
<GridPixelSizeContext.Provider value={debouncedStageScale}
value={gridPixelSize} >
> <GridContext.Provider value={grid}>
<GridCellPixelSizeContext.Provider <GridPixelSizeContext.Provider
value={gridCellPixelSize} value={gridPixelSize}
> >
<GridCellNormalizedSizeContext.Provider <GridCellPixelSizeContext.Provider
value={gridCellNormalizedSize} value={gridCellPixelSize}
> >
<GridOffsetContext.Provider <GridCellNormalizedSizeContext.Provider
value={gridOffset} value={gridCellNormalizedSize}
> >
<GridStrokeWidthContext.Provider <GridOffsetContext.Provider
value={gridStrokeWidth} value={gridOffset}
> >
<GridCellPixelOffsetContext.Provider <GridStrokeWidthContext.Provider
value={gridCellPixelOffset} value={gridStrokeWidth}
> >
{children} <GridCellPixelOffsetContext.Provider
</GridCellPixelOffsetContext.Provider> value={gridCellPixelOffset}
</GridStrokeWidthContext.Provider> >
</GridOffsetContext.Provider> {children}
</GridCellNormalizedSizeContext.Provider> </GridCellPixelOffsetContext.Provider>
</GridCellPixelSizeContext.Provider> </GridStrokeWidthContext.Provider>
</GridPixelSizeContext.Provider> </GridOffsetContext.Provider>
</GridContext.Provider> </GridCellNormalizedSizeContext.Provider>
</DebouncedStageScaleContext.Provider> </GridCellPixelSizeContext.Provider>
</StageScaleContext.Provider> </GridPixelSizeContext.Provider>
</MapHeightContext.Provider> </GridContext.Provider>
</MapWidthContext.Provider> </DebouncedStageScaleContext.Provider>
</StageHeightContext.Provider> </StageScaleContext.Provider>
</StageWidthContext.Provider> </MapHeightContext.Provider>
</SetPreventMapInteractionContext.Provider> </MapWidthContext.Provider>
</InteractionEmitterContext.Provider> </StageHeightContext.Provider>
</TokenDataContext.Provider> </StageWidthContext.Provider>
</AssetURLsUpdaterContext.Provider> </SetPreventMapInteractionContext.Provider>
</AssetURLsStateContext.Provider> </InteractionEmitterContext.Provider>
</AssetsContext.Provider> </TokenDataContext.Provider>
</MapStageProvider> </AssetURLsUpdaterContext.Provider>
</KeyboardContext.Provider> </AssetURLsStateContext.Provider>
</SettingsContext.Provider> </AssetsContext.Provider>
</AuthContext.Provider> </MapStageProvider>
</KeyboardContext.Provider>
</SettingsContext.Provider>
</AuthContext.Provider>
</DatabaseContext.Provider>
); );
} }

View File

@ -1,3 +1,5 @@
import { v4 as uuid } from "uuid";
import blobToBuffer from "./blobToBuffer"; import blobToBuffer from "./blobToBuffer";
const lightnessDetectionOffset = 0.1; const lightnessDetectionOffset = 0.1;
@ -88,12 +90,12 @@ export async function resizeImage(image, size, type, quality) {
} }
/** /**
* @typedef ImageFile * @typedef Asset
* @property {Uint8Array|null} file * @property {string} id
* @property {number} width * @property {number} width
* @property {number} height * @property {number} height
* @property {"file"} type * @property {Uint8Array} file
* @property {string} id * @property {string} mime
*/ */
/** /**
@ -102,7 +104,7 @@ export async function resizeImage(image, size, type, quality) {
* @param {string} type the mime type of the image * @param {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<ImageFile>} * @returns {Promise<Asset>}
*/ */
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");
@ -150,7 +152,7 @@ export async function createThumbnail(image, type, size = 300, quality = 0.5) {
file: thumbnailBuffer, file: thumbnailBuffer,
width: thumbnailImage.width, width: thumbnailImage.width,
height: thumbnailImage.height, height: thumbnailImage.height,
type: "file", mime: type,
id: "thumbnail", id: uuid(),
}; };
} }

View File

@ -1,6 +1,6 @@
import React, { useRef, useState } from "react"; import React, { useRef, useState } from "react";
import { Button, Flex, Label } from "theme-ui"; import { Button, Flex, Label } from "theme-ui";
import shortid from "shortid"; import { v4 as uuid } from "uuid";
import Case from "case"; import Case from "case";
import { useToasts } from "react-toast-notifications"; import { useToasts } from "react-toast-notifications";
@ -28,6 +28,7 @@ import useResponsiveLayout from "../hooks/useResponsiveLayout";
import { useMapData } from "../contexts/MapDataContext"; import { useMapData } from "../contexts/MapDataContext";
import { useAuth } from "../contexts/AuthContext"; import { useAuth } from "../contexts/AuthContext";
import { useKeyboard, useBlur } from "../contexts/KeyboardContext"; import { useKeyboard, useBlur } from "../contexts/KeyboardContext";
import { useAssets } from "../contexts/AssetsContext";
import shortcuts from "../shortcuts"; import shortcuts from "../shortcuts";
@ -72,6 +73,7 @@ function SelectMapModal({
getMapFromDB, getMapFromDB,
getMapStateFromDB, getMapStateFromDB,
} = useMapData(); } = useMapData();
const { addAssets } = useAssets();
/** /**
* Search * Search
@ -221,6 +223,8 @@ function SelectMapModal({
gridSize = { x: 22, y: 22 }; gridSize = { x: 22, y: 22 };
} }
let assets = [];
// Create resolutions // Create resolutions
const resolutions = {}; const resolutions = {};
for (let resolution of mapResolutions) { for (let resolution of mapResolutions) {
@ -239,26 +243,38 @@ function SelectMapModal({
resolution.quality resolution.quality
); );
if (resized.blob) { if (resized.blob) {
const assetId = uuid();
resolutions[resolution.id] = assetId;
const resizedBuffer = await blobToBuffer(resized.blob); const resizedBuffer = await blobToBuffer(resized.blob);
resolutions[resolution.id] = { const asset = {
file: resizedBuffer, file: resizedBuffer,
width: resized.width, width: resized.width,
height: resized.height, height: resized.height,
type: "file", id: assetId,
id: resolution.id, mime: file.type,
}; };
assets.push(asset);
} }
} }
} }
// Create thumbnail // Create thumbnail
const thumbnail = await createThumbnail(image, file.type); const thumbnail = await createThumbnail(image, file.type);
assets.push(thumbnail);
handleMapAdd({ const fileAsset = {
// Save as a buffer to send with msgpack id: uuid(),
file: buffer, file: buffer,
resolutions, width: image.width,
thumbnail, height: image.height,
mime: file.type,
};
assets.push(fileAsset);
const map = {
name, name,
resolutions,
file: fileAsset.id,
thumbnail: thumbnail.id,
type: "file", type: "file",
grid: { grid: {
size: gridSize, size: gridSize,
@ -275,13 +291,15 @@ function SelectMapModal({
}, },
width: image.width, width: image.width,
height: image.height, height: image.height,
id: shortid.generate(), id: uuid(),
created: Date.now(), created: Date.now(),
lastModified: Date.now(), lastModified: Date.now(),
lastUsed: Date.now(), lastUsed: Date.now(),
owner: userId, owner: userId,
...defaultMapProps, ...defaultMapProps,
}); };
handleMapAdd(map, assets);
setIsLoading(false); setIsLoading(false);
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
resolve(); resolve();
@ -311,8 +329,9 @@ function SelectMapModal({
selectedMapIds.includes(state.mapId) selectedMapIds.includes(state.mapId)
); );
async function handleMapAdd(map) { async function handleMapAdd(map, assets) {
await addMap(map); await addMap(map);
await addAssets(assets);
setSelectedMapIds([map.id]); setSelectedMapIds([map.id]);
} }

View File

@ -1,6 +1,6 @@
import React, { useRef, useState } from "react"; import React, { useRef, useState } from "react";
import { Flex, Label, Button } from "theme-ui"; import { Flex, Label, Button } from "theme-ui";
import shortid from "shortid"; import { v4 as uuid } from "uuid";
import Case from "case"; import Case from "case";
import { useToasts } from "react-toast-notifications"; import { useToasts } from "react-toast-notifications";
@ -22,6 +22,7 @@ import useResponsiveLayout from "../hooks/useResponsiveLayout";
import { useTokenData } from "../contexts/TokenDataContext"; import { useTokenData } from "../contexts/TokenDataContext";
import { useAuth } from "../contexts/AuthContext"; import { useAuth } from "../contexts/AuthContext";
import { useKeyboard, useBlur } from "../contexts/KeyboardContext"; import { useKeyboard, useBlur } from "../contexts/KeyboardContext";
import { useAssets } from "../contexts/AssetsContext";
import shortcuts from "../shortcuts"; import shortcuts from "../shortcuts";
@ -36,6 +37,7 @@ function SelectTokensModal({ isOpen, onRequestClose }) {
updateTokens, updateTokens,
tokensLoading, tokensLoading,
} = useTokenData(); } = useTokenData();
const { addAssets } = useAssets();
/** /**
* Search * Search
@ -160,13 +162,24 @@ function SelectTokensModal({ isOpen, onRequestClose }) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
image.onload = async function () { image.onload = async function () {
let assets = [];
const thumbnail = await createThumbnail(image, file.type); const thumbnail = await createThumbnail(image, file.type);
assets.push(thumbnail);
handleTokenAdd({ const fileAsset = {
id: uuid(),
file: buffer, file: buffer,
thumbnail, width: image.width,
height: image.height,
mime: file.type,
};
assets.push(fileAsset);
const token = {
name, name,
id: shortid.generate(), thumbnail: thumbnail.id,
file: fileAsset.id,
id: uuid(),
type: "file", type: "file",
created: Date.now(), created: Date.now(),
lastModified: Date.now(), lastModified: Date.now(),
@ -178,8 +191,11 @@ function SelectTokensModal({ isOpen, onRequestClose }) {
group: "", group: "",
width: image.width, width: image.width,
height: image.height, height: image.height,
}); };
handleTokenAdd(token, assets);
setIsLoading(false); setIsLoading(false);
URL.revokeObjectURL(url);
resolve(); resolve();
}; };
image.onerror = reject; image.onerror = reject;
@ -196,8 +212,9 @@ function SelectTokensModal({ isOpen, onRequestClose }) {
selectedTokenIds.includes(token.id) selectedTokenIds.includes(token.id)
); );
function handleTokenAdd(token) { async function handleTokenAdd(token, assets) {
addToken(token); await addToken(token);
await addAssets(assets);
setSelectedTokenIds([token.id]); setSelectedTokenIds([token.id]);
} }