329 lines
11 KiB
React
329 lines
11 KiB
React
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 soundLibraryContent from '../lib/libraries/sounds.json';
|
|
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 {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';
|
|
|
|
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)});
|
|
}
|
|
|
|
handleSurpriseSound () {
|
|
const soundItem = soundLibraryContent[Math.floor(Math.random() * soundLibraryContent.length)];
|
|
const vmSound = {
|
|
format: soundItem.format,
|
|
md5: soundItem.md5,
|
|
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;
|
|
handleFileUpload(e.target, (buffer, fileType, fileName) => {
|
|
soundUpload(buffer, fileType, storage, newSound => {
|
|
newSound.name = fileName;
|
|
this.props.vm.addSound(newSound).then(this.handleNewSound);
|
|
});
|
|
});
|
|
}
|
|
|
|
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 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 (
|
|
<AssetPanel
|
|
buttons={[{
|
|
title: intl.formatMessage(messages.addSound),
|
|
img: addSoundFromLibraryIcon,
|
|
onClick: onNewSoundFromLibraryClick
|
|
}, {
|
|
title: intl.formatMessage(messages.fileUploadSound),
|
|
img: fileUploadIcon,
|
|
onClick: this.handleFileUploadClick,
|
|
fileAccept: '.wav, .mp3',
|
|
fileChange: this.handleSoundUpload,
|
|
fileInput: this.setFileInput,
|
|
fileMultiple: true
|
|
}, {
|
|
title: intl.formatMessage(messages.surpriseSound),
|
|
img: surpriseIcon,
|
|
onClick: this.handleSurpriseSound
|
|
}, {
|
|
title: intl.formatMessage(messages.recordSound),
|
|
img: addSoundFromRecordingIcon,
|
|
onClick: onNewSoundFromRecordingClick
|
|
}, {
|
|
title: intl.formatMessage(messages.addSound),
|
|
img: searchIcon,
|
|
onClick: onNewSoundFromLibraryClick
|
|
}]}
|
|
dragType={DragConstants.SOUND}
|
|
isRtl={isRtl}
|
|
items={sounds}
|
|
selectedItemIndex={this.state.selectedSoundIndex}
|
|
onDeleteClick={this.handleDeleteSound}
|
|
onDrop={this.handleDrop}
|
|
onDuplicateClick={this.handleDuplicateSound}
|
|
onExportClick={this.handleExportSound}
|
|
onItemClick={this.handleSelectSound}
|
|
>
|
|
{sprite.sounds && sprite.sounds[this.state.selectedSoundIndex] ? (
|
|
<SoundEditor soundIndex={this.state.selectedSoundIndex} />
|
|
) : null}
|
|
{this.props.soundRecorderVisible ? (
|
|
<RecordModal
|
|
onNewSound={this.handleNewSound}
|
|
/>
|
|
) : null}
|
|
{this.props.soundLibraryVisible ? (
|
|
<SoundLibrary
|
|
vm={this.props.vm}
|
|
onNewSound={this.handleNewSound}
|
|
onRequestClose={this.props.onRequestCloseSoundLibrary}
|
|
/>
|
|
) : null}
|
|
</AssetPanel>
|
|
);
|
|
}
|
|
}
|
|
|
|
SoundTab.propTypes = {
|
|
dispatchUpdateRestore: PropTypes.func,
|
|
editingTarget: PropTypes.string,
|
|
intl: intlShape,
|
|
isRtl: PropTypes.bool,
|
|
onActivateCostumesTab: PropTypes.func.isRequired,
|
|
onNewSoundFromLibraryClick: PropTypes.func.isRequired,
|
|
onNewSoundFromRecordingClick: PropTypes.func.isRequired,
|
|
onRequestCloseSoundLibrary: 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));
|
|
}
|
|
});
|
|
|
|
export default errorBoundaryHOC('Sound Tab')(
|
|
injectIntl(connect(
|
|
mapStateToProps,
|
|
mapDispatchToProps
|
|
)(SoundTab))
|
|
);
|