diff --git a/package.json b/package.json index dd46c50ee..e44c89a02 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,7 @@ "scratch-paint": "0.2.0-prerelease.20180222192821", "scratch-render": "0.1.0-prerelease.1516837442", "scratch-storage": "0.4.0", - "scratch-vm": "0.1.0-prerelease.1518812805-prerelease.1518812820", + "scratch-vm": "0.1.0-prerelease.1519653784-prerelease.1519653801", "selenium-webdriver": "3.6.0", "startaudiocontext": "1.2.1", "style-loader": "^0.20.0", diff --git a/src/components/asset-panel/selector.jsx b/src/components/asset-panel/selector.jsx index 012206517..b69707781 100644 --- a/src/components/asset-panel/selector.jsx +++ b/src/components/asset-panel/selector.jsx @@ -13,6 +13,7 @@ const Selector = props => { items, selectedItemIndex, onDeleteClick, + onDuplicateClick, onItemClick } = props; @@ -30,6 +31,7 @@ const Selector = props => { selected={index === selectedItemIndex} onClick={onItemClick} onDeleteButtonClick={onDeleteClick} + onDuplicateButtonClick={onDuplicateClick} /> ))} @@ -58,6 +60,7 @@ Selector.propTypes = { name: PropTypes.string.isRequired })), onDeleteClick: PropTypes.func, + onDuplicateClick: PropTypes.func, onItemClick: PropTypes.func.isRequired, selectedItemIndex: PropTypes.number.isRequired }; diff --git a/src/containers/costume-tab.jsx b/src/containers/costume-tab.jsx index bb48f1b5d..e22baebf2 100644 --- a/src/containers/costume-tab.jsx +++ b/src/containers/costume-tab.jsx @@ -51,10 +51,21 @@ class CostumeTab extends React.Component { bindAll(this, [ 'handleSelectCostume', 'handleDeleteCostume', + 'handleDuplicateCostume', 'handleNewCostume', 'handleNewBlankCostume' ]); - this.state = {selectedCostumeIndex: 0}; + const { + editingTarget, + sprites, + stage + } = props; + const target = editingTarget && sprites[editingTarget] ? sprites[editingTarget] : stage; + if (target && target.currentCostume) { + this.state = {selectedCostumeIndex: target.currentCostume}; + } else { + this.state = {selectedCostumeIndex: 0}; + } } componentWillReceiveProps (nextProps) { const { @@ -75,6 +86,11 @@ class CostumeTab extends React.Component { handleDeleteCostume (costumeIndex) { this.props.vm.deleteCostume(costumeIndex); } + handleDuplicateCostume (costumeIndex) { + this.props.vm.duplicateCostume(costumeIndex).then(() => { + this.setState({selectedCostumeIndex: costumeIndex + 1}); + }); + } handleNewCostume () { if (!this.props.vm.editingTarget) return; const costumes = this.props.vm.editingTarget.getCostumes() || []; @@ -145,6 +161,7 @@ class CostumeTab extends React.Component { items={target.costumes || []} selectedItemIndex={this.state.selectedCostumeIndex} onDeleteClick={target.costumes.length > 1 ? this.handleDeleteCostume : null} + onDuplicateClick={this.handleDuplicateCostume} onItemClick={this.handleSelectCostume} > {target.costumes ? @@ -186,7 +203,8 @@ CostumeTab.propTypes = { id: PropTypes.shape({ costumes: PropTypes.arrayOf(PropTypes.shape({ url: PropTypes.string, - name: PropTypes.string.isRequired + name: PropTypes.string.isRequired, + skinId: PropTypes.number })) }) }), diff --git a/src/containers/sound-editor.jsx b/src/containers/sound-editor.jsx index 781b2cc5f..bbe032c0b 100644 --- a/src/containers/sound-editor.jsx +++ b/src/containers/sound-editor.jsx @@ -1,6 +1,7 @@ import bindAll from 'lodash.bindall'; import PropTypes from 'prop-types'; import React from 'react'; +import WavEncoder from 'wav-encoder'; import {connect} from 'react-redux'; @@ -9,6 +10,7 @@ import {computeChunkedRMS} from '../lib/audio/audio-util.js'; import AudioEffects from '../lib/audio/audio-effects.js'; import SoundEditorComponent from '../components/sound-editor/sound-editor.jsx'; import AudioBufferPlayer from '../lib/audio/audio-buffer-player.js'; +import log from '../lib/log.js'; const UNDO_STACK_SIZE = 99; @@ -72,11 +74,25 @@ class SoundEditor extends React.Component { } this.undoStack.push(this.copyCurrentBuffer()); } + // Encode the new sound into a wav so that it can be stored + let wavBuffer = null; + try { + wavBuffer = WavEncoder.encode.sync({ + sampleRate: sampleRate, + channelData: [samples] + }); + } catch (e) { + // This error state is mostly for the mock sounds used during testing. + // Any incorrect sound buffer trying to get interpretd as a Wav file + // should yield this error. + log.error(`Encountered error while trying to encode sound update: ${e}`); + } + this.resetState(samples, sampleRate); this.props.onUpdateSoundBuffer( this.props.soundIndex, - this.audioBufferPlayer.buffer - ); + this.audioBufferPlayer.buffer, + wavBuffer ? new Uint8Array(wavBuffer) : new Uint8Array()); } handlePlay () { this.audioBufferPlayer.play( diff --git a/src/containers/sound-tab.jsx b/src/containers/sound-tab.jsx index af95c6b0b..8686ae6f9 100644 --- a/src/containers/sound-tab.jsx +++ b/src/containers/sound-tab.jsx @@ -26,6 +26,7 @@ class SoundTab extends React.Component { bindAll(this, [ 'handleSelectSound', 'handleDeleteSound', + 'handleDuplicateSound', 'handleNewSound' ]); this.state = {selectedSoundIndex: 0}; @@ -56,6 +57,12 @@ class SoundTab extends React.Component { } } + handleDuplicateSound (soundIndex) { + this.props.vm.duplicateSound(soundIndex).then(() => { + this.setState({selectedSoundIndex: soundIndex + 1}); + }); + } + handleNewSound () { if (!this.props.vm.editingTarget) { return null; @@ -116,6 +123,7 @@ class SoundTab extends React.Component { }))} selectedItemIndex={this.state.selectedSoundIndex} onDeleteClick={this.handleDeleteSound} + onDuplicateClick={this.handleDuplicateSound} onItemClick={this.handleSelectSound} > {sprite.sounds && sprite.sounds[this.state.selectedSoundIndex] ? ( diff --git a/test/helpers/selenium-helper.js b/test/helpers/selenium-helper.js index c8ead9072..b63ea2bf6 100644 --- a/test/helpers/selenium-helper.js +++ b/test/helpers/selenium-helper.js @@ -25,6 +25,7 @@ class SeleniumHelper { // List of useful xpath scopes for finding elements return { blocksTab: "*[@id='react-tabs-1']", + costumesTab: "*[@id='react-tabs-3']", modal: '*[@class="ReactModalPortal"]', reportedValue: '*[@class="blocklyDropDownContent"]', soundsTab: "*[@id='react-tabs-5']", diff --git a/test/integration/costumes.test.js b/test/integration/costumes.test.js index 98f31629a..f07c3d0f3 100644 --- a/test/integration/costumes.test.js +++ b/test/integration/costumes.test.js @@ -7,7 +7,9 @@ const { findByXpath, getDriver, getLogs, - loadUri + loadUri, + rightClickText, + scope } = new SeleniumHelper(); const uri = path.resolve(__dirname, '../../build/index.html'); @@ -36,6 +38,22 @@ describe('Working with costumes', () => { await expect(logs).toEqual([]); }); + test('Duplicating a costume', async () => { + await loadUri(uri); + await clickXpath('//button[@title="tryit"]'); + await clickText('Costumes'); + + await rightClickText('costume1', scope.costumesTab); + await clickText('duplicate', scope.costumesTab); + await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for duplication to finish + + // Make sure the duplicated costume is named correctly. + await clickText('costume3', scope.costumesTab); + + const logs = await getLogs(); + await expect(logs).toEqual([]); + }); + test('Adding a backdrop', async () => { await loadUri(uri); await clickXpath('//button[@title="tryit"]'); diff --git a/test/integration/sounds.test.js b/test/integration/sounds.test.js index 244ebb9b4..e31548684 100644 --- a/test/integration/sounds.test.js +++ b/test/integration/sounds.test.js @@ -63,6 +63,22 @@ describe('Working with sounds', () => { await expect(logs).toEqual([]); }); + test('Duplicating a sound', async () => { + await loadUri(uri); + await clickXpath('//button[@title="tryit"]'); + await clickText('Sounds'); + + await rightClickText('Meow', scope.soundsTab); + await clickText('duplicate', scope.soundsTab); + await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for error + + // Make sure the duplicated sound is named correctly. + await clickText('Meow2', scope.soundsTab); + + const logs = await getLogs(); + await expect(logs).toEqual([]); + }); + // Regression test for gui issue #1320 test('Switching sprites with different numbers of sounds', async () => { await loadUri(uri);