2021-04-22 02:53:35 -04:00
|
|
|
import React, { useState, useContext, useCallback, useEffect } from "react";
|
2021-04-29 01:44:57 -04:00
|
|
|
import * as Comlink from "comlink";
|
|
|
|
import { encode } from "@msgpack/msgpack";
|
2021-04-22 02:53:35 -04:00
|
|
|
|
|
|
|
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
|
2021-04-23 03:10:10 -04:00
|
|
|
* @property {string} owner
|
2021-04-22 02:53:35 -04:00
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @callback getAsset
|
|
|
|
* @param {string} assetId
|
|
|
|
* @returns {Promise<Asset|undefined>}
|
|
|
|
*/
|
|
|
|
|
2021-04-22 21:48:24 -04:00
|
|
|
/**
|
|
|
|
* @callback addAssets
|
|
|
|
* @param {Asset[]} assets
|
|
|
|
*/
|
|
|
|
|
2021-04-23 03:10:10 -04:00
|
|
|
/**
|
|
|
|
* @callback putAsset
|
|
|
|
* @param {Asset} asset
|
|
|
|
*/
|
|
|
|
|
2021-04-22 02:53:35 -04:00
|
|
|
/**
|
|
|
|
* @typedef AssetsContext
|
|
|
|
* @property {getAsset} getAsset
|
2021-04-22 21:48:24 -04:00
|
|
|
* @property {addAssets} addAssets
|
2021-04-23 03:10:10 -04:00
|
|
|
* @property {putAsset} putAsset
|
2021-04-22 02:53:35 -04:00
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @type {React.Context<undefined|AssetsContext>}
|
|
|
|
*/
|
|
|
|
const AssetsContext = React.createContext();
|
|
|
|
|
|
|
|
export function AssetsProvider({ children }) {
|
2021-04-22 21:48:24 -04:00
|
|
|
const { worker, database } = useDatabase();
|
2021-04-22 02:53:35 -04:00
|
|
|
|
|
|
|
const getAsset = useCallback(
|
|
|
|
async (assetId) => {
|
2021-04-29 01:44:57 -04:00
|
|
|
return await database.table("assets").get(assetId);
|
2021-04-22 02:53:35 -04:00
|
|
|
},
|
2021-04-29 01:44:57 -04:00
|
|
|
[database]
|
2021-04-22 02:53:35 -04:00
|
|
|
);
|
|
|
|
|
2021-04-22 21:48:24 -04:00
|
|
|
const addAssets = useCallback(
|
|
|
|
async (assets) => {
|
|
|
|
return database.table("assets").bulkAdd(assets);
|
|
|
|
},
|
|
|
|
[database]
|
|
|
|
);
|
|
|
|
|
2021-04-23 03:10:10 -04:00
|
|
|
const putAsset = useCallback(
|
|
|
|
async (asset) => {
|
2021-04-29 01:44:57 -04:00
|
|
|
// Attempt to use worker to put map to avoid UI lockup
|
|
|
|
const packedAsset = encode(asset);
|
|
|
|
const success = await worker.putData(
|
|
|
|
Comlink.transfer(packedAsset, [packedAsset.buffer]),
|
|
|
|
"assets"
|
|
|
|
);
|
|
|
|
if (!success) {
|
|
|
|
await database.table("assets").put(asset);
|
|
|
|
}
|
2021-04-23 03:10:10 -04:00
|
|
|
},
|
2021-04-29 02:38:33 -04:00
|
|
|
[database, worker]
|
2021-04-23 03:10:10 -04:00
|
|
|
);
|
|
|
|
|
2021-04-22 21:48:24 -04:00
|
|
|
const value = {
|
|
|
|
getAsset,
|
|
|
|
addAssets,
|
2021-04-23 03:10:10 -04:00
|
|
|
putAsset,
|
2021-04-22 21:48:24 -04:00
|
|
|
};
|
|
|
|
|
2021-04-22 02:53:35 -04:00
|
|
|
return (
|
2021-04-22 21:48:24 -04:00
|
|
|
<AssetsContext.Provider value={value}>{children}</AssetsContext.Provider>
|
2021-04-22 02:53:35 -04:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
2021-04-22 21:48:24 -04:00
|
|
|
* @param {string|undefined} unknownSource
|
|
|
|
* @returns {string|undefined}
|
2021-04-22 02:53:35 -04:00
|
|
|
*/
|
2021-04-22 21:48:24 -04:00
|
|
|
export function useAssetURL(assetId, type, defaultSources, unknownSource) {
|
2021-04-22 02:53:35 -04:00
|
|
|
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();
|
2021-04-23 22:39:04 -04:00
|
|
|
const { database, databaseStatus } = useDatabase();
|
2021-04-22 02:53:35 -04:00
|
|
|
|
|
|
|
useEffect(() => {
|
2021-04-23 22:39:04 -04:00
|
|
|
if (
|
|
|
|
!assetId ||
|
|
|
|
type !== "file" ||
|
|
|
|
!database ||
|
|
|
|
databaseStatus === "loading"
|
|
|
|
) {
|
2021-04-22 02:53:35 -04:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
async function updateAssetURL() {
|
|
|
|
const asset = await getAsset(assetId);
|
|
|
|
if (asset) {
|
|
|
|
setAssetURLs((prevURLs) => {
|
|
|
|
if (assetId in prevURLs) {
|
2021-04-23 22:39:04 -04:00
|
|
|
// Check if the asset url is already added and increase references
|
2021-04-22 02:53:35 -04:00
|
|
|
return {
|
|
|
|
...prevURLs,
|
|
|
|
[assetId]: {
|
|
|
|
...prevURLs[assetId],
|
|
|
|
references: prevURLs[assetId].references + 1,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
} else {
|
2021-04-23 22:39:04 -04:00
|
|
|
// Create url if the asset doesn't have a url
|
2021-04-22 02:53:35 -04:00
|
|
|
const url = URL.createObjectURL(
|
|
|
|
new Blob([asset.file], { type: asset.mime })
|
|
|
|
);
|
|
|
|
return {
|
|
|
|
...prevURLs,
|
|
|
|
[assetId]: { url, id: assetId, references: 1 },
|
|
|
|
};
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
updateAssetURL();
|
|
|
|
|
2021-04-23 22:39:04 -04:00
|
|
|
// Update the url when the asset is added to the db after the hook is used
|
|
|
|
function handleAssetChanges(changes) {
|
|
|
|
for (let change of changes) {
|
|
|
|
const id = change.key;
|
|
|
|
if (
|
|
|
|
change.table === "assets" &&
|
|
|
|
id === assetId &&
|
|
|
|
(change.type === 1 || change.type === 2)
|
|
|
|
) {
|
|
|
|
const asset = change.obj;
|
|
|
|
setAssetURLs((prevURLs) => {
|
|
|
|
if (!(assetId in prevURLs)) {
|
|
|
|
const url = URL.createObjectURL(
|
|
|
|
new Blob([asset.file], { type: asset.mime })
|
|
|
|
);
|
|
|
|
return {
|
|
|
|
...prevURLs,
|
|
|
|
[assetId]: { url, id: assetId, references: 1 },
|
|
|
|
};
|
|
|
|
} else {
|
|
|
|
return prevURLs;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
database.on("changes", handleAssetChanges);
|
|
|
|
|
2021-04-22 02:53:35 -04:00
|
|
|
return () => {
|
2021-04-23 22:39:04 -04:00
|
|
|
database.on("changes").unsubscribe(handleAssetChanges);
|
|
|
|
|
2021-04-22 02:53:35 -04:00
|
|
|
// Decrease references
|
|
|
|
setAssetURLs((prevURLs) => {
|
|
|
|
if (assetId in prevURLs) {
|
|
|
|
return {
|
|
|
|
...prevURLs,
|
|
|
|
[assetId]: {
|
|
|
|
...prevURLs[assetId],
|
|
|
|
references: prevURLs[assetId].references - 1,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
} else {
|
|
|
|
return prevURLs;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
2021-04-23 22:39:04 -04:00
|
|
|
}, [assetId, setAssetURLs, getAsset, type, database, databaseStatus]);
|
2021-04-22 02:53:35 -04:00
|
|
|
|
|
|
|
if (!assetId) {
|
|
|
|
return unknownSource;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (type === "default") {
|
|
|
|
return defaultSources[assetId];
|
|
|
|
}
|
|
|
|
|
|
|
|
if (type === "file") {
|
2021-04-22 21:48:24 -04:00
|
|
|
return assetURLs[assetId]?.url || unknownSource;
|
2021-04-22 02:53:35 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
return unknownSource;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @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
|
2021-04-22 21:48:24 -04:00
|
|
|
* @param {string|undefined} unknownSource
|
2021-04-22 02:53:35 -04:00
|
|
|
* @param {boolean} thumbnail
|
2021-04-22 21:48:24 -04:00
|
|
|
* @returns {string|undefined}
|
2021-04-22 02:53:35 -04:00
|
|
|
*/
|
|
|
|
export function useDataURL(
|
|
|
|
data,
|
|
|
|
defaultSources,
|
2021-04-22 21:48:24 -04:00
|
|
|
unknownSource,
|
2021-04-22 02:53:35 -04:00
|
|
|
thumbnail = false
|
|
|
|
) {
|
|
|
|
const [assetId, setAssetId] = useState();
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
if (!data) {
|
|
|
|
return;
|
|
|
|
}
|
2021-04-23 22:39:04 -04:00
|
|
|
async function loadAssetId() {
|
2021-04-22 02:53:35 -04:00
|
|
|
if (data.type === "default") {
|
|
|
|
setAssetId(data.key);
|
|
|
|
} else {
|
|
|
|
if (thumbnail) {
|
|
|
|
setAssetId(data.thumbnail);
|
2021-04-23 22:39:04 -04:00
|
|
|
} else if (data.resolutions && data.quality !== "original") {
|
|
|
|
setAssetId(data.resolutions[data.quality]);
|
2021-04-22 02:53:35 -04:00
|
|
|
} else {
|
|
|
|
setAssetId(data.file);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-23 22:39:04 -04:00
|
|
|
loadAssetId();
|
|
|
|
}, [data, thumbnail]);
|
2021-04-22 02:53:35 -04:00
|
|
|
|
2021-04-23 22:39:04 -04:00
|
|
|
const assetURL = useAssetURL(
|
|
|
|
assetId,
|
|
|
|
data?.type,
|
|
|
|
defaultSources,
|
|
|
|
unknownSource
|
|
|
|
);
|
2021-04-22 02:53:35 -04:00
|
|
|
return assetURL;
|
|
|
|
}
|
|
|
|
|
|
|
|
export default AssetsContext;
|