s4s-editor / src /containers /sound-tab.jsx
soiz1's picture
Upload 2891 files
6bcb42f verified
raw
history blame
13.1 kB
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
};
if (soundItem.fromPenguinModLibrary) {
vmSound.fromPenguinModLibrary = true;
vmSound.libraryId = soundItem.libraryFilePage;
}
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 (
<AssetPanel
buttons={isSupported ? [{
title: intl.formatMessage(messages.addSound),
img: addSoundFromLibraryIcon,
onClick: onNewSoundFromLibraryClick
}, {
title: intl.formatMessage(messages.fileUploadSound),
img: fileUploadIcon,
onClick: this.handleFileUploadClick,
fileAccept: '.wav, .mp3, .ogg, .flac, .aac, .m4a',
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] ? (
isSupported ? (
<SoundEditor soundIndex={this.state.selectedSoundIndex} />
) : (
<SoundEditorNotSupported />
)
) : 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,
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))
);