Spaces:
Runtime error
Runtime error
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)); | |