Moved assets into new table in the database
This commit is contained in:
parent
d620463c15
commit
9f11161b23
@ -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": {
|
||||
|
72
src/App.js
72
src/App.js
@ -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>
|
||||
|
@ -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} />
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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)}
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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={{
|
||||
|
@ -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);
|
||||
|
@ -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)}
|
||||
|
273
src/contexts/AssetsContext.js
Normal file
273
src/contexts/AssetsContext.js
Normal 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;
|
@ -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;
|
||||
}
|
133
src/database.js
133
src/database.js
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user