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", "source-map-explorer": "^2.5.2",
"theme-ui": "^0.3.1", "theme-ui": "^0.3.1",
"use-image": "^1.0.7", "use-image": "^1.0.7",
"uuid": "^8.3.2",
"webrtc-adapter": "^7.7.1" "webrtc-adapter": "^7.7.1"
}, },
"resolutions": { "resolutions": {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,7 +2,7 @@ import React from "react";
import Tile from "../Tile"; import Tile from "../Tile";
import { useImageSource } from "../../contexts/ImageSourceContext"; import { useAssetURL } from "../../contexts/AssetsContext";
import { import {
tokenSources as defaultTokenSources, tokenSources as defaultTokenSources,
@ -18,7 +18,7 @@ function TokenTile({
canEdit, canEdit,
badges, badges,
}) { }) {
const tokenSource = useImageSource( const tokenURL = useAssetURL(
token, token,
defaultTokenSources, defaultTokenSources,
unknownSource, unknownSource,
@ -27,7 +27,7 @@ function TokenTile({
return ( return (
<Tile <Tile
src={tokenSource} src={tokenURL}
title={token.name} title={token.name}
isSelected={isSelected} isSelected={isSelected}
onSelect={() => onTokenSelect(token)} 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, { Version, DexieOptions } from "dexie";
import "dexie-observable"; import "dexie-observable";
import shortid from "shortid"; import shortid from "shortid";
import { v4 as uuid } from "uuid";
import blobToBuffer from "./helpers/blobToBuffer"; import blobToBuffer from "./helpers/blobToBuffer";
import { getGridDefaultInset } from "./helpers/grid"; 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 * 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 SettingsContext, { useSettings } from "../contexts/SettingsContext";
import KeyboardContext from "../contexts/KeyboardContext"; import KeyboardContext from "../contexts/KeyboardContext";
import TokenDataContext, { useTokenData } from "../contexts/TokenDataContext"; import TokenDataContext, { useTokenData } from "../contexts/TokenDataContext";
import { import AssetsContext, {
ImageSourcesStateContext, AssetURLsStateContext,
ImageSourcesUpdaterContext, AssetURLsUpdaterContext,
} from "../contexts/ImageSourceContext"; } from "../contexts/AssetsContext";
import { import {
useGrid, useGrid,
useGridCellPixelSize, useGridCellPixelSize,
@ -52,8 +52,9 @@ function KonvaBridge({ stageRender, children }) {
const auth = useAuth(); const auth = useAuth();
const settings = useSettings(); const settings = useSettings();
const tokenData = useTokenData(); const tokenData = useTokenData();
const imageSources = useContext(ImageSourcesStateContext); const assets = useContext(AssetsContext);
const setImageSources = useContext(ImageSourcesUpdaterContext); const assetURLs = useContext(AssetURLsStateContext);
const setAssetURLs = useContext(AssetURLsUpdaterContext);
const keyboardValue = useContext(KeyboardContext); const keyboardValue = useContext(KeyboardContext);
const stageScale = useStageScale(); const stageScale = useStageScale();
@ -78,61 +79,63 @@ function KonvaBridge({ stageRender, children }) {
<SettingsContext.Provider value={settings}> <SettingsContext.Provider value={settings}>
<KeyboardContext.Provider value={keyboardValue}> <KeyboardContext.Provider value={keyboardValue}>
<MapStageProvider value={mapStageRef}> <MapStageProvider value={mapStageRef}>
<TokenDataContext.Provider value={tokenData}> <AssetsContext.Provider value={assets}>
<ImageSourcesStateContext.Provider value={imageSources}> <AssetURLsStateContext.Provider value={assetURLs}>
<ImageSourcesUpdaterContext.Provider value={setImageSources}> <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>
</ImageSourcesUpdaterContext.Provider> </StageWidthContext.Provider>
</ImageSourcesStateContext.Provider> </SetPreventMapInteractionContext.Provider>
</TokenDataContext.Provider> </InteractionEmitterContext.Provider>
</TokenDataContext.Provider>
</AssetURLsUpdaterContext.Provider>
</AssetURLsStateContext.Provider>
</AssetsContext.Provider>
</MapStageProvider> </MapStageProvider>
</KeyboardContext.Provider> </KeyboardContext.Provider>
</SettingsContext.Provider> </SettingsContext.Provider>

View File

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

View File

@ -15,26 +15,21 @@ let service = {
* Load either a whole table or individual item from the DB * Load either a whole table or individual item from the DB
* @param {string} table Table to load from * @param {string} table Table to load from
* @param {string=} 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, excludeFiles = true) { async loadData(table, key) {
try { try {
let db = getDatabase({}); let db = getDatabase({});
if (key) { if (key) {
// Load specific item // Load specific item
const data = await db.table(table).get(key); const data = await db.table(table).get(key);
return data; const packed = encode(data);
return Comlink.transfer(packed, [packed.buffer]);
} else { } else {
// Load entire table // Load entire table
let items = []; let items = [];
// Use a cursor instead of toArray to prevent IPC max size error // Use a cursor instead of toArray to prevent IPC max size error
await db.table(table).each((item) => { await db.table(table).each((item) => {
if (excludeFiles) { items.push(item);
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 // 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" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.1.tgz#2ba2e6ca000da60fce5a196954ab241131e05a31"
integrity sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg== 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: v8-compile-cache@^2.0.3:
version "2.2.0" version "2.2.0"
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.2.0.tgz#9471efa3ef9128d2f7c6a7ca39c4dd6b5055b132" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.2.0.tgz#9471efa3ef9128d2f7c6a7ca39c4dd6b5055b132"