Create idb-backed image prop (#366)
Co-authored-by: Clement Roche <rchclement@gmail.com>
This commit is contained in:
parent
95b329b02d
commit
8d8e2348dd
21 changed files with 838 additions and 31 deletions
67
packages/playground/src/shared/image/index.tsx
Normal file
67
packages/playground/src/shared/image/index.tsx
Normal file
|
@ -0,0 +1,67 @@
|
|||
/**
|
||||
* A super basic Turtle geometry renderer hooked up to Theatre, so the parameters
|
||||
* can be tweaked and animated.
|
||||
*/
|
||||
import {getProject, types} from '@theatre/core'
|
||||
import studio from '@theatre/studio'
|
||||
import React, {useEffect, useState} from 'react'
|
||||
import {render} from 'react-dom'
|
||||
import styled from 'styled-components'
|
||||
|
||||
studio.initialize()
|
||||
const project = getProject('Image type playground', {
|
||||
assets: {
|
||||
baseUrl: 'http://localhost:3000',
|
||||
},
|
||||
})
|
||||
const sheet = project.sheet('Image type')
|
||||
|
||||
const Wrapper = styled.div`
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`
|
||||
|
||||
const ImageTypeExample: React.FC<{}> = (props) => {
|
||||
const [imageUrl, setImageUrl] = useState<string>()
|
||||
|
||||
useEffect(() => {
|
||||
const object = sheet.object('image', {
|
||||
image: types.image('', {
|
||||
label: 'texture',
|
||||
}),
|
||||
image2: types.image('', {
|
||||
label: 'another texture',
|
||||
}),
|
||||
something: 'asdf',
|
||||
})
|
||||
object.onValuesChange(({image}) => {
|
||||
setImageUrl(project.getAssetUrl(image))
|
||||
})
|
||||
|
||||
return () => {
|
||||
sheet.detachObject('canvas')
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Wrapper
|
||||
onClick={() => {
|
||||
if (sheet.sequence.position === 0) {
|
||||
sheet.sequence.position = 0
|
||||
sheet.sequence.play()
|
||||
} else {
|
||||
sheet.sequence.position = 0
|
||||
}
|
||||
}}
|
||||
>
|
||||
<img src={imageUrl} />
|
||||
</Wrapper>
|
||||
)
|
||||
}
|
||||
|
||||
project.ready.then(() => {
|
||||
render(<ImageTypeExample />, document.getElementById('root'))
|
||||
})
|
191
theatre/core/src/projects/DefaultAssetStorage.ts
Normal file
191
theatre/core/src/projects/DefaultAssetStorage.ts
Normal file
|
@ -0,0 +1,191 @@
|
|||
import {createStore} from './IDBStorage'
|
||||
import type Project from './Project'
|
||||
import type {IAssetStorageConfig} from './Project'
|
||||
// @ts-ignore
|
||||
import blobCompare from 'blob-compare'
|
||||
import {notify} from '@theatre/core/coreExports'
|
||||
import {getAllPossibleAssetIDs} from '@theatre/shared/utils/assets'
|
||||
|
||||
export const createDefaultAssetStorageConfig = ({
|
||||
project,
|
||||
baseUrl = '',
|
||||
}: {
|
||||
project: Project
|
||||
baseUrl?: string
|
||||
}): IAssetStorageConfig => {
|
||||
return {
|
||||
coreAssetStorage: {
|
||||
getAssetUrl: (assetId: string) => `${baseUrl}/${assetId}`,
|
||||
},
|
||||
createStudioAssetStorage: async () => {
|
||||
// in SSR we bail out and return a dummy asset manager
|
||||
if (typeof window === 'undefined') {
|
||||
return {
|
||||
getAssetUrl: () => '',
|
||||
createAsset: () => Promise.resolve(null),
|
||||
}
|
||||
}
|
||||
|
||||
// Check for support.
|
||||
if (!('indexedDB' in window)) {
|
||||
console.log("This browser doesn't support IndexedDB.")
|
||||
|
||||
return {
|
||||
getAssetUrl: (assetId: string) => {
|
||||
throw new Error(
|
||||
`IndexedDB is required by the default asset manager, but it's not supported by this browser. To use assets, please provide your own asset manager to the project config.`,
|
||||
)
|
||||
},
|
||||
createAsset: (asset: Blob) => {
|
||||
throw new Error(
|
||||
`IndexedDB is required by the default asset manager, but it's not supported by this browser. To use assets, please provide your own asset manager to the project config.`,
|
||||
)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const idb = createStore(`${project.address.projectId}-assets`)
|
||||
|
||||
// get all possible asset ids referenced by either static props or keyframes
|
||||
const possibleAssetIDs = getAllPossibleAssetIDs(project)
|
||||
|
||||
// Clean up assets not referenced by the project. We can only do this at the start because otherwise
|
||||
// we'd break undo/redo.
|
||||
const idbKeys = await idb.keys<string>()
|
||||
await Promise.all(
|
||||
idbKeys.map(async (key) => {
|
||||
if (!possibleAssetIDs.includes(key)) {
|
||||
await idb.del(key)
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
// Clean up idb entries exported to disk
|
||||
await Promise.all(
|
||||
idbKeys.map(async (key) => {
|
||||
const assetUrl = `${baseUrl}/${key}`
|
||||
|
||||
try {
|
||||
const response = await fetch(assetUrl, {method: 'HEAD'})
|
||||
if (response.ok) {
|
||||
await idb.del(key)
|
||||
}
|
||||
} catch (e) {
|
||||
notify.error(
|
||||
'Failed to access assets',
|
||||
`Failed to access assets at ${
|
||||
project.config.assets?.baseUrl ?? '/'
|
||||
}. This is likely due to a CORS issue.`,
|
||||
)
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
// A map for caching the assets outside of the db. We also need this to be able to retrieve idb asset urls synchronously.
|
||||
const assetsMap = new Map(await idb.entries<string, Blob>())
|
||||
|
||||
// A map for caching the object urls created from idb assets.
|
||||
const urlCache = new Map<Blob, string>()
|
||||
|
||||
/** Gets idb aset url from asset blob */
|
||||
const getUrlForAsset = (asset: Blob) => {
|
||||
if (urlCache.has(asset)) {
|
||||
return urlCache.get(asset)!
|
||||
} else {
|
||||
const url = URL.createObjectURL(asset)
|
||||
urlCache.set(asset, url)
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
/** Gets idb asset url from id */
|
||||
const getUrlForId = (assetId: string) => {
|
||||
const asset = assetsMap.get(assetId)
|
||||
if (!asset) {
|
||||
throw new Error(`Asset with id ${assetId} not found`)
|
||||
}
|
||||
return getUrlForAsset(asset)
|
||||
}
|
||||
|
||||
return {
|
||||
getAssetUrl: (assetId: string) => {
|
||||
return assetsMap.has(assetId)
|
||||
? getUrlForId(assetId)
|
||||
: `${baseUrl}/${assetId}`
|
||||
},
|
||||
createAsset: async (asset: File) => {
|
||||
const existingIDs = getAllPossibleAssetIDs(project)
|
||||
|
||||
let sameSame = false
|
||||
|
||||
if (existingIDs.includes(asset.name)) {
|
||||
let existingAsset: Blob | undefined
|
||||
try {
|
||||
existingAsset =
|
||||
assetsMap.get(asset.name) ??
|
||||
(await fetch(`${baseUrl}/${asset.name}`).then((r) =>
|
||||
r.ok ? r.blob() : undefined,
|
||||
))
|
||||
} catch (e) {
|
||||
notify.error(
|
||||
'Failed to access assets',
|
||||
`Failed to access assets at ${
|
||||
project.config.assets?.baseUrl ?? '/'
|
||||
}. This is likely due to a CORS issue.`,
|
||||
)
|
||||
|
||||
return Promise.resolve(null)
|
||||
}
|
||||
|
||||
if (existingAsset) {
|
||||
// @ts-ignore
|
||||
sameSame = await blobCompare.isEqual(asset, existingAsset)
|
||||
|
||||
// if same same, we do nothing
|
||||
if (sameSame) {
|
||||
return asset.name
|
||||
// if different, we ask the user to pls rename
|
||||
} else {
|
||||
/** Initiates rename using a dialog. Returns a boolean indicating if the rename was succesful. */
|
||||
const renameAsset = (text: string): boolean => {
|
||||
const newAssetName = prompt(text, asset.name)
|
||||
|
||||
if (newAssetName === null) {
|
||||
// asset creation canceled
|
||||
return false
|
||||
} else if (newAssetName === '') {
|
||||
return renameAsset(
|
||||
'Asset name cannot be empty. Please choose a different file name for this asset.',
|
||||
)
|
||||
} else if (existingIDs.includes(newAssetName)) {
|
||||
console.log(existingIDs)
|
||||
return renameAsset(
|
||||
'An asset with this name already exists. Please choose a different file name for this asset.',
|
||||
)
|
||||
}
|
||||
|
||||
// rename asset
|
||||
asset = new File([asset], newAssetName, {type: asset.type})
|
||||
return true
|
||||
}
|
||||
|
||||
// rename asset returns false if the user cancels the rename
|
||||
const success = renameAsset(
|
||||
'An asset with this name already exists. Please choose a different file name for this asset.',
|
||||
)
|
||||
|
||||
if (!success) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assetsMap.set(asset.name, asset)
|
||||
await idb.set(asset.name, asset)
|
||||
return asset.name
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
22
theatre/core/src/projects/IDBStorage.ts
Normal file
22
theatre/core/src/projects/IDBStorage.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import * as idb from 'idb-keyval'
|
||||
|
||||
/**
|
||||
* Custom IDB keyval storage creator. Right now this exists solely as a more convenient way to use idb-keyval with a custom db name.
|
||||
* It also automatically prefixes the provided name with `theatrejs-` to avoid conflicts with other libraries.
|
||||
*
|
||||
* @param name - The name of the database
|
||||
* @returns An object with the same methods as idb-keyval, but with a custom database name
|
||||
*/
|
||||
export const createStore = (name: string) => {
|
||||
const customStore = idb.createStore(`theatrejs-${name}`, 'default-store')
|
||||
|
||||
return {
|
||||
set: (key: string, value: any) => idb.set(key, value, customStore),
|
||||
get: <T = any>(key: string) => idb.get<T>(key, customStore),
|
||||
del: (key: string) => idb.del(key, customStore),
|
||||
keys: <T extends IDBValidKey>() => idb.keys<T>(customStore),
|
||||
entries: <KeyType extends IDBValidKey, ValueType = any>() =>
|
||||
idb.entries<KeyType, ValueType>(customStore),
|
||||
values: <T = any>() => idb.values<T>(customStore),
|
||||
}
|
||||
}
|
|
@ -24,9 +24,35 @@ import type {
|
|||
ITheatreLoggingConfig,
|
||||
} from '@theatre/shared/logger'
|
||||
import {_coreLogger} from '@theatre/core/_coreLogger'
|
||||
import {createDefaultAssetStorageConfig} from './DefaultAssetStorage'
|
||||
|
||||
type ICoreAssetStorage = {
|
||||
/** Returns a URL for the provided asset ID */
|
||||
getAssetUrl: (assetId: string) => string
|
||||
}
|
||||
|
||||
interface IStudioAssetStorage extends ICoreAssetStorage {
|
||||
/** Creates an asset from the provided blob and returns a promise to its ID */
|
||||
createAsset: (asset: File) => Promise<string | null>
|
||||
}
|
||||
|
||||
export type IAssetStorageConfig = {
|
||||
/**
|
||||
* An object containing the core asset storage methods.
|
||||
*/
|
||||
coreAssetStorage: ICoreAssetStorage
|
||||
/** A function that returns a promise to an object containing asset storage methods to be used by studio. */
|
||||
createStudioAssetStorage: () => Promise<IStudioAssetStorage>
|
||||
}
|
||||
|
||||
type IAssetConf = {
|
||||
/** The base URL for assets. */
|
||||
baseUrl?: string
|
||||
}
|
||||
|
||||
export type Conf = Partial<{
|
||||
state: OnDiskState
|
||||
assets: IAssetConf
|
||||
experiments: ExperimentsConf
|
||||
}>
|
||||
|
||||
|
@ -50,13 +76,17 @@ export default class Project {
|
|||
|
||||
readonly address: ProjectAddress
|
||||
|
||||
private readonly _readyDeferred: Deferred<undefined>
|
||||
private readonly _studioReadyDeferred: Deferred<undefined>
|
||||
private readonly _defaultAssetStorageReadyDeferred: Deferred<undefined>
|
||||
private readonly _readyPromise: Promise<void>
|
||||
|
||||
private _sheetTemplates = new Atom<{
|
||||
[sheetId: string]: SheetTemplate | undefined
|
||||
}>({})
|
||||
sheetTemplatesP = this._sheetTemplates.pointer
|
||||
private _studio: Studio | undefined
|
||||
private _defaultAssetStorageConfig: IAssetStorageConfig
|
||||
assetStorage: IStudioAssetStorage
|
||||
|
||||
type: 'Theatre_Project' = 'Theatre_Project'
|
||||
readonly _logger: ILogger
|
||||
|
@ -87,6 +117,20 @@ export default class Project {
|
|||
},
|
||||
})
|
||||
|
||||
this._defaultAssetStorageConfig = createDefaultAssetStorageConfig({
|
||||
project: this,
|
||||
baseUrl: config.assets?.baseUrl,
|
||||
})
|
||||
this._defaultAssetStorageReadyDeferred = defer()
|
||||
this.assetStorage = {
|
||||
...this._defaultAssetStorageConfig.coreAssetStorage,
|
||||
|
||||
// Until the asset storage is ready, we'll throw an error when the user tries to use it
|
||||
createAsset: () => {
|
||||
throw new Error(`Please wait for Project.ready to use assets.`)
|
||||
},
|
||||
}
|
||||
|
||||
this._pointerProxies = {
|
||||
historic: new PointerProxy(onDiskStateAtom.pointer.historic),
|
||||
ahistoric: new PointerProxy(onDiskStateAtom.pointer.ahistoric),
|
||||
|
@ -101,14 +145,21 @@ export default class Project {
|
|||
|
||||
projectsSingleton.add(id, this)
|
||||
|
||||
this._readyDeferred = defer()
|
||||
this._studioReadyDeferred = defer()
|
||||
|
||||
this._readyPromise = Promise.all([
|
||||
this._studioReadyDeferred.promise,
|
||||
this._defaultAssetStorageReadyDeferred.promise,
|
||||
// hide the array from the user, i.e. make it Promise<void> instead of Promise<[undefined, undefined]>
|
||||
]).then(() => {})
|
||||
|
||||
if (config.state) {
|
||||
setTimeout(() => {
|
||||
// The user has provided config.state but in case @theatre/studio is loaded,
|
||||
// let's give it one tick to attach itself
|
||||
if (!this._studio) {
|
||||
this._readyDeferred.resolve(undefined)
|
||||
this._studioReadyDeferred.resolve(undefined)
|
||||
this._defaultAssetStorageReadyDeferred.resolve(undefined)
|
||||
this._logger._trace('ready deferred resolved with no state')
|
||||
}
|
||||
}, 0)
|
||||
|
@ -166,7 +217,15 @@ export default class Project {
|
|||
studio.atomP.ephemeral.coreByProject[this.address.projectId],
|
||||
)
|
||||
|
||||
this._readyDeferred.resolve(undefined)
|
||||
// asset storage has to be initialized after the pointers are set
|
||||
this._defaultAssetStorageConfig
|
||||
.createStudioAssetStorage()
|
||||
.then((assetStorage) => {
|
||||
this.assetStorage = assetStorage
|
||||
this._defaultAssetStorageReadyDeferred.resolve(undefined)
|
||||
})
|
||||
|
||||
this._studioReadyDeferred.resolve(undefined)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -175,11 +234,14 @@ export default class Project {
|
|||
}
|
||||
|
||||
get ready() {
|
||||
return this._readyDeferred.promise
|
||||
return this._readyPromise
|
||||
}
|
||||
|
||||
isReady() {
|
||||
return this._readyDeferred.status === 'resolved'
|
||||
return (
|
||||
this._studioReadyDeferred.status === 'resolved' &&
|
||||
this._defaultAssetStorageReadyDeferred.status === 'resolved'
|
||||
)
|
||||
}
|
||||
|
||||
getOrCreateSheet(
|
||||
|
|
|
@ -3,6 +3,7 @@ import Project from '@theatre/core/projects/Project'
|
|||
import type {ISheet} from '@theatre/core/sheets/TheatreSheet'
|
||||
|
||||
import type {ProjectAddress} from '@theatre/shared/utils/addresses'
|
||||
import type {Asset} from '@theatre/shared/utils/assets'
|
||||
import type {
|
||||
ProjectId,
|
||||
SheetId,
|
||||
|
@ -20,7 +21,9 @@ export type IProjectConfig = {
|
|||
* The state of the project, as [exported](https://www.theatrejs.com/docs/latest/manual/projects#state) by the studio.
|
||||
*/
|
||||
state?: $IntentionalAny
|
||||
// experiments?: IProjectConfigExperiments
|
||||
assets?: {
|
||||
baseUrl?: string
|
||||
}
|
||||
}
|
||||
|
||||
// export type IProjectConfigExperiments = {
|
||||
|
@ -70,6 +73,14 @@ export interface IProject {
|
|||
* **Docs: https://www.theatrejs.com/docs/latest/manual/sheets**
|
||||
*/
|
||||
sheet(sheetId: string, instanceId?: string): ISheet
|
||||
|
||||
/**
|
||||
* Returns the URL for an asset.
|
||||
*
|
||||
* @param asset - The asset to get the URL for
|
||||
* @returns The URL for the asset, or `undefined` if the asset is not found
|
||||
*/
|
||||
getAssetUrl(asset: Asset): string | undefined
|
||||
}
|
||||
|
||||
export default class TheatreProject implements IProject {
|
||||
|
@ -95,6 +106,12 @@ export default class TheatreProject implements IProject {
|
|||
return {...privateAPI(this).address}
|
||||
}
|
||||
|
||||
getAssetUrl(asset: Asset): string | undefined {
|
||||
return asset.id
|
||||
? privateAPI(this).assetStorage.getAssetUrl(asset.id)
|
||||
: undefined
|
||||
}
|
||||
|
||||
sheet(sheetId: string, instanceId: string = 'default'): ISheet {
|
||||
const sanitizedPath = validateAndSanitiseSlashedPathOrThrow(
|
||||
sheetId,
|
||||
|
|
|
@ -17,6 +17,7 @@ import type {
|
|||
import {propTypeSymbol, sanitizeCompoundProps} from './internals'
|
||||
// eslint-disable-next-line unused-imports/no-unused-imports
|
||||
import type SheetObject from '@theatre/core/sheetObjects/SheetObject'
|
||||
import type {Asset} from '@theatre/shared/utils/assets'
|
||||
|
||||
// Notes on naming:
|
||||
// As of now, prop types are either `simple` or `composite`.
|
||||
|
@ -138,6 +139,74 @@ export const compound = <Props extends UnknownShorthandCompoundProps>(
|
|||
return config
|
||||
}
|
||||
|
||||
/**
|
||||
* An image prop type
|
||||
*
|
||||
* @example
|
||||
* Usage:
|
||||
* ```ts
|
||||
*
|
||||
* // with a label:
|
||||
* const obj = sheet.object('key', {
|
||||
* url: t.image('My image.png', {
|
||||
* label: 'texture'
|
||||
* })
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* @param opts - Options (See usage examples)
|
||||
*/
|
||||
export const image = (
|
||||
// The defaultValue parameter is a string for convenience, but it will be converted to an Asset object
|
||||
defaultValue: Asset['id'],
|
||||
opts: {
|
||||
label?: string
|
||||
interpolate?: Interpolator<Asset['id']>
|
||||
} = {},
|
||||
): PropTypeConfig_Image => {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
validateCommonOpts('t.image(defaultValue, opts)', opts)
|
||||
}
|
||||
|
||||
const interpolate: Interpolator<Asset> = (left, right, progression) => {
|
||||
const stringInterpolate = opts.interpolate ?? leftInterpolate
|
||||
|
||||
return {
|
||||
type: 'image',
|
||||
id: stringInterpolate(left.id, right.id, progression),
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'image',
|
||||
default: {type: 'image', id: defaultValue},
|
||||
valueType: null as $IntentionalAny,
|
||||
[propTypeSymbol]: 'TheatrePropType',
|
||||
label: opts.label,
|
||||
interpolate,
|
||||
deserializeAndSanitize: _ensureImage,
|
||||
}
|
||||
}
|
||||
|
||||
const _ensureImage = (val: unknown): Asset | undefined => {
|
||||
if (!val) return undefined
|
||||
|
||||
let valid = true
|
||||
|
||||
if (
|
||||
typeof (val as $IntentionalAny).id !== 'string' &&
|
||||
![null, undefined].includes((val as $IntentionalAny).id)
|
||||
) {
|
||||
valid = false
|
||||
}
|
||||
|
||||
if ((val as $IntentionalAny).type !== 'image') valid = false
|
||||
|
||||
if (!valid) return undefined
|
||||
|
||||
return val as Asset
|
||||
}
|
||||
|
||||
/**
|
||||
* A number prop type.
|
||||
*
|
||||
|
@ -690,6 +759,8 @@ export interface PropTypeConfig_StringLiteral<T extends string>
|
|||
|
||||
export interface PropTypeConfig_Rgba extends ISimplePropType<'rgba', Rgba> {}
|
||||
|
||||
export interface PropTypeConfig_Image extends ISimplePropType<'image', Asset> {}
|
||||
|
||||
type DeepPartialCompound<Props extends UnknownValidCompoundProps> = {
|
||||
[K in keyof Props]?: DeepPartial<Props[K]>
|
||||
}
|
||||
|
@ -722,6 +793,7 @@ export type PropTypeConfig_AllSimples =
|
|||
| PropTypeConfig_String
|
||||
| PropTypeConfig_StringLiteral<$IntentionalAny>
|
||||
| PropTypeConfig_Rgba
|
||||
| PropTypeConfig_Image
|
||||
|
||||
export type PropTypeConfig =
|
||||
| PropTypeConfig_AllSimples
|
||||
|
|
|
@ -48,6 +48,7 @@
|
|||
"babel-loader": "^8.2.2",
|
||||
"babel-polyfill": "^6.26.0",
|
||||
"babel-register": "^6.26.0",
|
||||
"blob-compare": "1.1.0",
|
||||
"circular-dependency-plugin": "^5.2.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"esbuild": "^0.12.15",
|
||||
|
@ -57,10 +58,12 @@
|
|||
"file-loader": "^6.2.0",
|
||||
"fs-extra": "^10.0.0",
|
||||
"html-loader": "^2.1.2",
|
||||
"idb-keyval": "^6.2.0",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"immer": "^9.0.6",
|
||||
"jiff": "^0.7.3",
|
||||
"json-touch-patch": "^0.11.2",
|
||||
"jszip": "3.10.1",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash-es": "^4.17.21",
|
||||
"marked": "^4.1.1",
|
||||
|
|
37
theatre/shared/src/utils/assets.ts
Normal file
37
theatre/shared/src/utils/assets.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
import type Project from '@theatre/core/projects/Project'
|
||||
import {val} from '@theatre/dataverse'
|
||||
|
||||
export function getAllPossibleAssetIDs(project: Project, type?: string) {
|
||||
// Apparently the value returned by val() can be undefined. Should fix TS.
|
||||
const sheets = Object.values(val(project.pointers.historic.sheetsById) ?? {})
|
||||
const staticValues = sheets
|
||||
.flatMap((sheet) => Object.values(sheet?.staticOverrides.byObject ?? {}))
|
||||
.flatMap((overrides) => Object.values(overrides ?? {}))
|
||||
const keyframeValues = sheets
|
||||
.flatMap((sheet) => Object.values(sheet?.sequence?.tracksByObject ?? {}))
|
||||
.flatMap((tracks) => Object.values(tracks?.trackData ?? {}))
|
||||
.flatMap((track) => track?.keyframes)
|
||||
.map((keyframe) => keyframe?.value)
|
||||
|
||||
const allAssets = [...staticValues, ...keyframeValues]
|
||||
// value is Asset of the type provided
|
||||
.filter((value) => {
|
||||
return (
|
||||
(value as Asset | undefined)?.type &&
|
||||
(type
|
||||
? (value as Asset | undefined)?.type == type
|
||||
: typeof (value as Asset | undefined)?.type === 'string')
|
||||
)
|
||||
})
|
||||
// map assets to their ids
|
||||
.map((value) => (value as Asset).id)
|
||||
// ensure ids are unique and not null and not empty
|
||||
.filter(
|
||||
(id, index, self) =>
|
||||
id !== null && id !== '' && self.indexOf(id) === index,
|
||||
) as string[]
|
||||
|
||||
return allAssets
|
||||
}
|
||||
|
||||
export type Asset = {type: 'image'; id: string | undefined}
|
|
@ -1,3 +1,5 @@
|
|||
import type {Asset} from './assets'
|
||||
|
||||
export type GenericAction = {type: string; payload: unknown}
|
||||
|
||||
export type ReduxReducer<State extends {}> = (
|
||||
|
@ -48,6 +50,7 @@ export type SerializablePrimitive =
|
|||
| number
|
||||
| boolean
|
||||
| {r: number; g: number; b: number; a: number}
|
||||
| Asset
|
||||
|
||||
/**
|
||||
* This type represents all values that can be safely serialized.
|
||||
|
|
|
@ -6,6 +6,9 @@ import React, {useCallback, useState} from 'react'
|
|||
import styled from 'styled-components'
|
||||
import DetailPanelButton from '@theatre/studio/uiComponents/DetailPanelButton'
|
||||
import StateConflictRow from './ProjectDetails/StateConflictRow'
|
||||
import JSZip from 'jszip'
|
||||
import {notify} from '@theatre/studio/notify'
|
||||
import {getAllPossibleAssetIDs} from '@theatre/shared/utils/assets'
|
||||
|
||||
const Container = styled.div``
|
||||
|
||||
|
@ -17,10 +20,35 @@ const TheExportRow = styled.div`
|
|||
`
|
||||
|
||||
const ExportTooltip = styled(BasicPopover)`
|
||||
display flex;
|
||||
flex-direction: column;
|
||||
gap: 1em;
|
||||
width: 280px;
|
||||
padding: 1em;
|
||||
`
|
||||
|
||||
/**
|
||||
* Initiates a file download for the provided data with the provided file name
|
||||
*
|
||||
* @param content - The content to save
|
||||
* @param fileName - The name of the file to save
|
||||
*/
|
||||
function saveFile(content: string | Blob, fileName: string) {
|
||||
const file = new File([content], fileName)
|
||||
const objUrl = URL.createObjectURL(file)
|
||||
const a = Object.assign(document.createElement('a'), {
|
||||
href: objUrl,
|
||||
target: '_blank',
|
||||
rel: 'noopener',
|
||||
})
|
||||
a.setAttribute('download', fileName)
|
||||
a.click()
|
||||
|
||||
setTimeout(() => {
|
||||
URL.revokeObjectURL(objUrl)
|
||||
}, 40000)
|
||||
}
|
||||
|
||||
const ProjectDetails: React.FC<{
|
||||
projects: Project[]
|
||||
}> = ({projects}) => {
|
||||
|
@ -34,41 +62,74 @@ const ProjectDetails: React.FC<{
|
|||
|
||||
const [downloaded, setDownloaded] = useState(false)
|
||||
|
||||
const exportProject = useCallback(() => {
|
||||
const exportProject = useCallback(async () => {
|
||||
// get all possible asset ids referenced by either static props or keyframes
|
||||
const allValues = getAllPossibleAssetIDs(project)
|
||||
|
||||
const blobs = new Map<string, Blob>()
|
||||
|
||||
try {
|
||||
// only export assets that are referenced by the project
|
||||
await Promise.all(
|
||||
allValues.map(async (value) => {
|
||||
const assetUrl = project.assetStorage.getAssetUrl(value)
|
||||
|
||||
const response = await fetch(assetUrl)
|
||||
if (response.ok) {
|
||||
blobs.set(value, await response.blob())
|
||||
}
|
||||
}),
|
||||
)
|
||||
} catch (e) {
|
||||
notify.error(
|
||||
`Failed to access assets`,
|
||||
`Export aborted. Failed to access assets at ${
|
||||
project.config.assets?.baseUrl ?? '/'
|
||||
}. This is likely due to a CORS issue.`,
|
||||
)
|
||||
|
||||
// abort the export
|
||||
return
|
||||
}
|
||||
|
||||
if (blobs.size > 0) {
|
||||
const zip = new JSZip()
|
||||
|
||||
for (const [assetID, blob] of blobs) {
|
||||
zip.file(assetID, blob)
|
||||
}
|
||||
|
||||
const assetsFile = await zip.generateAsync({type: 'blob'})
|
||||
saveFile(assetsFile, `${slugifiedProjectId}.assets.zip`)
|
||||
}
|
||||
|
||||
const str = JSON.stringify(
|
||||
getStudio().createContentOfSaveFile(project.address.projectId),
|
||||
null,
|
||||
2,
|
||||
)
|
||||
const file = new File([str], suggestedFileName, {
|
||||
type: 'application/json',
|
||||
})
|
||||
const objUrl = URL.createObjectURL(file)
|
||||
const a = Object.assign(document.createElement('a'), {
|
||||
href: objUrl,
|
||||
target: '_blank',
|
||||
rel: 'noopener',
|
||||
})
|
||||
a.setAttribute('download', suggestedFileName)
|
||||
a.click()
|
||||
|
||||
saveFile(str, suggestedFileName)
|
||||
|
||||
setDownloaded(true)
|
||||
setTimeout(() => {
|
||||
setDownloaded(false)
|
||||
}, 2000)
|
||||
|
||||
setTimeout(() => {
|
||||
URL.revokeObjectURL(objUrl)
|
||||
}, 40000)
|
||||
}, [project, suggestedFileName])
|
||||
|
||||
const exportTooltip = usePopover(
|
||||
{debugName: 'ProjectDetails', pointerDistanceThreshold: 50},
|
||||
() => (
|
||||
<ExportTooltip>
|
||||
<p>
|
||||
This will create a JSON file with the state of your project. You can
|
||||
commit this file to your git repo and include it in your production
|
||||
bundle.
|
||||
</p>
|
||||
<p>
|
||||
If your project uses assets, this will also create a zip file with all
|
||||
the assets that you can unpack in your public folder.
|
||||
</p>
|
||||
<a
|
||||
href="https://www.theatrejs.com/docs/latest/manual/projects#state"
|
||||
target="_blank"
|
||||
|
|
|
@ -15,6 +15,7 @@ import {valueInProp} from '@theatre/shared/propTypes/utils'
|
|||
const SingleKeyframePropEditorContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
min-width: 200px;
|
||||
|
||||
select {
|
||||
min-width: 100px;
|
||||
|
@ -153,5 +154,5 @@ function useEditingToolsForKeyframeEditorPopover(
|
|||
keyframes: [newKeyframe],
|
||||
snappingFunction: obj.sheet.getSequence().closestGridPosition,
|
||||
})
|
||||
})
|
||||
}, obj)
|
||||
}
|
||||
|
|
|
@ -6,6 +6,8 @@ import type {
|
|||
} from '@theatre/studio/StudioStore/StudioStore'
|
||||
import type {IEditingTools} from '@theatre/studio/propEditors/utils/IEditingTools'
|
||||
import {useMemo} from 'react'
|
||||
import type SheetObject from '@theatre/core/sheetObjects/SheetObject'
|
||||
import type {Asset} from '@theatre/shared/utils/assets'
|
||||
|
||||
/**
|
||||
* This function takes a function `writeTx` that sets a value in the private Studio API and
|
||||
|
@ -21,12 +23,14 @@ import {useMemo} from 'react'
|
|||
*/
|
||||
export function useTempTransactionEditingTools<T extends SerializableValue>(
|
||||
writeTx: (api: ITransactionPrivateApi, value: T) => void,
|
||||
obj: SheetObject,
|
||||
): IEditingTools<T> {
|
||||
return useMemo(() => createTempTransactionEditingTools<T>(writeTx), [])
|
||||
return useMemo(() => createTempTransactionEditingTools<T>(writeTx, obj), [])
|
||||
}
|
||||
|
||||
function createTempTransactionEditingTools<T>(
|
||||
writeTx: (api: ITransactionPrivateApi, value: T) => void,
|
||||
obj: SheetObject,
|
||||
) {
|
||||
let currentTransaction: CommitOrDiscard | null = null
|
||||
const createTempTx = (value: T) =>
|
||||
|
@ -37,6 +41,14 @@ function createTempTransactionEditingTools<T>(
|
|||
currentTransaction = null
|
||||
}
|
||||
|
||||
const editAssets = {
|
||||
createAsset: obj.sheet.project.assetStorage.createAsset,
|
||||
getAssetUrl: (asset: Asset) =>
|
||||
asset.id
|
||||
? obj.sheet.project.assetStorage.getAssetUrl(asset.id)
|
||||
: undefined,
|
||||
}
|
||||
|
||||
return {
|
||||
temporarilySetValue(value: T): void {
|
||||
discardTemporaryValue()
|
||||
|
@ -47,5 +59,6 @@ function createTempTransactionEditingTools<T>(
|
|||
discardTemporaryValue()
|
||||
createTempTx(value).commit()
|
||||
},
|
||||
...editAssets,
|
||||
}
|
||||
}
|
||||
|
|
132
theatre/studio/src/propEditors/simpleEditors/ImagePropEditor.tsx
Normal file
132
theatre/studio/src/propEditors/simpleEditors/ImagePropEditor.tsx
Normal file
|
@ -0,0 +1,132 @@
|
|||
import type {PropTypeConfig_Image} from '@theatre/core/propTypes'
|
||||
import {Trash} from '@theatre/studio/uiComponents/icons'
|
||||
import React, {useCallback, useEffect} from 'react'
|
||||
import styled from 'styled-components'
|
||||
import type {ISimplePropEditorReactProps} from './ISimplePropEditorReactProps'
|
||||
|
||||
const Container = styled.div`
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 2px;
|
||||
`
|
||||
|
||||
const Group = styled.div<{empty: boolean}>`
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
${({empty}) =>
|
||||
empty
|
||||
? `border: 1px dashed rgba(255, 255, 255, 0.2)`
|
||||
: `border: 1px solid rgba(255, 255, 255, 0.05)`}
|
||||
`
|
||||
|
||||
const InputLabel = styled.label`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
color: #919191;
|
||||
`
|
||||
|
||||
// file input
|
||||
const Input = styled.input.attrs({type: 'file', accept: 'image/*'})`
|
||||
display: none;
|
||||
`
|
||||
|
||||
const Preview = styled.img`
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
`
|
||||
|
||||
const DeleteButton = styled.button`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
outline: none;
|
||||
|
||||
color: #a8a8a9;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
|
||||
border: none;
|
||||
height: 100%;
|
||||
aspect-ratio: 1/1;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
`
|
||||
|
||||
function ImagePropEditor({
|
||||
propConfig,
|
||||
editingTools,
|
||||
value,
|
||||
autoFocus,
|
||||
}: ISimplePropEditorReactProps<PropTypeConfig_Image>) {
|
||||
const [previewUrl, setPreviewUrl] = React.useState<string>()
|
||||
|
||||
useEffect(() => {
|
||||
if (value) {
|
||||
setPreviewUrl(editingTools.getAssetUrl(value))
|
||||
} else {
|
||||
setPreviewUrl(undefined)
|
||||
}
|
||||
}, [value])
|
||||
|
||||
const onChange = useCallback(
|
||||
async (event) => {
|
||||
const file = event.target.files[0]
|
||||
editingTools.permanentlySetValue({type: 'image', id: undefined})
|
||||
const imageId = await editingTools.createAsset(file)
|
||||
|
||||
if (!imageId) {
|
||||
editingTools.permanentlySetValue(value)
|
||||
} else {
|
||||
editingTools.permanentlySetValue({
|
||||
type: 'image',
|
||||
id: imageId,
|
||||
})
|
||||
}
|
||||
event.target.value = null
|
||||
},
|
||||
[editingTools, value],
|
||||
)
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Group empty={!value}>
|
||||
<InputLabel>
|
||||
<Input
|
||||
type="file"
|
||||
onChange={onChange}
|
||||
accept="image/*"
|
||||
autoFocus={autoFocus}
|
||||
/>
|
||||
{previewUrl ? <Preview src={previewUrl} /> : <span>Add image</span>}
|
||||
</InputLabel>
|
||||
{value && (
|
||||
<DeleteButton
|
||||
onClick={() => {
|
||||
editingTools.permanentlySetValue({type: 'image', id: undefined})
|
||||
}}
|
||||
>
|
||||
<Trash />
|
||||
</DeleteButton>
|
||||
)}
|
||||
</Group>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default ImagePropEditor
|
|
@ -89,7 +89,6 @@ function RgbaPropEditor({
|
|||
editingTools.temporarilySetValue(rgba)
|
||||
}}
|
||||
permanentlySetValue={(color) => {
|
||||
// console.log('perm')
|
||||
const rgba = decorateRgba(color)
|
||||
editingTools.permanentlySetValue(rgba)
|
||||
}}
|
||||
|
|
|
@ -7,6 +7,7 @@ import StringPropEditor from './StringPropEditor'
|
|||
import RgbaPropEditor from './RgbaPropEditor'
|
||||
import type {ISimplePropEditorReactProps} from './ISimplePropEditorReactProps'
|
||||
import type {PropConfigForType} from '@theatre/studio/propEditors/utils/PropConfigForType'
|
||||
import ImagePropEditor from './ImagePropEditor'
|
||||
|
||||
export const simplePropEditorByPropType: ISimplePropEditorByPropType = {
|
||||
number: NumberPropEditor,
|
||||
|
@ -14,6 +15,7 @@ export const simplePropEditorByPropType: ISimplePropEditorByPropType = {
|
|||
boolean: BooleanPropEditor,
|
||||
stringLiteral: StringLiteralPropEditor,
|
||||
rgba: RgbaPropEditor,
|
||||
image: ImagePropEditor,
|
||||
}
|
||||
|
||||
type ISimplePropEditorByPropType = {
|
||||
|
|
|
@ -20,6 +20,7 @@ import type {NearbyKeyframes} from './getNearbyKeyframesOfTrack'
|
|||
import {getNearbyKeyframesOfTrack} from './getNearbyKeyframesOfTrack'
|
||||
import type {NearbyKeyframesControls} from './NextPrevKeyframeCursors'
|
||||
import NextPrevKeyframeCursors from './NextPrevKeyframeCursors'
|
||||
import type {Asset} from '@theatre/shared/utils/assets'
|
||||
|
||||
interface EditingToolsCommon<T> {
|
||||
value: T
|
||||
|
@ -31,6 +32,9 @@ interface EditingToolsCommon<T> {
|
|||
temporarilySetValue(v: T): void
|
||||
discardTemporaryValue(): void
|
||||
permanentlySetValue(v: T): void
|
||||
|
||||
getAssetUrl: (asset: Asset) => string | undefined
|
||||
createAsset(asset: Blob): Promise<string | null>
|
||||
}
|
||||
|
||||
interface EditingToolsDefault<T> extends EditingToolsCommon<T> {
|
||||
|
@ -109,6 +113,14 @@ function createDerivation<T extends SerializablePrimitive>(
|
|||
[],
|
||||
)
|
||||
|
||||
const editAssets = {
|
||||
createAsset: obj.sheet.project.assetStorage.createAsset,
|
||||
getAssetUrl: (asset: Asset) =>
|
||||
asset.id
|
||||
? obj.sheet.project.assetStorage.getAssetUrl(asset.id)
|
||||
: undefined,
|
||||
}
|
||||
|
||||
const beingScrubbed =
|
||||
val(
|
||||
get(
|
||||
|
@ -125,6 +137,7 @@ function createDerivation<T extends SerializablePrimitive>(
|
|||
|
||||
const common: EditingToolsCommon<T> = {
|
||||
...editPropValue,
|
||||
...editAssets,
|
||||
value: final,
|
||||
beingScrubbed,
|
||||
contextMenuItems,
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
import type {Asset} from '@theatre/shared/utils/assets'
|
||||
|
||||
export interface IEditingTools<T> {
|
||||
temporarilySetValue(v: T): void
|
||||
discardTemporaryValue(): void
|
||||
permanentlySetValue(v: T): void
|
||||
|
||||
getAssetUrl(asset: Asset): string | undefined
|
||||
createAsset(asset: Blob): Promise<string | null>
|
||||
}
|
||||
|
|
21
theatre/studio/src/uiComponents/icons/AddImage.tsx
Normal file
21
theatre/studio/src/uiComponents/icons/AddImage.tsx
Normal file
|
@ -0,0 +1,21 @@
|
|||
import * as React from 'react'
|
||||
|
||||
function AddImage(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
width={16}
|
||||
height={16}
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M3.335 13.998c-.367 0-.68-.13-.942-.391a1.285 1.285 0 01-.391-.942v-9.33c0-.367.13-.68.391-.942.261-.26.575-.391.942-.391h5.998v3.332h1.333v1.333h3.332v5.998c0 .367-.13.68-.391.942-.261.26-.575.391-.942.391h-9.33zM4 11.332H12L9.499 8 7.5 10.666l-1.5-2-1.999 2.666zm7.331-5.331V4.668H10V3.335h1.333V2.002h1.333v1.333h1.333v1.333h-1.333V6h-1.333z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export default AddImage
|
21
theatre/studio/src/uiComponents/icons/Trash.tsx
Normal file
21
theatre/studio/src/uiComponents/icons/Trash.tsx
Normal file
|
@ -0,0 +1,21 @@
|
|||
import * as React from 'react'
|
||||
|
||||
function Trash(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
width={16}
|
||||
height={16}
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M6.8 11.6a.6.6 0 00.6-.6V7.4a.6.6 0 00-1.2 0V11a.6.6 0 00.6.6zm6-7.2h-2.4v-.6A1.8 1.8 0 008.6 2H7.4a1.8 1.8 0 00-1.8 1.8v.6H3.2a.6.6 0 100 1.2h.6v6.6A1.8 1.8 0 005.6 14h4.8a1.8 1.8 0 001.8-1.8V5.6h.6a.6.6 0 100-1.2zm-6-.6a.6.6 0 01.6-.6h1.2a.6.6 0 01.6.6v.6H6.8v-.6zm4.2 8.4a.6.6 0 01-.6.6H5.6a.6.6 0 01-.6-.6V5.6h6v6.6zm-1.8-.6a.6.6 0 00.6-.6V7.4a.6.6 0 00-1.2 0V11a.6.6 0 00.6.6z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export default Trash
|
|
@ -15,3 +15,5 @@ export {default as GlobeSimple} from './GlobeSimple'
|
|||
export {default as Resize} from './Resize'
|
||||
export {default as Package} from './Package'
|
||||
export {default as Bell} from './Bell'
|
||||
export {default as Trash} from './Trash'
|
||||
export {default as AddImage} from './AddImage'
|
||||
|
|
67
yarn.lock
67
yarn.lock
|
@ -10896,6 +10896,15 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"blob-compare@npm:1.1.0":
|
||||
version: 1.1.0
|
||||
resolution: "blob-compare@npm:1.1.0"
|
||||
dependencies:
|
||||
webworker-promise: ^0.4.2
|
||||
checksum: 28cb1f5c6dc53195e67bb2765ec5a1aa7f1123460d9ccf64116aebf3ed36a3f4df5f065ac5ec4caf5da4fbbb5e944bca5c7296b960f693bea70dc4118953539d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"blob-polyfill@npm:^5.0.20210201":
|
||||
version: 5.0.20210201
|
||||
resolution: "blob-polyfill@npm:5.0.20210201"
|
||||
|
@ -18042,6 +18051,15 @@ fsevents@^1.2.7:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"idb-keyval@npm:^6.2.0":
|
||||
version: 6.2.0
|
||||
resolution: "idb-keyval@npm:6.2.0"
|
||||
dependencies:
|
||||
safari-14-idb-fix: ^3.0.0
|
||||
checksum: ddd58f95829ff734e3e83b29608709cdfba38e5795b1f6261ff1afc582ce48f475d0c827d7cf32aacbc5ea6b13d00c2d4a2f7240afccd5252565abeafbd7b990
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"idb@npm:^6.1.4":
|
||||
version: 6.1.5
|
||||
resolution: "idb@npm:6.1.5"
|
||||
|
@ -18104,6 +18122,13 @@ fsevents@^1.2.7:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"immediate@npm:~3.0.5":
|
||||
version: 3.0.6
|
||||
resolution: "immediate@npm:3.0.6"
|
||||
checksum: f9b3486477555997657f70318cc8d3416159f208bec4cca3ff3442fd266bc23f50f0c9bd8547e1371a6b5e82b821ec9a7044a4f7b944798b25aa3cc6d5e63e62
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"immer@npm:8.0.1":
|
||||
version: 8.0.1
|
||||
resolution: "immer@npm:8.0.1"
|
||||
|
@ -20589,6 +20614,18 @@ fsevents@^1.2.7:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"jszip@npm:3.10.1":
|
||||
version: 3.10.1
|
||||
resolution: "jszip@npm:3.10.1"
|
||||
dependencies:
|
||||
lie: ~3.3.0
|
||||
pako: ~1.0.2
|
||||
readable-stream: ~2.3.6
|
||||
setimmediate: ^1.0.5
|
||||
checksum: abc77bfbe33e691d4d1ac9c74c8851b5761fba6a6986630864f98d876f3fcc2d36817dfc183779f32c00157b5d53a016796677298272a714ae096dfe6b1c8b60
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"just-curry-it@npm:^3.1.0":
|
||||
version: 3.1.0
|
||||
resolution: "just-curry-it@npm:3.1.0"
|
||||
|
@ -20762,6 +20799,15 @@ fsevents@^1.2.7:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lie@npm:~3.3.0":
|
||||
version: 3.3.0
|
||||
resolution: "lie@npm:3.3.0"
|
||||
dependencies:
|
||||
immediate: ~3.0.5
|
||||
checksum: 33102302cf19766f97919a6a98d481e01393288b17a6aa1f030a3542031df42736edde8dab29ffdbf90bebeffc48c761eb1d064dc77592ca3ba3556f9fe6d2a8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lilconfig@npm:2.0.5, lilconfig@npm:^2.0.3, lilconfig@npm:^2.0.5":
|
||||
version: 2.0.5
|
||||
resolution: "lilconfig@npm:2.0.5"
|
||||
|
@ -23102,7 +23148,7 @@ fsevents@^1.2.7:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"pako@npm:~1.0.5":
|
||||
"pako@npm:~1.0.2, pako@npm:~1.0.5":
|
||||
version: 1.0.11
|
||||
resolution: "pako@npm:1.0.11"
|
||||
checksum: 1be2bfa1f807608c7538afa15d6f25baa523c30ec870a3228a89579e474a4d992f4293859524e46d5d87fd30fa17c5edf34dbef0671251d9749820b488660b16
|
||||
|
@ -27504,6 +27550,13 @@ fsevents@^1.2.7:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"safari-14-idb-fix@npm:^3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "safari-14-idb-fix@npm:3.0.0"
|
||||
checksum: 30d5baf3d9a17b52842b2c3fd3f0001fde7095a36191b6140ac78c19e8e1ff4b2607b9beb3b066c6bec107ccb5399c96e29748a6f5fcd592425f806f5e36f759
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"safe-buffer@npm:5.1.2, safe-buffer@npm:~5.1.0, safe-buffer@npm:~5.1.1":
|
||||
version: 5.1.2
|
||||
resolution: "safe-buffer@npm:5.1.2"
|
||||
|
@ -27983,7 +28036,7 @@ fsevents@^1.2.7:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"setimmediate@npm:^1.0.4":
|
||||
"setimmediate@npm:^1.0.4, setimmediate@npm:^1.0.5":
|
||||
version: 1.0.5
|
||||
resolution: "setimmediate@npm:1.0.5"
|
||||
checksum: c9a6f2c5b51a2dabdc0247db9c46460152ffc62ee139f3157440bd48e7c59425093f42719ac1d7931f054f153e2d26cf37dfeb8da17a794a58198a2705e527fd
|
||||
|
@ -29695,6 +29748,7 @@ fsevents@^1.2.7:
|
|||
babel-loader: ^8.2.2
|
||||
babel-polyfill: ^6.26.0
|
||||
babel-register: ^6.26.0
|
||||
blob-compare: 1.1.0
|
||||
circular-dependency-plugin: ^5.2.2
|
||||
cross-env: ^7.0.3
|
||||
esbuild: ^0.12.15
|
||||
|
@ -29706,10 +29760,12 @@ fsevents@^1.2.7:
|
|||
fs-extra: ^10.0.0
|
||||
fuzzy: ^0.1.3
|
||||
html-loader: ^2.1.2
|
||||
idb-keyval: ^6.2.0
|
||||
identity-obj-proxy: ^3.0.0
|
||||
immer: ^9.0.6
|
||||
jiff: ^0.7.3
|
||||
json-touch-patch: ^0.11.2
|
||||
jszip: 3.10.1
|
||||
lodash: ^4.17.21
|
||||
lodash-es: ^4.17.21
|
||||
marked: ^4.1.1
|
||||
|
@ -31424,6 +31480,13 @@ fsevents@^1.2.7:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"webworker-promise@npm:^0.4.2":
|
||||
version: 0.4.4
|
||||
resolution: "webworker-promise@npm:0.4.4"
|
||||
checksum: 6edb84c235ff49e437c3cfd8c10b75a202911c78e92050dbfad79d051d4df3ea7abbd2e16e0056e46ce0eaf53441105902647bd2b7cca4bc7c0c4f8dfd6b6da7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"whatwg-encoding@npm:^1.0.1, whatwg-encoding@npm:^1.0.5":
|
||||
version: 1.0.5
|
||||
resolution: "whatwg-encoding@npm:1.0.5"
|
||||
|
|
Loading…
Reference in a new issue