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 ( {/* Add Extension Button - always first */} {/* Loaded Extensions */} {this.state.loadedExtensions.map(extension => ( this.handleExtensionClick(extension.id)} > {extension.name} {extension.blockCount} {extension.blockCount === 1 ? 'block' : 'blocks'} ))} ); } } 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));