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-06-25 04:59:59 -04:00
|
|
|
import { useLiveQuery } from "dexie-react-hooks";
|
2021-04-22 02:53:35 -04:00
|
|
|
|
|
|
|
import { useDatabase } from "./DatabaseContext";
|
|
|
|
|
2021-06-05 00:40:44 -04:00
|
|
|
import useDebounce from "../hooks/useDebounce";
|
|
|
|
|
2021-04-22 02:53:35 -04:00
|
|
|
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();
|
|
|
|
|
2021-04-29 21:56:37 -04:00
|
|
|
// 100 MB max cache size
|
|
|
|
const maxCacheSize = 1e8;
|
|
|
|
|
2021-04-22 02:53:35 -04:00
|
|
|
export function AssetsProvider({ children }) {
|
2021-06-24 02:14:20 -04:00
|
|
|
const { worker, database, databaseStatus } = useDatabase();
|
2021-04-22 02:53:35 -04:00
|
|
|
|
2021-04-29 21:56:37 -04:00
|
|
|
useEffect(() => {
|
2021-06-24 02:14:20 -04:00
|
|
|
if (databaseStatus === "loaded") {
|
|
|
|
worker.cleanAssetCache(maxCacheSize);
|
|
|
|
}
|
|
|
|
}, [worker, databaseStatus]);
|
2021-04-29 21:56:37 -04:00
|
|
|
|
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) => {
|
2021-04-29 21:56:37 -04:00
|
|
|
await database.table("assets").bulkAdd(assets);
|
2021-04-22 21:48:24 -04:00
|
|
|
},
|
|
|
|
[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({});
|
2021-06-25 04:59:59 -04:00
|
|
|
const { database } = useDatabase();
|
|
|
|
|
|
|
|
// Keep track of when the asset keys change so we can update the URLs
|
|
|
|
const [assetKeys, setAssetKeys] = useState(new Set());
|
|
|
|
useEffect(() => {
|
|
|
|
const keys = Object.keys(assetURLs);
|
|
|
|
let newKeys = keys.filter((key) => !assetKeys.has(key));
|
|
|
|
if (newKeys.length > 0) {
|
|
|
|
setAssetKeys((prevKeys) => new Set([...prevKeys, ...newKeys]));
|
|
|
|
}
|
|
|
|
}, [assetURLs, assetKeys]);
|
|
|
|
|
|
|
|
// Get the new assets whenever the keys change
|
|
|
|
const assets = useLiveQuery(
|
|
|
|
() => database?.table("assets").where(":id").anyOf(assetKeys).toArray(),
|
|
|
|
[database, assetKeys]
|
|
|
|
);
|
|
|
|
|
|
|
|
// Update asset URLs when assets are loaded
|
|
|
|
useEffect(() => {
|
|
|
|
if (!assets) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
setAssetURLs((prevURLs) => {
|
|
|
|
let newURLs = { ...prevURLs };
|
|
|
|
for (let asset of assets) {
|
|
|
|
if (!newURLs[asset.id].url) {
|
|
|
|
newURLs[asset.id].url = URL.createObjectURL(
|
|
|
|
new Blob([asset.file], { type: asset.mime })
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return newURLs;
|
|
|
|
});
|
|
|
|
}, [assets]);
|
2021-04-22 02:53:35 -04:00
|
|
|
|
2021-06-05 00:40:44 -04:00
|
|
|
// Clean up asset URLs every minute
|
|
|
|
const debouncedAssetURLs = useDebounce(assetURLs, 60 * 1000);
|
|
|
|
|
2021-04-22 02:53:35 -04:00
|
|
|
// Revoke url when no more references
|
|
|
|
useEffect(() => {
|
2021-06-05 00:40:44 -04:00
|
|
|
setAssetURLs((prevURLs) => {
|
|
|
|
let urlsToCleanup = [];
|
|
|
|
for (let url of Object.values(prevURLs)) {
|
|
|
|
if (url.references <= 0) {
|
|
|
|
URL.revokeObjectURL(url.url);
|
|
|
|
urlsToCleanup.push(url.id);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (urlsToCleanup.length > 0) {
|
|
|
|
return omit(prevURLs, urlsToCleanup);
|
|
|
|
} else {
|
|
|
|
return prevURLs;
|
2021-04-22 02:53:35 -04:00
|
|
|
}
|
2021-06-05 00:40:44 -04:00
|
|
|
});
|
|
|
|
}, [debouncedAssetURLs]);
|
2021-04-22 02:53:35 -04:00
|
|
|
|
|
|
|
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();
|
|
|
|
|
|
|
|
useEffect(() => {
|
2021-06-25 04:59:59 -04:00
|
|
|
if (!assetId || type !== "file") {
|
2021-04-22 02:53:35 -04:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2021-05-13 02:28:02 -04:00
|
|
|
function updateAssetURL() {
|
|
|
|
function increaseReferences(prevURLs) {
|
|
|
|
return {
|
|
|
|
...prevURLs,
|
|
|
|
[assetId]: {
|
|
|
|
...prevURLs[assetId],
|
|
|
|
references: prevURLs[assetId].references + 1,
|
|
|
|
},
|
|
|
|
};
|
2021-04-22 02:53:35 -04:00
|
|
|
}
|
2021-05-13 02:28:02 -04:00
|
|
|
|
2021-06-25 04:59:59 -04:00
|
|
|
function createReference(prevURLs) {
|
2021-05-13 02:28:02 -04:00
|
|
|
return {
|
|
|
|
...prevURLs,
|
2021-06-25 04:59:59 -04:00
|
|
|
[assetId]: { url: null, id: assetId, references: 1 },
|
2021-05-13 02:28:02 -04:00
|
|
|
};
|
|
|
|
}
|
|
|
|
setAssetURLs((prevURLs) => {
|
|
|
|
if (assetId in prevURLs) {
|
|
|
|
// Check if the asset url is already added and increase references
|
|
|
|
return increaseReferences(prevURLs);
|
|
|
|
} else {
|
2021-06-25 04:59:59 -04:00
|
|
|
return createReference(prevURLs);
|
2021-05-13 02:28:02 -04:00
|
|
|
}
|
|
|
|
});
|
2021-04-22 02:53:35 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
updateAssetURL();
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
// Decrease references
|
|
|
|
setAssetURLs((prevURLs) => {
|
|
|
|
if (assetId in prevURLs) {
|
|
|
|
return {
|
|
|
|
...prevURLs,
|
|
|
|
[assetId]: {
|
|
|
|
...prevURLs[assetId],
|
|
|
|
references: prevURLs[assetId].references - 1,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
} else {
|
|
|
|
return prevURLs;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
2021-06-25 04:59:59 -04:00
|
|
|
}, [assetId, setAssetURLs, getAsset, type]);
|
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-05-13 02:28:02 -04:00
|
|
|
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;
|