scratch-gui / src /containers /extension-library.jsx
soiz1's picture
Upload folder using huggingface_hub
8fd7a1d verified
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);