Moved to global image source manager to allow resource sharing
This commit is contained in:
parent
b6c6d9b553
commit
5231d14937
@ -18,6 +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 { ToastProvider } from "./components/Toast";
|
||||
|
||||
@ -29,6 +30,7 @@ function App() {
|
||||
<AuthProvider>
|
||||
<KeyboardProvider>
|
||||
<ToastProvider>
|
||||
<ImageSourcesProvider>
|
||||
<Router>
|
||||
<Switch>
|
||||
<Route path="/donate">
|
||||
@ -61,6 +63,7 @@ function App() {
|
||||
</Route>
|
||||
</Switch>
|
||||
</Router>
|
||||
</ImageSourcesProvider>
|
||||
</ToastProvider>
|
||||
</KeyboardProvider>
|
||||
</AuthProvider>
|
||||
|
@ -1,7 +1,8 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import useImage from "use-image";
|
||||
|
||||
import useDataSource from "../../hooks/useDataSource";
|
||||
import { useImageSource } from "../../contexts/ImageSourceContext";
|
||||
|
||||
import { mapSources as defaultMapSources } from "../../maps";
|
||||
|
||||
import { getImageLightness } from "../../helpers/image";
|
||||
@ -17,7 +18,7 @@ function MapGrid({ map }) {
|
||||
mapSourceMap = map.resolutions[resolutionArray[0]];
|
||||
}
|
||||
}
|
||||
const mapSource = useDataSource(mapSourceMap, defaultMapSources);
|
||||
const mapSource = useImageSource(mapSourceMap, defaultMapSources);
|
||||
const [mapImage, mapLoadingStatus] = useImage(mapSource);
|
||||
|
||||
const [isImageLight, setIsImageLight] = useState(true);
|
||||
|
@ -21,6 +21,10 @@ import TokenDataContext, {
|
||||
} from "../../contexts/TokenDataContext";
|
||||
import { GridProvider } from "../../contexts/GridContext";
|
||||
import { useKeyboard } from "../../contexts/KeyboardContext";
|
||||
import {
|
||||
ImageSourcesStateContext,
|
||||
ImageSourcesUpdaterContext,
|
||||
} from "../../contexts/ImageSourceContext";
|
||||
|
||||
function MapInteraction({
|
||||
map,
|
||||
@ -182,6 +186,8 @@ function MapInteraction({
|
||||
const auth = useAuth();
|
||||
const settings = useSettings();
|
||||
const tokenData = useTokenData();
|
||||
const imageSources = useContext(ImageSourcesStateContext);
|
||||
const setImageSources = useContext(ImageSourcesUpdaterContext);
|
||||
|
||||
const mapInteraction = {
|
||||
stageScale,
|
||||
@ -232,7 +238,15 @@ function MapInteraction({
|
||||
>
|
||||
<MapStageProvider value={mapStageRef}>
|
||||
<TokenDataContext.Provider value={tokenData}>
|
||||
<ImageSourcesStateContext.Provider
|
||||
value={imageSources}
|
||||
>
|
||||
<ImageSourcesUpdaterContext.Provider
|
||||
value={setImageSources}
|
||||
>
|
||||
{mapLoaded && children}
|
||||
</ImageSourcesUpdaterContext.Provider>
|
||||
</ImageSourcesStateContext.Provider>
|
||||
</TokenDataContext.Provider>
|
||||
</MapStageProvider>
|
||||
</GridProvider>
|
||||
|
@ -2,7 +2,7 @@ import React from "react";
|
||||
|
||||
import Tile from "../Tile";
|
||||
|
||||
import useDataSource from "../../hooks/useDataSource";
|
||||
import { useImageSource } from "../../contexts/ImageSourceContext";
|
||||
import { mapSources as defaultMapSources, unknownSource } from "../../maps";
|
||||
|
||||
function MapTile({
|
||||
@ -15,11 +15,11 @@ function MapTile({
|
||||
canEdit,
|
||||
badges,
|
||||
}) {
|
||||
const isDefault = map.type === "default";
|
||||
const mapSource = useDataSource(
|
||||
isDefault ? map : map.thumbnail,
|
||||
const mapSource = useImageSource(
|
||||
map,
|
||||
defaultMapSources,
|
||||
unknownSource
|
||||
unknownSource,
|
||||
map.type === "file"
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -4,7 +4,6 @@ import { useSpring, animated } from "react-spring/konva";
|
||||
import useImage from "use-image";
|
||||
import Konva from "konva";
|
||||
|
||||
import useDataSource from "../../hooks/useDataSource";
|
||||
import useDebounce from "../../hooks/useDebounce";
|
||||
import usePrevious from "../../hooks/usePrevious";
|
||||
import useGridSnapping from "../../hooks/useGridSnapping";
|
||||
@ -17,6 +16,7 @@ import {
|
||||
useDebouncedStageScale,
|
||||
} from "../../contexts/MapInteractionContext";
|
||||
import { useGridCellPixelSize } from "../../contexts/GridContext";
|
||||
import { useImageSource } from "../../contexts/ImageSourceContext";
|
||||
|
||||
import TokenStatus from "../token/TokenStatus";
|
||||
import TokenLabel from "../token/TokenLabel";
|
||||
@ -44,7 +44,7 @@ function MapToken({
|
||||
|
||||
const gridCellPixelSize = useGridCellPixelSize();
|
||||
|
||||
const tokenSource = useDataSource(token, tokenSources, unknownSource);
|
||||
const tokenSource = useImageSource(token, tokenSources, unknownSource);
|
||||
const [tokenSourceImage, tokenSourceStatus] = useImage(tokenSource);
|
||||
const [tokenAspectRatio, setTokenAspectRatio] = useState(1);
|
||||
|
||||
|
@ -2,16 +2,17 @@ import React, { useRef } from "react";
|
||||
import { Box, Image } from "theme-ui";
|
||||
|
||||
import usePreventTouch from "../../hooks/usePreventTouch";
|
||||
import useDataSource from "../../hooks/useDataSource";
|
||||
|
||||
import { useImageSource } from "../../contexts/ImageSourceContext";
|
||||
|
||||
import { tokenSources, unknownSource } from "../../tokens";
|
||||
|
||||
function ListToken({ token, className }) {
|
||||
const isDefault = token.type === "default";
|
||||
const tokenSource = useDataSource(
|
||||
isDefault ? token : token.thumbnail,
|
||||
const tokenSource = useImageSource(
|
||||
token,
|
||||
tokenSources,
|
||||
unknownSource
|
||||
unknownSource,
|
||||
token.type === "file"
|
||||
);
|
||||
|
||||
const imageRef = useRef();
|
||||
|
@ -6,11 +6,11 @@ import useImage from "use-image";
|
||||
|
||||
import usePreventOverscroll from "../../hooks/usePreventOverscroll";
|
||||
import useStageInteraction from "../../hooks/useStageInteraction";
|
||||
import useDataSource from "../../hooks/useDataSource";
|
||||
import useImageCenter from "../../hooks/useImageCenter";
|
||||
import useResponsiveLayout from "../../hooks/useResponsiveLayout";
|
||||
|
||||
import { GridProvider } from "../../contexts/GridContext";
|
||||
import { useImageSource } from "../../contexts/ImageSourceContext";
|
||||
|
||||
import GridOnIcon from "../../icons/GridOnIcon";
|
||||
import GridOffIcon from "../../icons/GridOffIcon";
|
||||
@ -27,7 +27,7 @@ function TokenPreview({ token }) {
|
||||
}
|
||||
}, [token, tokenSourceData]);
|
||||
|
||||
const tokenSource = useDataSource(
|
||||
const tokenSource = useImageSource(
|
||||
tokenSourceData,
|
||||
tokenSources,
|
||||
unknownSource
|
||||
|
@ -2,7 +2,8 @@ import React from "react";
|
||||
|
||||
import Tile from "../Tile";
|
||||
|
||||
import useDataSource from "../../hooks/useDataSource";
|
||||
import { useImageSource } from "../../contexts/ImageSourceContext";
|
||||
|
||||
import {
|
||||
tokenSources as defaultTokenSources,
|
||||
unknownSource,
|
||||
@ -17,11 +18,11 @@ function TokenTile({
|
||||
canEdit,
|
||||
badges,
|
||||
}) {
|
||||
const isDefault = token.type === "default";
|
||||
const tokenSource = useDataSource(
|
||||
isDefault ? token : token.thumbnail,
|
||||
const tokenSource = useImageSource(
|
||||
token,
|
||||
defaultTokenSources,
|
||||
unknownSource
|
||||
unknownSource,
|
||||
token.type === "file"
|
||||
);
|
||||
|
||||
return (
|
||||
|
155
src/contexts/ImageSourceContext.js
Normal file
155
src/contexts/ImageSourceContext.js
Normal file
@ -0,0 +1,155 @@
|
||||
import React, { useContext, useState, useEffect, useRef } 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"
|
||||
);
|
||||
}
|
||||
|
||||
const imageSourcesRef = useRef(imageSources);
|
||||
useEffect(() => {
|
||||
imageSourcesRef.current = imageSources;
|
||||
}, [imageSources]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!data || data.type !== "file") {
|
||||
return;
|
||||
}
|
||||
const id = getImageFileId(data, thumbnail);
|
||||
|
||||
function addImageSource(file) {
|
||||
if (file) {
|
||||
const url = URL.createObjectURL(new Blob([file]));
|
||||
setImageSources((prevSources) => ({
|
||||
...prevSources,
|
||||
[id]: { url, id, references: 1 },
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
if (id in imageSourcesRef.current) {
|
||||
// Increase references
|
||||
setImageSources((prevSources) => ({
|
||||
...prevSources,
|
||||
[id]: {
|
||||
...prevSources[id],
|
||||
references: prevSources[id].references + 1,
|
||||
},
|
||||
}));
|
||||
} else {
|
||||
if (thumbnail) {
|
||||
addImageSource(data.thumbnail.file);
|
||||
} else if (data.resolutions) {
|
||||
// Check is a resolution is specified
|
||||
if (data.quality && data.resolutions[data.quality]) {
|
||||
addImageSource(data.resolutions[data.quality].file);
|
||||
}
|
||||
// If no file available fallback to the highest resolution
|
||||
else if (!data.file) {
|
||||
const resolutionArray = Object.keys(data.resolutions);
|
||||
addImageSource(
|
||||
data.resolutions[resolutionArray[resolutionArray.length - 1]].file
|
||||
);
|
||||
} else {
|
||||
addImageSource(data.file);
|
||||
}
|
||||
} else {
|
||||
addImageSource(data.file);
|
||||
}
|
||||
}
|
||||
|
||||
return () => {
|
||||
// Decrease references
|
||||
if (id in imageSourcesRef.current) {
|
||||
setImageSources((prevSources) => ({
|
||||
...prevSources,
|
||||
[id]: {
|
||||
...prevSources[id],
|
||||
references: prevSources[id].references - 1,
|
||||
},
|
||||
}));
|
||||
}
|
||||
};
|
||||
}, [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;
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
// Helper function to load either file or default data
|
||||
// into a URL and ensure that it is revoked if needed
|
||||
function useDataSource(data, defaultSources, unknownSource) {
|
||||
const [dataSource, setDataSource] = useState(null);
|
||||
useEffect(() => {
|
||||
if (!data) {
|
||||
setDataSource(unknownSource);
|
||||
return;
|
||||
}
|
||||
let url = unknownSource;
|
||||
if (data.type === "file") {
|
||||
if (data.resolutions) {
|
||||
// Check is a resolution is specified
|
||||
if (data.quality && data.resolutions[data.quality]) {
|
||||
url = URL.createObjectURL(
|
||||
new Blob([data.resolutions[data.quality].file])
|
||||
);
|
||||
}
|
||||
// If no file available fallback to the highest resolution
|
||||
else if (!data.file) {
|
||||
const resolutionArray = Object.keys(data.resolutions);
|
||||
url = URL.createObjectURL(
|
||||
new Blob([
|
||||
data.resolutions[resolutionArray[resolutionArray.length - 1]]
|
||||
.file,
|
||||
])
|
||||
);
|
||||
} else {
|
||||
url = URL.createObjectURL(new Blob([data.file]));
|
||||
}
|
||||
} else {
|
||||
url = URL.createObjectURL(new Blob([data.file]));
|
||||
}
|
||||
} else if (data.type === "default") {
|
||||
url = defaultSources[data.key];
|
||||
}
|
||||
setDataSource(url);
|
||||
|
||||
return () => {
|
||||
if (data.type === "file" && url) {
|
||||
// Remove file url after 5 seconds as we still may be using it while the next image loads
|
||||
setTimeout(() => {
|
||||
URL.revokeObjectURL(url);
|
||||
}, 5000);
|
||||
}
|
||||
};
|
||||
}, [data, defaultSources, unknownSource]);
|
||||
|
||||
return dataSource;
|
||||
}
|
||||
|
||||
export default useDataSource;
|
@ -1,12 +1,12 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import useImage from "use-image";
|
||||
|
||||
import useDataSource from "./useDataSource";
|
||||
import { useImageSource } from "../contexts/ImageSourceContext";
|
||||
|
||||
import { mapSources as defaultMapSources } from "../maps";
|
||||
|
||||
function useMapImage(map) {
|
||||
const mapSource = useDataSource(map, defaultMapSources);
|
||||
const mapSource = useImageSource(map, defaultMapSources);
|
||||
const [mapSourceImage, mapSourceImageStatus] = useImage(mapSource);
|
||||
|
||||
// Create a map source that only updates when the image is fully loaded
|
||||
|
Loading…
Reference in New Issue
Block a user