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 ( {credit.name} ); } 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 ( {credit.name} ); } 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 ( ); } } 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);