Spaces:
Runtime error
Runtime error
import bindAll from 'lodash.bindall'; | |
import PropTypes from 'prop-types'; | |
import React from 'react'; | |
import VM from 'scratch-vm'; | |
import {defineMessages, injectIntl, intlShape} from 'react-intl'; | |
import log from '../lib/log'; | |
import extensionLibraryContent, { | |
galleryError, | |
galleryLoading, | |
galleryMore | |
} from '../lib/libraries/extensions/index.jsx'; | |
import extensionTags from '../lib/libraries/tw-extension-tags'; | |
import LibraryComponent from '../components/library/library.jsx'; | |
import extensionIcon from '../components/action-menu/icon--sprite.svg'; | |
const messages = defineMessages({ | |
extensionTitle: { | |
defaultMessage: 'Choose an Extension', | |
description: 'Heading for the extension library', | |
id: 'gui.extensionLibrary.chooseAnExtension' | |
} | |
}); | |
const toLibraryItem = extension => { | |
if (typeof extension === 'object') { | |
return ({ | |
rawURL: extension.iconURL || extensionIcon, | |
...extension | |
}); | |
} | |
return extension; | |
}; | |
const translateGalleryItem = (extension, locale) => ({ | |
...extension, | |
name: extension.nameTranslations[locale] || extension.name, | |
description: extension.descriptionTranslations[locale] || extension.description | |
}); | |
let cachedGallery = null; | |
const fetchLibrary = async () => { | |
// Fetch from TurboWarp extensions | |
const twRes = await fetch('https://extensions.turbowarp.org/generated-metadata/extensions-v0.json'); | |
if (!twRes.ok) throw new Error(`TurboWarp extensions: HTTP status ${twRes.status}`); | |
const twData = await twRes.json(); | |
// Fetch from Mistium extensions | |
const mistiumRes = await fetch('https://extensions.mistium.com/generated-metadata/extensions-v0.json'); | |
if (!mistiumRes.ok) throw new Error(`Mistium extensions: HTTP status ${mistiumRes.status}`); | |
const mistiumData = await mistiumRes.json(); | |
// Process TurboWarp extensions | |
const twExtensions = twData.extensions.map(extension => ({ | |
name: extension.name, | |
nameTranslations: extension.nameTranslations || {}, | |
description: extension.description, | |
descriptionTranslations: extension.descriptionTranslations || {}, | |
extensionId: extension.id, | |
extensionURL: `https://extensions.turbowarp.org/${extension.slug}.js`, | |
iconURL: `https://extensions.turbowarp.org/${extension.image || 'images/unknown.svg'}`, | |
tags: ['tw'], | |
credits: [ | |
...(extension.by || []), | |
...(extension.original || []) | |
].map(credit => { | |
if (credit.link) { | |
return ( | |
<a | |
href={credit.link} | |
target="_blank" | |
rel="noreferrer" | |
key={credit.name} | |
> | |
{credit.name} | |
</a> | |
); | |
} | |
return credit.name; | |
}), | |
docsURI: extension.docs ? `https://extensions.turbowarp.org/${extension.slug}` : null, | |
samples: extension.samples ? extension.samples.map(sample => ({ | |
href: `${process.env.ROOT}editor?project_url=https://extensions.turbowarp.org/samples/${encodeURIComponent(sample)}.sb3`, | |
text: sample | |
})) : null, | |
incompatibleWithScratch: true, | |
featured: true | |
})); | |
// Process Mistium extensions | |
const mistiumExtensions = mistiumData.extensions.map(extension => ({ | |
name: extension.name, | |
nameTranslations: extension.nameTranslations || {}, | |
description: extension.description, | |
descriptionTranslations: extension.descriptionTranslations || {}, | |
extensionId: extension.id, | |
extensionURL: `https://extensions.mistium.com/featured/${extension.name}.js`, | |
iconURL: `https://extensions.mistium.com/${extension.image || 'images/unknown.svg'}`, | |
tags: ['mistium', 'tw'], | |
credits: [ | |
...(extension.by || []), | |
...(extension.original || []) | |
].map(credit => { | |
if (credit.link) { | |
return ( | |
<a | |
href={credit.link} | |
target="_blank" | |
rel="noreferrer" | |
key={credit.name} | |
> | |
{credit.name} | |
</a> | |
); | |
} | |
return credit.name; | |
}), | |
docsURI: null, | |
samples: extension.samples ? extension.samples.map(sample => ({ | |
href: `${process.env.ROOT}editor?project_url=https://extensions-mistium.pages.dev/samples/${encodeURIComponent(sample)}.sb3`, | |
text: sample | |
})) : null, | |
incompatibleWithScratch: true, | |
featured: true | |
})); | |
// Combine both extension sets | |
return [...twExtensions, ...mistiumExtensions]; | |
}; | |
class ExtensionLibrary extends React.PureComponent { | |
constructor (props) { | |
super(props); | |
bindAll(this, [ | |
'handleItemSelect' | |
]); | |
this.state = { | |
gallery: cachedGallery, | |
galleryError: null, | |
galleryTimedOut: false | |
}; | |
} | |
componentDidMount () { | |
if (!this.state.gallery) { | |
const timeout = setTimeout(() => { | |
this.setState({ | |
galleryTimedOut: true | |
}); | |
}, 750); | |
fetchLibrary() | |
.then(gallery => { | |
cachedGallery = gallery; | |
this.setState({ | |
gallery | |
}); | |
clearTimeout(timeout); | |
}) | |
.catch(error => { | |
log.error(error); | |
this.setState({ | |
galleryError: error | |
}); | |
clearTimeout(timeout); | |
}); | |
} | |
} | |
handleItemSelect (item) { | |
if (item.href) { | |
return; | |
} | |
const extensionId = item.extensionId; | |
if (extensionId === 'custom_extension') { | |
this.props.onOpenCustomExtensionModal(); | |
return; | |
} | |
if (extensionId === 'procedures_enable_return') { | |
if (this.props.onEnableProcedureReturns) { | |
this.props.onEnableProcedureReturns(); | |
} | |
// Switch to blocks tab after enabling returns | |
if (typeof this.props.onActivateBlocksTab === 'function') { | |
this.props.onActivateBlocksTab(); | |
} | |
// Switch to My Blocks category after enabling returns (correct ID is "more") | |
if (typeof this.props.onCategorySelected === 'function') { | |
this.props.onCategorySelected('more'); | |
} | |
return; | |
} | |
const url = item.extensionURL ? item.extensionURL : extensionId; | |
if (!item.disabled) { | |
if (this.props.vm.extensionManager.isExtensionLoaded(extensionId)) { | |
if (typeof this.props.onCategorySelected === 'function') { | |
this.props.onCategorySelected(extensionId); | |
} | |
} else { | |
this.props.vm.extensionManager.loadExtensionURL(url) | |
.then(() => { | |
if (typeof this.props.onCategorySelected === 'function') { | |
this.props.onCategorySelected(extensionId); | |
} | |
}) | |
.catch(err => { | |
log.error(err); | |
// eslint-disable-next-line no-alert | |
alert(err); | |
}); | |
} | |
} | |
} | |
render () { | |
let library = null; | |
if (this.state.gallery || this.state.galleryError || this.state.galleryTimedOut) { | |
library = extensionLibraryContent.map(toLibraryItem); | |
library.push('---'); | |
if (this.state.gallery) { | |
library.push(toLibraryItem(galleryMore)); | |
const locale = this.props.intl.locale; | |
library.push( | |
...this.state.gallery | |
.map(i => translateGalleryItem(i, locale)) | |
.map(toLibraryItem) | |
); | |
} else if (this.state.galleryError) { | |
library.push(toLibraryItem(galleryError)); | |
} else { | |
library.push(toLibraryItem(galleryLoading)); | |
} | |
} | |
return ( | |
<LibraryComponent | |
data={library} | |
filterable | |
persistableKey="extensionId" | |
id="extensionLibrary" | |
tags={extensionTags} | |
title={this.props.intl.formatMessage(messages.extensionTitle)} | |
visible={this.props.visible} | |
onItemSelected={this.handleItemSelect} | |
onRequestClose={this.props.onRequestClose} | |
/> | |
); | |
} | |
} | |
ExtensionLibrary.propTypes = { | |
intl: intlShape.isRequired, | |
onActivateBlocksTab: PropTypes.func, | |
onCategorySelected: PropTypes.func, | |
onEnableProcedureReturns: PropTypes.func, | |
onOpenCustomExtensionModal: PropTypes.func, | |
onRequestClose: PropTypes.func, | |
visible: PropTypes.bool, | |
vm: PropTypes.instanceOf(VM).isRequired // eslint-disable-line react/no-unused-prop-types | |
}; | |
export default injectIntl(ExtensionLibrary); | |