Moved assets into new table in the database

This commit is contained in:
Mitchell McCaffrey 2021-04-22 16:53:35 +10:00
parent d620463c15
commit 9f11161b23
17 changed files with 544 additions and 297 deletions

View File

@ -57,6 +57,7 @@
"source-map-explorer": "^2.5.2",
"theme-ui": "^0.3.1",
"use-image": "^1.0.7",
"uuid": "^8.3.2",
"webrtc-adapter": "^7.7.1"
},
"resolutions": {

View File

@ -18,7 +18,7 @@ import { TokenDataProvider } from "./contexts/TokenDataContext";
import { MapLoadingProvider } from "./contexts/MapLoadingContext";
import { SettingsProvider } from "./contexts/SettingsContext";
import { KeyboardProvider } from "./contexts/KeyboardContext";
import { ImageSourcesProvider } from "./contexts/ImageSourceContext";
import { AssetsProvider, AssetURLsProvider } from "./contexts/AssetsContext";
import { ToastProvider } from "./components/Toast";
@ -30,40 +30,42 @@ function App() {
<AuthProvider>
<KeyboardProvider>
<ToastProvider>
<ImageSourcesProvider>
<Router>
<Switch>
<Route path="/donate">
<Donate />
</Route>
{/* Legacy support camel case routes */}
<Route path={["/howTo", "/how-to"]}>
<HowTo />
</Route>
<Route path={["/releaseNotes", "/release-notes"]}>
<ReleaseNotes />
</Route>
<Route path="/about">
<About />
</Route>
<Route path="/faq">
<FAQ />
</Route>
<Route path="/game/:id">
<MapLoadingProvider>
<MapDataProvider>
<TokenDataProvider>
<Game />
</TokenDataProvider>
</MapDataProvider>
</MapLoadingProvider>
</Route>
<Route path="/">
<Home />
</Route>
</Switch>
</Router>
</ImageSourcesProvider>
<Router>
<Switch>
<Route path="/donate">
<Donate />
</Route>
{/* Legacy support camel case routes */}
<Route path={["/howTo", "/how-to"]}>
<HowTo />
</Route>
<Route path={["/releaseNotes", "/release-notes"]}>
<ReleaseNotes />
</Route>
<Route path="/about">
<About />
</Route>
<Route path="/faq">
<FAQ />
</Route>
<Route path="/game/:id">
<AssetsProvider>
<AssetURLsProvider>
<MapLoadingProvider>
<MapDataProvider>
<TokenDataProvider>
<Game />
</TokenDataProvider>
</MapDataProvider>
</MapLoadingProvider>
</AssetURLsProvider>
</AssetsProvider>
</Route>
<Route path="/">
<Home />
</Route>
</Switch>
</Router>
</ToastProvider>
</KeyboardProvider>
</AuthProvider>

View File

@ -23,7 +23,7 @@ import MapGrid from "./MapGrid";
import MapGridEditor from "./MapGridEditor";
function MapEditor({ map, onSettingsChange }) {
const [mapImageSource] = useMapImage(map);
const [mapImage] = useMapImage(map);
const [stageWidth, setStageWidth] = useState(1);
const [stageHeight, setStageHeight] = useState(1);
@ -132,11 +132,7 @@ function MapEditor({ map, onSettingsChange }) {
)}
>
<Layer ref={mapLayerRef}>
<Image
image={mapImageSource}
width={mapWidth}
height={mapHeight}
/>
<Image image={mapImage} width={mapWidth} height={mapHeight} />
{showGridControls && canEditGrid && (
<>
<MapGrid map={map} />

View File

@ -1,7 +1,7 @@
import React, { useEffect, useState } from "react";
import useImage from "use-image";
import { useImageSource } from "../../contexts/ImageSourceContext";
import { useDataURL } from "../../contexts/AssetsContext";
import { mapSources as defaultMapSources } from "../../maps";
@ -13,13 +13,14 @@ function MapGrid({ map }) {
let mapSourceMap = map;
// Use lowest resolution for grid lightness
if (map && map.type === "file" && map.resolutions) {
// FIXME - move to resolutions array
const resolutionArray = Object.keys(map.resolutions);
if (resolutionArray.length > 0) {
mapSourceMap = map.resolutions[resolutionArray[0]];
mapSourceMap.quality = resolutionArray[0];
}
}
const mapSource = useImageSource(mapSourceMap, defaultMapSources);
const [mapImage, mapLoadingStatus] = useImage(mapSource);
const mapURL = useDataURL(mapSourceMap, defaultMapSources);
const [mapImage, mapLoadingStatus] = useImage(mapURL);
const [isImageLight, setIsImageLight] = useState(true);

View File

@ -28,7 +28,7 @@ function MapInteraction({
onSelectedToolChange,
disabledControls,
}) {
const [mapImageSource, mapImageSourceStatus] = useMapImage(map);
const [mapImage, mapImageStatus] = useMapImage(map);
// Map loaded taking in to account different resolutions
const [mapLoaded, setMapLoaded] = useState(false);
@ -36,14 +36,15 @@ function MapInteraction({
if (
!map ||
!mapState ||
// FIXME
(map.type === "file" && !map.file && !map.resolutions) ||
mapState.mapId !== map.id
) {
setMapLoaded(false);
} else if (mapImageSourceStatus === "loaded") {
} else if (mapImageStatus === "loaded") {
setMapLoaded(true);
}
}, [mapImageSourceStatus, map, mapState]);
}, [mapImageStatus, map, mapState]);
const [stageWidth, setStageWidth] = useState(1);
const [stageHeight, setStageHeight] = useState(1);
@ -211,7 +212,7 @@ function MapInteraction({
>
<Layer ref={mapLayerRef}>
<Image
image={mapLoaded && mapImageSource}
image={mapLoaded && mapImage}
width={mapWidth}
height={mapHeight}
id="mapImage"

View File

@ -2,7 +2,7 @@ import React from "react";
import Tile from "../Tile";
import { useImageSource } from "../../contexts/ImageSourceContext";
import { useDataURL } from "../../contexts/AssetsContext";
import { mapSources as defaultMapSources, unknownSource } from "../../maps";
function MapTile({
@ -15,7 +15,7 @@ function MapTile({
canEdit,
badges,
}) {
const mapSource = useImageSource(
const mapURL = useDataURL(
map,
defaultMapSources,
unknownSource,
@ -24,7 +24,7 @@ function MapTile({
return (
<Tile
src={mapSource}
src={mapURL}
title={map.name}
isSelected={isSelected}
onSelect={() => onMapSelect(map)}

View File

@ -16,7 +16,7 @@ import {
useDebouncedStageScale,
} from "../../contexts/MapInteractionContext";
import { useGridCellPixelSize } from "../../contexts/GridContext";
import { useImageSource } from "../../contexts/ImageSourceContext";
import { useDataURL } from "../../contexts/AssetsContext";
import TokenStatus from "../token/TokenStatus";
import TokenLabel from "../token/TokenLabel";
@ -43,7 +43,7 @@ function MapToken({
const gridCellPixelSize = useGridCellPixelSize();
const tokenSource = useImageSource(token, tokenSources, unknownSource);
const tokenSource = useDataURL(token, tokenSources, unknownSource);
const [tokenSourceImage, tokenSourceStatus] = useImage(tokenSource);
const [tokenAspectRatio, setTokenAspectRatio] = useState(1);

View File

@ -3,12 +3,12 @@ import { Box, Image } from "theme-ui";
import usePreventTouch from "../../hooks/usePreventTouch";
import { useImageSource } from "../../contexts/ImageSourceContext";
import { useDataURL } from "../../contexts/AssetsContext";
import { tokenSources, unknownSource } from "../../tokens";
function ListToken({ token, className }) {
const tokenSource = useImageSource(
const tokenURL = useDataURL(
token,
tokenSources,
unknownSource,
@ -22,7 +22,7 @@ function ListToken({ token, className }) {
return (
<Box my={2} mx={3} sx={{ width: "48px", height: "48px" }}>
<Image
src={tokenSource}
src={tokenURL}
ref={imageRef}
className={className}
sx={{

View File

@ -10,7 +10,7 @@ import useImageCenter from "../../hooks/useImageCenter";
import useResponsiveLayout from "../../hooks/useResponsiveLayout";
import { GridProvider } from "../../contexts/GridContext";
import { useImageSource } from "../../contexts/ImageSourceContext";
import { useDataURL } from "../../contexts/AssetsContext";
import GridOnIcon from "../../icons/GridOnIcon";
import GridOffIcon from "../../icons/GridOffIcon";
@ -27,12 +27,8 @@ function TokenPreview({ token }) {
}
}, [token, tokenSourceData]);
const tokenSource = useImageSource(
tokenSourceData,
tokenSources,
unknownSource
);
const [tokenSourceImage] = useImage(tokenSource);
const tokenURL = useDataURL(tokenSourceData, tokenSources, unknownSource);
const [tokenSourceImage] = useImage(tokenURL);
const [stageWidth, setStageWidth] = useState(1);
const [stageHeight, setStageHeight] = useState(1);

View File

@ -2,7 +2,7 @@ import React from "react";
import Tile from "../Tile";
import { useImageSource } from "../../contexts/ImageSourceContext";
import { useAssetURL } from "../../contexts/AssetsContext";
import {
tokenSources as defaultTokenSources,
@ -18,7 +18,7 @@ function TokenTile({
canEdit,
badges,
}) {
const tokenSource = useImageSource(
const tokenURL = useAssetURL(
token,
defaultTokenSources,
unknownSource,
@ -27,7 +27,7 @@ function TokenTile({
return (
<Tile
src={tokenSource}
src={tokenURL}
title={token.name}
isSelected={isSelected}
onSelect={() => onTokenSelect(token)}

View File

@ -0,0 +1,273 @@
import React, { useState, useContext, useCallback, useEffect } from "react";
import { decode } from "@msgpack/msgpack";
import { useDatabase } from "./DatabaseContext";
import { omit } from "../helpers/shared";
/**
* @typedef Asset
* @property {string} id
* @property {number} width
* @property {number} height
* @property {Uint8Array} file
* @property {string} mime
*/
/**
* @callback getAsset
* @param {string} assetId
* @returns {Promise<Asset|undefined>}
*/
/**
* @typedef AssetsContext
* @property {getAsset} getAsset
*/
/**
* @type {React.Context<undefined|AssetsContext>}
*/
const AssetsContext = React.createContext();
export function AssetsProvider({ children }) {
const { worker } = useDatabase();
const getAsset = useCallback(
async (assetId) => {
const packed = await worker.loadData("assets", assetId);
return decode(packed);
},
[worker]
);
return (
<AssetsContext.Provider value={{ getAsset }}>
{children}
</AssetsContext.Provider>
);
}
export function useAssets() {
const context = useContext(AssetsContext);
if (context === undefined) {
throw new Error("useAssets must be used within a AssetsProvider");
}
return context;
}
/**
* @typedef AssetURL
* @property {string} url
* @property {string} id
* @property {number} references
*/
/**
* @type React.Context<undefined|Object.<string, AssetURL>>
*/
export const AssetURLsStateContext = React.createContext();
/**
* @type React.Context<undefined|React.Dispatch<React.SetStateAction<{}>>>
*/
export const AssetURLsUpdaterContext = React.createContext();
/**
* Helper to manage sharing of custom image sources between uses of useAssetURL
*/
export function AssetURLsProvider({ children }) {
const [assetURLs, setAssetURLs] = useState({});
// Revoke url when no more references
useEffect(() => {
let urlsToCleanup = [];
for (let url of Object.values(assetURLs)) {
if (url.references <= 0) {
URL.revokeObjectURL(url.url);
urlsToCleanup.push(url.id);
}
}
if (urlsToCleanup.length > 0) {
setAssetURLs((prevURLs) => omit(prevURLs, urlsToCleanup));
}
}, [assetURLs]);
return (
<AssetURLsStateContext.Provider value={assetURLs}>
<AssetURLsUpdaterContext.Provider value={setAssetURLs}>
{children}
</AssetURLsUpdaterContext.Provider>
</AssetURLsStateContext.Provider>
);
}
/**
* Helper function to load either file or default asset into a URL
* @param {string} assetId
* @param {"file"|"default"} type
* @param {Object.<string, string>} defaultSources
* @param {string} unknownSource
* @returns {string}
*/
export function useAssetURL(assetId, type, defaultSources, unknownSource = "") {
const assetURLs = useContext(AssetURLsStateContext);
if (assetURLs === undefined) {
throw new Error("useAssetURL must be used within a AssetURLsProvider");
}
const setAssetURLs = useContext(AssetURLsUpdaterContext);
if (setAssetURLs === undefined) {
throw new Error("useAssetURL must be used within a AssetURLsProvider");
}
const { getAsset } = useAssets();
useEffect(() => {
if (!assetId || type !== "file") {
return;
}
async function updateAssetURL() {
const asset = await getAsset(assetId);
if (asset) {
setAssetURLs((prevURLs) => {
if (assetId in prevURLs) {
// Check if the asset url is already added
return {
...prevURLs,
[assetId]: {
...prevURLs[assetId],
// Increase references
references: prevURLs[assetId].references + 1,
},
};
} else {
const url = URL.createObjectURL(
new Blob([asset.file], { type: asset.mime })
);
return {
...prevURLs,
[assetId]: { url, id: assetId, references: 1 },
};
}
});
}
}
updateAssetURL();
return () => {
// Decrease references
setAssetURLs((prevURLs) => {
if (assetId in prevURLs) {
return {
...prevURLs,
[assetId]: {
...prevURLs[assetId],
references: prevURLs[assetId].references - 1,
},
};
} else {
return prevURLs;
}
});
};
}, [assetId, setAssetURLs, getAsset, type]);
if (!assetId) {
return unknownSource;
}
if (type === "default") {
return defaultSources[assetId];
}
if (type === "file") {
return assetURLs[assetId]?.url;
}
return unknownSource;
}
const dataResolutions = ["ultra", "high", "medium", "low"];
/**
* @typedef FileData
* @property {string} file
* @property {"file"} type
* @property {string} thumbnail
* @property {string=} quality
* @property {Object.<string, string>=} resolutions
*/
/**
* @typedef DefaultData
* @property {string} key
* @property {"default"} type
*/
/**
* Load a map or token into a URL taking into account a thumbnail and multiple resolutions
* @param {FileData|DefaultData} data
* @param {Object.<string, string>} defaultSources
* @param {string} unknownSource
* @param {boolean} thumbnail
* @returns {string}
*/
export function useDataURL(
data,
defaultSources,
unknownSource = "",
thumbnail = false
) {
const { database } = useDatabase();
const [assetId, setAssetId] = useState();
useEffect(() => {
if (!data) {
return;
}
async function loastAssetId() {
if (data.type === "default") {
setAssetId(data.key);
} else {
if (thumbnail) {
setAssetId(data.thumbnail);
} else if (data.resolutions) {
const fileKeys = await database
.table("assets")
.where("id")
.equals(data.file)
.primaryKeys();
const fileExists = fileKeys.length > 0;
// Check if a resolution is specified
if (data.quality && data.resolutions[data.quality]) {
setAssetId(data.resolutions[data.quality]);
}
// If no file available fallback to the highest resolution
else if (!fileExists) {
for (let res of dataResolutions) {
if (res in data.resolutions) {
setAssetId(data.resolutions[res]);
break;
}
}
} else {
setAssetId(data.file);
}
} else {
setAssetId(data.file);
}
}
}
loastAssetId();
}, [data, thumbnail, database]);
const type = data?.type || "default";
const assetURL = useAssetURL(assetId, type, defaultSources, unknownSource);
return assetURL;
}
export default AssetsContext;

View File

@ -1,157 +0,0 @@
import React, { useContext, useState, useEffect } from "react";
import { omit } from "../helpers/shared";
export const ImageSourcesStateContext = React.createContext();
export const ImageSourcesUpdaterContext = React.createContext(() => {});
/**
* Helper to manage sharing of custom image sources between uses of useImageSource
*/
export function ImageSourcesProvider({ children }) {
const [imageSources, setImageSources] = useState({});
// Revoke url when no more references
useEffect(() => {
let sourcesToCleanup = [];
for (let source of Object.values(imageSources)) {
if (source.references <= 0) {
URL.revokeObjectURL(source.url);
sourcesToCleanup.push(source.id);
}
}
if (sourcesToCleanup.length > 0) {
setImageSources((prevSources) => omit(prevSources, sourcesToCleanup));
}
}, [imageSources]);
return (
<ImageSourcesStateContext.Provider value={imageSources}>
<ImageSourcesUpdaterContext.Provider value={setImageSources}>
{children}
</ImageSourcesUpdaterContext.Provider>
</ImageSourcesStateContext.Provider>
);
}
/**
* Get id from image data
*/
function getImageFileId(data, thumbnail) {
if (thumbnail) {
return `${data.id}-thumbnail`;
}
if (data.resolutions) {
// Check is a resolution is specified
if (data.quality && data.resolutions[data.quality]) {
return `${data.id}-${data.quality}`;
} else if (!data.file) {
// Fallback to the highest resolution
const resolutionArray = Object.keys(data.resolutions);
const resolution = resolutionArray[resolutionArray.length - 1];
return `${data.id}-${resolution.id}`;
}
}
return data.id;
}
/**
* Helper function to load either file or default image into a URL
*/
export function useImageSource(data, defaultSources, unknownSource, thumbnail) {
const imageSources = useContext(ImageSourcesStateContext);
if (imageSources === undefined) {
throw new Error(
"useImageSource must be used within a ImageSourcesProvider"
);
}
const setImageSources = useContext(ImageSourcesUpdaterContext);
if (setImageSources === undefined) {
throw new Error(
"useImageSource must be used within a ImageSourcesProvider"
);
}
useEffect(() => {
if (!data || data.type !== "file") {
return;
}
const id = getImageFileId(data, thumbnail);
function updateImageSource(file) {
if (file) {
setImageSources((prevSources) => {
if (id in prevSources) {
// Check if the image source is already added
return {
...prevSources,
[id]: {
...prevSources[id],
// Increase references
references: prevSources[id].references + 1,
},
};
} else {
const url = URL.createObjectURL(new Blob([file]));
return {
...prevSources,
[id]: { url, id, references: 1 },
};
}
});
}
}
if (thumbnail) {
updateImageSource(data.thumbnail.file);
} else if (data.resolutions) {
// Check is a resolution is specified
if (data.quality && data.resolutions[data.quality]) {
updateImageSource(data.resolutions[data.quality].file);
}
// If no file available fallback to the highest resolution
else if (!data.file) {
const resolutionArray = Object.keys(data.resolutions);
updateImageSource(
data.resolutions[resolutionArray[resolutionArray.length - 1]].file
);
} else {
updateImageSource(data.file);
}
} else {
updateImageSource(data.file);
}
return () => {
// Decrease references
setImageSources((prevSources) => {
if (id in prevSources) {
return {
...prevSources,
[id]: {
...prevSources[id],
references: prevSources[id].references - 1,
},
};
} else {
return prevSources;
}
});
};
}, [data, unknownSource, thumbnail, setImageSources]);
if (!data) {
return unknownSource;
}
if (data.type === "default") {
return defaultSources[data.key];
}
if (data.type === "file") {
const id = getImageFileId(data, thumbnail);
return imageSources[id]?.url;
}
return unknownSource;
}

View File

@ -2,6 +2,7 @@
import Dexie, { Version, DexieOptions } from "dexie";
import "dexie-observable";
import shortid from "shortid";
import { v4 as uuid } from "uuid";
import blobToBuffer from "./helpers/blobToBuffer";
import { getGridDefaultInset } from "./helpers/grid";
@ -431,9 +432,139 @@ const versions = {
});
});
},
// v1.9.0 - Move map assets into new table
23(v) {
v.stores({ assets: "id" }).upgrade((tx) => {
tx.table("maps").each((map) => {
let assets = [];
assets.push({
id: uuid(),
file: map.file,
width: map.width,
height: map.height,
mime: "",
prevId: map.id,
prevType: "map",
});
for (let resolution in map.resolutions) {
const mapRes = map.resolutions[resolution];
assets.push({
id: uuid(),
file: mapRes.file,
width: mapRes.width,
height: mapRes.height,
mime: "",
prevId: map.id,
prevType: "mapResolution",
resolution,
});
}
assets.push({
id: uuid(),
file: map.thumbnail.file,
width: map.thumbnail.width,
height: map.thumbnail.height,
mime: "",
prevId: map.id,
prevType: "mapThumbnail",
});
tx.table("assets").bulkAdd(assets);
});
});
},
// v1.9.0 - Move token assets into new table
24(v) {
v.stores().upgrade((tx) => {
tx.table("tokens").each((token) => {
let assets = [];
assets.push({
id: uuid(),
file: token.file,
width: token.width,
height: token.height,
mime: "",
prevId: token.id,
prevType: "token",
});
assets.push({
id: uuid(),
file: token.thumbnail.file,
width: token.thumbnail.width,
height: token.thumbnail.height,
mime: "",
prevId: token.id,
prevType: "tokenThumbnail",
});
tx.table("assets").bulkAdd(assets);
});
});
},
// v1.9.0 - Create foreign keys for assets
25(v) {
v.stores().upgrade((tx) => {
tx.table("assets").each((asset) => {
if (asset.prevType === "map") {
tx.table("maps").update(asset.prevId, {
file: asset.id,
width: undefined,
height: undefined,
});
} else if (asset.prevType === "token") {
tx.table("tokens").update(asset.prevId, {
file: asset.id,
width: undefined,
height: undefined,
});
} else if (asset.prevType === "mapThumbnail") {
tx.table("maps").update(asset.prevId, { thumbnail: asset.id });
} else if (asset.prevType === "tokenThumbnail") {
tx.table("tokens").update(asset.prevId, { thumbnail: asset.id });
} else if (asset.prevType === "mapResolution") {
tx.table("maps").update(asset.prevId, {
resolutions: undefined,
[asset.resolution]: asset.id,
});
}
});
});
},
// v1.9.0 - Remove asset migration helpers
26(v) {
v.stores().upgrade((tx) => {
tx.table("assets")
.toCollection()
.modify((asset) => {
delete asset.prevId;
if (asset.prevType === "mapResolution") {
delete asset.resolution;
}
delete asset.prevType;
});
});
},
// v1.9.0 - Remap map resolution assets
27(v) {
v.stores().upgrade((tx) => {
tx.table("maps")
.toCollection()
.modify((map) => {
const resolutions = ["low", "medium", "high", "ultra"];
map.resolutions = {};
for (let res of resolutions) {
if (res in map) {
map.resolutions[res] = map[res];
delete map[res];
}
}
});
});
},
};
const latestVersion = 22;
const latestVersion = 27;
/**
* Load versions onto a database up to a specific version number

View File

@ -23,10 +23,10 @@ import AuthContext, { useAuth } from "../contexts/AuthContext";
import SettingsContext, { useSettings } from "../contexts/SettingsContext";
import KeyboardContext from "../contexts/KeyboardContext";
import TokenDataContext, { useTokenData } from "../contexts/TokenDataContext";
import {
ImageSourcesStateContext,
ImageSourcesUpdaterContext,
} from "../contexts/ImageSourceContext";
import AssetsContext, {
AssetURLsStateContext,
AssetURLsUpdaterContext,
} from "../contexts/AssetsContext";
import {
useGrid,
useGridCellPixelSize,
@ -52,8 +52,9 @@ function KonvaBridge({ stageRender, children }) {
const auth = useAuth();
const settings = useSettings();
const tokenData = useTokenData();
const imageSources = useContext(ImageSourcesStateContext);
const setImageSources = useContext(ImageSourcesUpdaterContext);
const assets = useContext(AssetsContext);
const assetURLs = useContext(AssetURLsStateContext);
const setAssetURLs = useContext(AssetURLsUpdaterContext);
const keyboardValue = useContext(KeyboardContext);
const stageScale = useStageScale();
@ -78,61 +79,63 @@ function KonvaBridge({ stageRender, children }) {
<SettingsContext.Provider value={settings}>
<KeyboardContext.Provider value={keyboardValue}>
<MapStageProvider value={mapStageRef}>
<TokenDataContext.Provider value={tokenData}>
<ImageSourcesStateContext.Provider value={imageSources}>
<ImageSourcesUpdaterContext.Provider value={setImageSources}>
<InteractionEmitterContext.Provider
value={interactionEmitter}
>
<SetPreventMapInteractionContext.Provider
value={setPreventMapInteraction}
<AssetsContext.Provider value={assets}>
<AssetURLsStateContext.Provider value={assetURLs}>
<AssetURLsUpdaterContext.Provider value={setAssetURLs}>
<TokenDataContext.Provider value={tokenData}>
<InteractionEmitterContext.Provider
value={interactionEmitter}
>
<StageWidthContext.Provider value={stageWidth}>
<StageHeightContext.Provider value={stageHeight}>
<MapWidthContext.Provider value={mapWidth}>
<MapHeightContext.Provider value={mapHeight}>
<StageScaleContext.Provider value={stageScale}>
<DebouncedStageScaleContext.Provider
value={debouncedStageScale}
>
<GridContext.Provider value={grid}>
<GridPixelSizeContext.Provider
value={gridPixelSize}
>
<GridCellPixelSizeContext.Provider
value={gridCellPixelSize}
<SetPreventMapInteractionContext.Provider
value={setPreventMapInteraction}
>
<StageWidthContext.Provider value={stageWidth}>
<StageHeightContext.Provider value={stageHeight}>
<MapWidthContext.Provider value={mapWidth}>
<MapHeightContext.Provider value={mapHeight}>
<StageScaleContext.Provider value={stageScale}>
<DebouncedStageScaleContext.Provider
value={debouncedStageScale}
>
<GridContext.Provider value={grid}>
<GridPixelSizeContext.Provider
value={gridPixelSize}
>
<GridCellNormalizedSizeContext.Provider
value={gridCellNormalizedSize}
<GridCellPixelSizeContext.Provider
value={gridCellPixelSize}
>
<GridOffsetContext.Provider
value={gridOffset}
<GridCellNormalizedSizeContext.Provider
value={gridCellNormalizedSize}
>
<GridStrokeWidthContext.Provider
value={gridStrokeWidth}
<GridOffsetContext.Provider
value={gridOffset}
>
<GridCellPixelOffsetContext.Provider
value={gridCellPixelOffset}
<GridStrokeWidthContext.Provider
value={gridStrokeWidth}
>
{children}
</GridCellPixelOffsetContext.Provider>
</GridStrokeWidthContext.Provider>
</GridOffsetContext.Provider>
</GridCellNormalizedSizeContext.Provider>
</GridCellPixelSizeContext.Provider>
</GridPixelSizeContext.Provider>
</GridContext.Provider>
</DebouncedStageScaleContext.Provider>
</StageScaleContext.Provider>
</MapHeightContext.Provider>
</MapWidthContext.Provider>
</StageHeightContext.Provider>
</StageWidthContext.Provider>
</SetPreventMapInteractionContext.Provider>
</InteractionEmitterContext.Provider>
</ImageSourcesUpdaterContext.Provider>
</ImageSourcesStateContext.Provider>
</TokenDataContext.Provider>
<GridCellPixelOffsetContext.Provider
value={gridCellPixelOffset}
>
{children}
</GridCellPixelOffsetContext.Provider>
</GridStrokeWidthContext.Provider>
</GridOffsetContext.Provider>
</GridCellNormalizedSizeContext.Provider>
</GridCellPixelSizeContext.Provider>
</GridPixelSizeContext.Provider>
</GridContext.Provider>
</DebouncedStageScaleContext.Provider>
</StageScaleContext.Provider>
</MapHeightContext.Provider>
</MapWidthContext.Provider>
</StageHeightContext.Provider>
</StageWidthContext.Provider>
</SetPreventMapInteractionContext.Provider>
</InteractionEmitterContext.Provider>
</TokenDataContext.Provider>
</AssetURLsUpdaterContext.Provider>
</AssetURLsStateContext.Provider>
</AssetsContext.Provider>
</MapStageProvider>
</KeyboardContext.Provider>
</SettingsContext.Provider>

View File

@ -1,23 +1,23 @@
import { useEffect, useState } from "react";
import useImage from "use-image";
import { useImageSource } from "../contexts/ImageSourceContext";
import { useDataURL } from "../contexts/AssetsContext";
import { mapSources as defaultMapSources } from "../maps";
function useMapImage(map) {
const mapSource = useImageSource(map, defaultMapSources);
const [mapSourceImage, mapSourceImageStatus] = useImage(mapSource);
const mapURL = useDataURL(map, defaultMapSources);
const [mapImage, mapImageStatus] = useImage(mapURL);
// Create a map source that only updates when the image is fully loaded
const [loadedMapSourceImage, setLoadedMapSourceImage] = useState();
const [loadedMapImage, setLoadedMapImage] = useState();
useEffect(() => {
if (mapSourceImageStatus === "loaded") {
setLoadedMapSourceImage(mapSourceImage);
if (mapImageStatus === "loaded") {
setLoadedMapImage(mapImage);
}
}, [mapSourceImage, mapSourceImageStatus]);
}, [mapImage, mapImageStatus]);
return [loadedMapSourceImage, mapSourceImageStatus];
return [loadedMapImage, mapImageStatus];
}
export default useMapImage;

View File

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

View File

@ -13070,6 +13070,11 @@ uuid@^8.3.0:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.1.tgz#2ba2e6ca000da60fce5a196954ab241131e05a31"
integrity sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==
uuid@^8.3.2:
version "8.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
v8-compile-cache@^2.0.3:
version "2.2.0"
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.2.0.tgz#9471efa3ef9128d2f7c6a7ca39c4dd6b5055b132"