scratch-gui / src /containers /extensions-tab.jsx
soiz1's picture
Upload folder using huggingface_hub
8fd7a1d verified
import PropTypes from 'prop-types';
import React from 'react';
import {connect} from 'react-redux';
import {defineMessages, intlShape, injectIntl} from 'react-intl';
import VM from 'scratch-vm';
import Box from '../components/box/box.jsx';
import {
openExtensionLibrary,
closeExtensionLibrary,
openCustomExtensionModal
} from '../reducers/modals';
import {activateTab, BLOCKS_TAB_INDEX, EXTENSIONS_TAB_INDEX} from '../reducers/editor-tab';
import styles from './extensions-tab.css';
import addExtensionIcon from '../components/gui/icon--extensions.svg';
const messages = defineMessages({
addExtension: {
defaultMessage: 'Add Extension',
description: 'Button to add an extension in the extensions tab',
id: 'gui.extensionsTab.addExtension'
}
});
class ExtensionsTab extends React.Component {
constructor (props) {
super(props);
this.handleAddExtensionClick = this.handleAddExtensionClick.bind(this);
this.handleExtensionClick = this.handleExtensionClick.bind(this);
this.updateLoadedExtensions = this.updateLoadedExtensions.bind(this);
this.handleExtensionAdded = this.handleExtensionAdded.bind(this);
this.handleEnableProcedureReturns = this.handleEnableProcedureReturns.bind(this);
this.handleCategorySelected = this.handleCategorySelected.bind(this);
this.state = {
loadedExtensions: []
};
}
componentDidMount () {
this.updateLoadedExtensions();
// Listen for extension loading changes
if (this.props.vm) {
this.props.vm.on('EXTENSION_ADDED', this.handleExtensionAdded);
this.props.vm.runtime.on('PROJECT_LOADED', this.updateLoadedExtensions);
}
}
componentWillUnmount () {
if (this.props.vm) {
this.props.vm.off('EXTENSION_ADDED', this.handleExtensionAdded);
this.props.vm.runtime.off('PROJECT_LOADED', this.updateLoadedExtensions);
}
}
componentDidUpdate (prevProps) {
// Recalculate block counts every time the extensions tab becomes active
if (this.props.activeTabIndex === EXTENSIONS_TAB_INDEX &&
prevProps.activeTabIndex !== EXTENSIONS_TAB_INDEX) {
console.log('🏷️ Extensions tab became active - recalculating block counts');
this.updateLoadedExtensions();
}
}
handleAddExtensionClick () {
this.props.onExtensionButtonClick();
}
handleExtensionClick (extensionId) {
// Handle clicks on loaded extensions - could open documentation or settings
console.log('Extension clicked:', extensionId);
}
/**
* Handle extension added event with automatic tab navigation
*/
handleExtensionAdded () {
// First, navigate to blocks tab to ensure extension blocks are loaded
this.props.onActivateBlocksTab();
}
/**
* Count blocks used by a specific extension across all targets
* @param {string} extensionId - The extension ID to count blocks for
* @returns {number} - Number of blocks used by this extension
*/
countExtensionBlocks (extensionId) {
if (!this.props.vm || !this.props.vm.runtime) return 0;
let blockCount = 0;
// Iterate through all targets (sprites and stage)
this.props.vm.runtime.targets.forEach(target => {
if (!target.blocks) return;
// Get all blocks for this target
const blocks = target.blocks._blocks;
if (!blocks) return;
// Count blocks that belong to this extension
Object.values(blocks).forEach(block => {
if (block && block.opcode && block.opcode.startsWith(extensionId + '_')) {
blockCount++;
}
});
});
return blockCount;
}
/**
* Get information about a loaded extension
* @param {string} extensionId - The extension ID
* @returns {object} - Extension information including name and URL
*/
getExtensionInfo (extensionId) {
if (!this.props.vm || !this.props.vm.runtime) return { name: extensionId, url: null };
// Try to get extension info from runtime block info
const blockInfo = this.props.vm.runtime._blockInfo || [];
const extensionInfo = blockInfo.find(info => info.id === extensionId);
if (extensionInfo) {
return {
name: extensionInfo.name || extensionId,
url: this.getExtensionURL(extensionId)
};
}
return {
name: extensionId,
url: this.getExtensionURL(extensionId)
};
}
/**
* Get the URL for an extension if it's a custom extension
* @param {string} extensionId - The extension ID
* @returns {string|null} - The extension URL or null if it's a built-in extension
*/
getExtensionURL (extensionId) {
if (!this.props.vm || !this.props.vm.extensionManager) return null;
const extensionURLs = this.props.vm.extensionManager.getExtensionURLs();
return extensionURLs[extensionId] || null;
}
/**
* Update the list of loaded extensions with their block counts
*/
updateLoadedExtensions () {
if (!this.props.vm || !this.props.vm.extensionManager) {
this.setState({ loadedExtensions: [] });
return;
}
console.log('🔄 Recalculating extension block counts...');
const loadedExtensionIds = Array.from(this.props.vm.extensionManager._loadedExtensions.keys());
const extensionsWithCounts = loadedExtensionIds.map(extensionId => {
const blockCount = this.countExtensionBlocks(extensionId);
const info = this.getExtensionInfo(extensionId);
console.log(`📦 Extension "${info.name}" (${extensionId}): ${blockCount} blocks`);
return {
id: extensionId,
name: info.name,
url: info.url,
blockCount: blockCount
};
});
// Sort by block count (descending) and then by name
extensionsWithCounts.sort((a, b) => {
if (a.blockCount !== b.blockCount) {
return b.blockCount - a.blockCount;
}
return a.name.localeCompare(b.name);
});
this.setState({ loadedExtensions: extensionsWithCounts });
console.log('✅ Extension block counts updated');
}
/**
* Handle enabling procedure returns - set flag for blocks component to handle
*/
handleEnableProcedureReturns () {
console.log('ExtensionsTab: handleEnableProcedureReturns called - setting pending flag');
// Set a flag for the blocks component to handle when it becomes active
if (this.props.vm) {
this.props.vm._pendingProcedureReturns = true;
console.log('ExtensionsTab: Set _pendingProcedureReturns flag');
}
}
/**
* Handle category selection - set flag for blocks component to handle
*/
handleCategorySelected (categoryId) {
console.log('ExtensionsTab: handleCategorySelected called with', categoryId, '- setting pending flag');
// Set a flag for the blocks component to handle when it becomes active
if (this.props.vm) {
this.props.vm._pendingCategorySelection = categoryId;
console.log('ExtensionsTab: Set _pendingCategorySelection flag to', categoryId);
}
}
render () {
const {
intl,
onCategorySelected,
vm
} = this.props;
return (
<Box className={styles.extensionsTab}>
<Box className={styles.extensionsGrid}>
{/* Add Extension Button - always first */}
<Box className={styles.extensionGridItem}>
<button
className={styles.addExtensionButton}
title={intl.formatMessage(messages.addExtension)}
onClick={this.handleAddExtensionClick}
>
<img
className={styles.addExtensionIcon}
draggable={false}
src={addExtensionIcon}
/>
<span className={styles.addExtensionText}>
{intl.formatMessage(messages.addExtension)}
</span>
</button>
</Box>
{/* Loaded Extensions */}
{this.state.loadedExtensions.map(extension => (
<Box
key={extension.id}
className={styles.extensionGridItem}
onClick={() => this.handleExtensionClick(extension.id)}
>
<Box className={styles.extensionCard}>
<Box className={styles.extensionName}>
{extension.name}
</Box>
<Box className={styles.extensionBlockCount}>
<Box className={styles.blockCountNumber}>
{extension.blockCount}
</Box>
<Box className={styles.blockCountLabel}>
{extension.blockCount === 1 ? 'block' : 'blocks'}
</Box>
</Box>
</Box>
</Box>
))}
</Box>
</Box>
);
}
}
ExtensionsTab.propTypes = {
activeTabIndex: PropTypes.number,
intl: intlShape.isRequired,
onCategorySelected: PropTypes.func,
onExtensionButtonClick: PropTypes.func,
onActivateBlocksTab: PropTypes.func,
onActivateExtensionsTab: PropTypes.func,
onOpenCustomExtensionModal: PropTypes.func,
vm: PropTypes.instanceOf(VM)
};
const mapStateToProps = state => ({
activeTabIndex: state.scratchGui.editorTab.activeTabIndex
});
const mapDispatchToProps = dispatch => ({
onExtensionButtonClick: () => dispatch(openExtensionLibrary()),
onActivateBlocksTab: () => dispatch(activateTab(BLOCKS_TAB_INDEX)),
onActivateExtensionsTab: () => dispatch(activateTab(EXTENSIONS_TAB_INDEX)),
onOpenCustomExtensionModal: () => dispatch(openCustomExtensionModal())
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(injectIntl(ExtensionsTab));