diff --git a/packages/scratch-gui/src/containers/costume-tab.jsx b/packages/scratch-gui/src/containers/costume-tab.jsx index a475e59ab7..196474fd20 100644 --- a/packages/scratch-gui/src/containers/costume-tab.jsx +++ b/packages/scratch-gui/src/containers/costume-tab.jsx @@ -38,6 +38,8 @@ import searchIcon from '../components/action-menu/icon--search.svg'; import costumeLibraryContent from '../lib/libraries/costumes.json'; import backdropLibraryContent from '../lib/libraries/backdrops.json'; import {ModalFocusContext} from '../contexts/modal-focus-context.jsx'; +import {costumeShape} from '../lib/assets-prop-types.js'; +import mergeDynamicAssets from '../lib/merge-dynamic-assets.js'; let messages = defineMessages({ addLibraryBackdropMsg: { @@ -91,7 +93,9 @@ class CostumeTab extends React.Component { 'handleFileUploadClick', 'handleCostumeUpload', 'handleDrop', - 'setFileInput' + 'setFileInput', + 'mergeDynamicCostumes', + 'mergeDynamicBackdrops' ]); const { editingTarget, @@ -104,7 +108,18 @@ class CostumeTab extends React.Component { } else { this.state = {selectedCostumeIndex: 0}; } + this.processedCostumes = {}; + this.processedBackdrops = {}; } + componentDidMount () { + this.handleDocumentClick = () => { + if (document.activeElement !== document.body) { + document.activeElement.blur(); + } + }; + document.addEventListener('mousedown', this.handleDocumentClick); + } + componentWillReceiveProps (nextProps) { const { editingTarget, @@ -133,8 +148,32 @@ class CostumeTab extends React.Component { this.setState({selectedCostumeIndex: target.currentCostume}); } } + + componentWillUnmount () { + document.removeEventListener('mousedown', this.handleDocumentClick); + } static contextType = ModalFocusContext; + mergeDynamicCostumes () { + if (this.processedCostumes.source === this.props.dynamicCostumes) { + return this.processedCostumes.data; + } + this.processedCostumes = mergeDynamicAssets( + costumeLibraryContent, + this.props.dynamicCostumes + ); + return this.processedCostumes.data; + } + mergeDynamicBackdrops () { + if (this.processedBackdrops.source === this.props.dynamicBackdrops) { + return this.processedBackdrops.data; + } + this.processedBackdrops = mergeDynamicAssets( + backdropLibraryContent, + this.props.dynamicBackdrops + ); + return this.processedBackdrops.data; + } handleSelectCostume (costumeIndex) { this.props.vm.editingTarget.setCostume(costumeIndex); this.setState({selectedCostumeIndex: costumeIndex}); @@ -190,7 +229,9 @@ class CostumeTab extends React.Component { this.handleNewCostume(emptyCostume(name)); } handleSurpriseCostume () { - const item = costumeLibraryContent[Math.floor(Math.random() * costumeLibraryContent.length)]; + const costumes = this.mergeDynamicCostumes(); + + const item = costumes[Math.floor(Math.random() * costumes.length)]; const vmCostume = { name: item.name, md5: item.md5ext, @@ -202,7 +243,9 @@ class CostumeTab extends React.Component { this.handleNewCostume(vmCostume, true /* fromCostumeLibrary */); } handleSurpriseBackdrop () { - const item = backdropLibraryContent[Math.floor(Math.random() * backdropLibraryContent.length)]; + const backdrops = this.mergeDynamicBackdrops(); + + const item = backdrops[Math.floor(Math.random() * backdrops.length)]; const vmCostume = { name: item.name, md5: item.md5ext, @@ -375,7 +418,9 @@ CostumeTab.propTypes = { name: PropTypes.string.isRequired })) }), - vm: PropTypes.instanceOf(VM) + vm: PropTypes.instanceOf(VM), + dynamicCostumes: PropTypes.arrayOf(costumeShape), + dynamicBackdrops: PropTypes.arrayOf(costumeShape) }; const mapStateToProps = state => ({ @@ -383,7 +428,9 @@ const mapStateToProps = state => ({ isRtl: state.locales.isRtl, sprites: state.scratchGui.targets.sprites, stage: state.scratchGui.targets.stage, - dragging: state.scratchGui.assetDrag.dragging + dragging: state.scratchGui.assetDrag.dragging, + dynamicCostumes: state.scratchGui.dynamicAssets.costumes, + dynamicBackdrops: state.scratchGui.dynamicAssets.backdrops }); const mapDispatchToProps = dispatch => ({ diff --git a/packages/scratch-gui/src/containers/sound-tab.jsx b/packages/scratch-gui/src/containers/sound-tab.jsx index 6e4b17ca0c..63b5386baf 100644 --- a/packages/scratch-gui/src/containers/sound-tab.jsx +++ b/packages/scratch-gui/src/containers/sound-tab.jsx @@ -41,6 +41,9 @@ import {setRestore} from '../reducers/restore-deletion'; import {showStandardAlert, closeAlertWithId} from '../reducers/alerts'; import {ModalFocusContext} from '../contexts/modal-focus-context.jsx'; +import {soundShape} from '../lib/assets-prop-types.js'; +import mergeDynamicAssets from '../lib/merge-dynamic-assets.js'; + class SoundTab extends React.Component { constructor (props) { super(props); @@ -55,9 +58,11 @@ class SoundTab extends React.Component { 'handleSoundUpload', 'handleNewSoundFromLibraryClick', 'handleDrop', - 'setFileInput' + 'setFileInput', + 'mergeDynamicAssets' ]); this.state = {selectedSoundIndex: 0}; + this.processedSounds = {}; } componentWillReceiveProps (nextProps) { @@ -82,6 +87,17 @@ class SoundTab extends React.Component { static contextType = ModalFocusContext; + mergeDynamicAssets () { + if (this.processedSounds.source === this.props.dynamicSounds) { + return this.processedSounds.data; + } + this.processedSounds = mergeDynamicAssets( + soundLibraryContent, + this.props.dynamicSounds + ); + return this.processedSounds.data; + } + handleSelectSound (soundIndex) { this.setState({selectedSoundIndex: soundIndex}); } @@ -116,7 +132,9 @@ class SoundTab extends React.Component { } handleSurpriseSound () { - const soundItem = soundLibraryContent[Math.floor(Math.random() * soundLibraryContent.length)]; + const sounds = this.mergeDynamicAssets(); + + const soundItem = sounds[Math.floor(Math.random() * sounds.length)]; const vmSound = { format: soundItem.dataFormat, md5: soundItem.md5ext, @@ -317,7 +335,8 @@ SoundTab.propTypes = { name: PropTypes.string.isRequired })) }), - vm: PropTypes.instanceOf(VM).isRequired + vm: PropTypes.instanceOf(VM).isRequired, + dynamicSounds: PropTypes.arrayOf(soundShape) }; const mapStateToProps = state => ({ @@ -326,7 +345,8 @@ const mapStateToProps = state => ({ sprites: state.scratchGui.targets.sprites, stage: state.scratchGui.targets.stage, soundLibraryVisible: state.scratchGui.modals.soundLibrary, - soundRecorderVisible: state.scratchGui.modals.soundRecorder + soundRecorderVisible: state.scratchGui.modals.soundRecorder, + dynamicSounds: state.scratchGui.dynamicAssets.sounds }); const mapDispatchToProps = dispatch => ({ diff --git a/packages/scratch-gui/src/containers/stage-selector.jsx b/packages/scratch-gui/src/containers/stage-selector.jsx index 5197bccbec..aa41ffd863 100644 --- a/packages/scratch-gui/src/containers/stage-selector.jsx +++ b/packages/scratch-gui/src/containers/stage-selector.jsx @@ -25,6 +25,9 @@ import backdropLibraryContent from '../lib/libraries/backdrops.json'; import {handleFileUpload, costumeUpload} from '../lib/file-uploader.js'; import {ModalFocusContext} from '../contexts/modal-focus-context.jsx'; +import {costumeShape as backdropShape} from '../lib/assets-prop-types.js'; +import mergeDynamicAssets from '../lib/merge-dynamic-assets.js'; + const dragTypes = [ DragConstants.COSTUME, DragConstants.SOUND, @@ -54,8 +57,11 @@ class StageSelector extends React.Component { 'handleTouchEnd', 'handleDrop', 'setFileInput', - 'setRef' + 'setRef', + 'mergeDynamicAssets' ]); + + this.processedBackdrops = {}; } componentDidMount () { document.addEventListener('touchend', this.handleTouchEnd); @@ -66,6 +72,16 @@ class StageSelector extends React.Component { static contextType = ModalFocusContext; + mergeDynamicAssets () { + if (this.processedBackdrops.source === this.props.dynamicBackdrops) { + return this.processedBackdrops.data; + } + this.processedBackdrops = mergeDynamicAssets( + backdropLibraryContent, + this.props.dynamicBackdrops + ); + return this.processedBackdrops.data; + } handleTouchEnd (e) { const {x, y} = getEventXY(e); const {top, left, bottom, right} = this.ref.getBoundingClientRect(); @@ -107,7 +123,9 @@ class StageSelector extends React.Component { handleSurpriseBackdrop (e) { e.stopPropagation(); // Prevent click from falling through to selecting stage. // @todo should this not add a backdrop you already have? - const item = backdropLibraryContent[Math.floor(Math.random() * backdropLibraryContent.length)]; + const backdrops = this.mergeDynamicAssets(); + + const item = backdrops[Math.floor(Math.random() * backdrops.length)]; this.addBackdropFromLibraryItem(item, false); } handleEmptyBackdrop (e) { @@ -198,7 +216,8 @@ StageSelector.propTypes = { intl: intlShape.isRequired, onCloseImporting: PropTypes.func, onSelect: PropTypes.func, - onShowImporting: PropTypes.func + onShowImporting: PropTypes.func, + dynamicBackdrops: PropTypes.arrayOf(backdropShape) }; const mapStateToProps = (state, {asset, id}) => ({ @@ -206,7 +225,8 @@ const mapStateToProps = (state, {asset, id}) => ({ vm: state.scratchGui.vm, receivedBlocks: state.scratchGui.hoveredTarget.receivedBlocks && state.scratchGui.hoveredTarget.sprite === id, - raised: state.scratchGui.blockDrag + raised: state.scratchGui.blockDrag, + dynamicBackdrops: state.scratchGui.dynamicAssets.backdrops }); const mapDispatchToProps = dispatch => ({ diff --git a/packages/scratch-gui/src/containers/target-pane.jsx b/packages/scratch-gui/src/containers/target-pane.jsx index 501ca7fa25..26abd6def5 100644 --- a/packages/scratch-gui/src/containers/target-pane.jsx +++ b/packages/scratch-gui/src/containers/target-pane.jsx @@ -25,6 +25,8 @@ import {fetchSprite, fetchCode} from '../lib/backpack-api'; import randomizeSpritePosition from '../lib/randomize-sprite-position'; import downloadBlob from '../lib/download-blob'; import {ModalFocusContext} from '../contexts/modal-focus-context.jsx'; +import {spriteShape} from '../lib/assets-prop-types.js'; +import mergeDynamicAssets from '../lib/merge-dynamic-assets.js'; class TargetPane extends React.Component { constructor (props) { @@ -50,8 +52,11 @@ class TargetPane extends React.Component { 'handlePaintSpriteClick', 'handleFileUploadClick', 'handleSpriteUpload', - 'setFileInput' + 'setFileInput', + 'mergeDynamicAssets' ]); + + this.processedSprites = {}; } componentDidMount () { this.props.vm.addListener('BLOCK_DRAG_END', this.handleBlockDragEnd); @@ -62,6 +67,16 @@ class TargetPane extends React.Component { static contextType = ModalFocusContext; + mergeDynamicAssets () { + if (this.processedSprites.source === this.props.dynamicSprites) { + return this.processedSprites.data; + } + this.processedSprites = mergeDynamicAssets( + spriteLibraryContent, + this.props.dynamicSprites + ); + return this.processedSprites.data; + } handleChangeSpriteDirection (direction) { this.props.vm.postSpriteInfo({direction}); } @@ -112,7 +127,9 @@ class TargetPane extends React.Component { } } handleSurpriseSpriteClick () { - const surpriseSprites = spriteLibraryContent.filter(sprite => + const sprites = this.mergeDynamicAssets(); + + const surpriseSprites = sprites.filter(sprite => (sprite.tags.indexOf('letters') === -1) && (sprite.tags.indexOf('numbers') === -1) ); const item = surpriseSprites[Math.floor(Math.random() * surpriseSprites.length)]; @@ -296,6 +313,7 @@ TargetPane.propTypes = { intl: intlShape.isRequired, onCloseImporting: PropTypes.func, onShowImporting: PropTypes.func, + dynamicSprites: PropTypes.arrayOf(spriteShape), ...targetPaneProps }; @@ -307,7 +325,8 @@ const mapStateToProps = state => ({ sprites: state.scratchGui.targets.sprites, stage: state.scratchGui.targets.stage, raiseSprites: state.scratchGui.blockDrag, - workspaceMetrics: state.scratchGui.workspaceMetrics + workspaceMetrics: state.scratchGui.workspaceMetrics, + dynamicSprites: state.scratchGui.dynamicAssets.sprites }); const mapDispatchToProps = dispatch => ({