import PropTypes from 'prop-types'; import React from 'react'; import bindAll from 'lodash.bindall'; import {defineMessages, intlShape, injectIntl} from 'react-intl'; import VM from 'scratch-vm'; import AssetPanel from '../components/asset-panel/asset-panel.jsx'; import soundIcon from '../components/asset-panel/icon--sound.svg'; import soundIconRtl from '../components/asset-panel/icon--sound-rtl.svg'; import addSoundFromLibraryIcon from '../components/asset-panel/icon--add-sound-lib.svg'; import addSoundFromRecordingIcon from '../components/asset-panel/icon--add-sound-record.svg'; import fileUploadIcon from '../components/action-menu/icon--file-upload.svg'; import surpriseIcon from '../components/action-menu/icon--surprise.svg'; import searchIcon from '../components/action-menu/icon--search.svg'; import RecordModal from './record-modal.jsx'; import SoundEditor from './sound-editor.jsx'; import SoundLibrary from './sound-library.jsx'; import SoundEditorNotSupported from '../components/tw-sound-editor-not-supported/sound-editor-not-supported.jsx'; import {getSoundLibrary} from '../lib/libraries/tw-async-libraries'; import {handleFileUpload, soundUpload} from '../lib/file-uploader.js'; import errorBoundaryHOC from '../lib/error-boundary-hoc.jsx'; import DragConstants from '../lib/drag-constants'; import downloadBlob from '../lib/download-blob'; import SharedAudioContext from '../lib/audio/shared-audio-context.js'; import {connect} from 'react-redux'; import { closeSoundLibrary, openSoundLibrary, openSoundRecorder } from '../reducers/modals'; import { activateTab, COSTUMES_TAB_INDEX } from '../reducers/editor-tab'; import {setRestore} from '../reducers/restore-deletion'; import {showStandardAlert, closeAlertWithId} from '../reducers/alerts'; class SoundTab extends React.Component { constructor (props) { super(props); bindAll(this, [ 'handleSelectSound', 'handleDeleteSound', 'handleDuplicateSound', 'handleExportSound', 'handleNewSound', 'handleSurpriseSound', 'handleFileUploadClick', 'handleSoundUpload', 'handleDrop', 'setFileInput' ]); this.state = {selectedSoundIndex: 0}; } componentWillReceiveProps (nextProps) { const { editingTarget, sprites, stage } = nextProps; const target = editingTarget && sprites[editingTarget] ? sprites[editingTarget] : stage; if (!target || !target.sounds) { return; } // If switching editing targets, reset the sound index if (this.props.editingTarget !== editingTarget) { this.setState({selectedSoundIndex: 0}); } else if (this.state.selectedSoundIndex > target.sounds.length - 1) { this.setState({selectedSoundIndex: Math.max(target.sounds.length - 1, 0)}); } } handleSelectSound (soundIndex) { this.setState({selectedSoundIndex: soundIndex}); } handleDeleteSound (soundIndex) { const restoreFun = this.props.vm.deleteSound(soundIndex); if (soundIndex >= this.state.selectedSoundIndex) { this.setState({selectedSoundIndex: Math.max(0, soundIndex - 1)}); } this.props.dispatchUpdateRestore({restoreFun, deletedItem: 'Sound'}); } handleExportSound (soundIndex) { const item = this.props.vm.editingTarget.sprite.sounds[soundIndex]; const blob = new Blob([item.asset.data], {type: item.asset.assetType.contentType}); downloadBlob(`${item.name}.${item.asset.dataFormat}`, blob); } handleDuplicateSound (soundIndex) { this.props.vm.duplicateSound(soundIndex).then(() => { this.setState({selectedSoundIndex: soundIndex + 1}); }); } handleNewSound () { if (!this.props.vm.editingTarget) { return null; } const sprite = this.props.vm.editingTarget.sprite; const sounds = sprite.sounds ? sprite.sounds : []; this.setState({selectedSoundIndex: Math.max(sounds.length - 1, 0)}); } async handleSurpriseSound () { const soundLibraryContent = await getSoundLibrary(); const soundItem = soundLibraryContent[Math.floor(Math.random() * soundLibraryContent.length)]; const vmSound = { format: soundItem.dataFormat, md5: soundItem.md5ext, rate: soundItem.rate, sampleCount: soundItem.sampleCount, name: soundItem.name }; this.props.vm.addSound(vmSound).then(() => { this.handleNewSound(); }); } handleFileUploadClick () { this.fileInput.click(); } handleSoundUpload (e) { const storage = this.props.vm.runtime.storage; const targetId = this.props.vm.editingTarget.id; this.props.onShowImporting(); handleFileUpload(e.target, (buffer, fileType, fileName, fileIndex, fileCount) => { soundUpload(buffer, fileType, storage, newSound => { newSound.name = fileName; this.props.vm.addSound(newSound, targetId).then(() => { this.handleNewSound(); if (fileIndex === fileCount - 1) { this.props.onCloseImporting(); } }); }, this.props.onCloseImporting); }, this.props.onCloseImporting); } handleDrop (dropInfo) { if (dropInfo.dragType === DragConstants.SOUND) { const sprite = this.props.vm.editingTarget.sprite; const activeSound = sprite.sounds[this.state.selectedSoundIndex]; this.props.vm.reorderSound(this.props.vm.editingTarget.id, dropInfo.index, dropInfo.newIndex); this.setState({selectedSoundIndex: sprite.sounds.indexOf(activeSound)}); } else if (dropInfo.dragType === DragConstants.BACKPACK_COSTUME) { this.props.onActivateCostumesTab(); this.props.vm.addCostume(dropInfo.payload.body, { name: dropInfo.payload.name }); } else if (dropInfo.dragType === DragConstants.BACKPACK_SOUND) { this.props.vm.addSound({ md5: dropInfo.payload.body, name: dropInfo.payload.name }).then(this.handleNewSound); } } setFileInput (input) { this.fileInput = input; } render () { const { dispatchUpdateRestore, // eslint-disable-line no-unused-vars intl, isRtl, vm, onNewSoundFromLibraryClick, onNewSoundFromRecordingClick } = this.props; if (!vm.editingTarget) { return null; } const isSupported = !!(vm.runtime.audioEngine && new SharedAudioContext()); const sprite = vm.editingTarget.sprite; const sounds = sprite.sounds ? sprite.sounds.map(sound => ( { url: isRtl ? soundIconRtl : soundIcon, name: sound.name, details: (sound.sampleCount / sound.rate).toFixed(2), dragPayload: sound } )) : []; const messages = defineMessages({ fileUploadSound: { defaultMessage: 'Upload Sound', description: 'Button to upload sound from file in the editor tab', id: 'gui.soundTab.fileUploadSound' }, surpriseSound: { defaultMessage: 'Surprise', description: 'Button to get a random sound in the editor tab', id: 'gui.soundTab.surpriseSound' }, recordSound: { defaultMessage: 'Record', description: 'Button to record a sound in the editor tab', id: 'gui.soundTab.recordSound' }, addSound: { defaultMessage: 'Choose a Sound', description: 'Button to add a sound in the editor tab', id: 'gui.soundTab.addSoundFromLibrary' } }); return ( {sprite.sounds && sprite.sounds[this.state.selectedSoundIndex] ? ( isSupported ? ( ) : ( ) ) : null} {this.props.soundRecorderVisible ? ( ) : null} {this.props.soundLibraryVisible ? ( ) : null} ); } } SoundTab.propTypes = { dispatchUpdateRestore: PropTypes.func, editingTarget: PropTypes.string, intl: intlShape, isRtl: PropTypes.bool, onActivateCostumesTab: PropTypes.func.isRequired, onCloseImporting: PropTypes.func.isRequired, onNewSoundFromLibraryClick: PropTypes.func.isRequired, onNewSoundFromRecordingClick: PropTypes.func.isRequired, onRequestCloseSoundLibrary: PropTypes.func.isRequired, onShowImporting: PropTypes.func.isRequired, soundLibraryVisible: PropTypes.bool, soundRecorderVisible: PropTypes.bool, sprites: PropTypes.shape({ id: PropTypes.shape({ sounds: PropTypes.arrayOf(PropTypes.shape({ name: PropTypes.string.isRequired })) }) }), stage: PropTypes.shape({ sounds: PropTypes.arrayOf(PropTypes.shape({ name: PropTypes.string.isRequired })) }), vm: PropTypes.instanceOf(VM).isRequired }; const mapStateToProps = state => ({ editingTarget: state.scratchGui.targets.editingTarget, isRtl: state.locales.isRtl, sprites: state.scratchGui.targets.sprites, stage: state.scratchGui.targets.stage, soundLibraryVisible: state.scratchGui.modals.soundLibrary, soundRecorderVisible: state.scratchGui.modals.soundRecorder }); const mapDispatchToProps = dispatch => ({ onActivateCostumesTab: () => dispatch(activateTab(COSTUMES_TAB_INDEX)), onNewSoundFromLibraryClick: e => { e.preventDefault(); dispatch(openSoundLibrary()); }, onNewSoundFromRecordingClick: () => { dispatch(openSoundRecorder()); }, onRequestCloseSoundLibrary: () => { dispatch(closeSoundLibrary()); }, dispatchUpdateRestore: restoreState => { dispatch(setRestore(restoreState)); }, onCloseImporting: () => dispatch(closeAlertWithId('importingAsset')), onShowImporting: () => dispatch(showStandardAlert('importingAsset')) }); export default errorBoundaryHOC('Sound Tab')( injectIntl(connect( mapStateToProps, mapDispatchToProps )(SoundTab)) );