Selection and delete
This commit is contained in:
1 parent
0d88c7755b
commit
64562a8a05
5 files changed
+406
-36
No files matched your search
@@ -0,0 +1,119 @@
|
||||
@import "../../css/colors.css";
|
||||
|
||||
$border-radius: 4px;
|
||||
$trim-handle-width: 12px;
|
||||
$trim-handle-height: 14px;
|
||||
$stripe-size: 10px;
|
||||
$hover-scale: 2;
|
||||
|
||||
.absolute {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
|
||||
/* Force the browser to paint separately to avoid composite cost with waveform */
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
.trim-background {
|
||||
cursor: pointer;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.trim-background-mask {
|
||||
border: 1px solid $motion-tertiary;
|
||||
opacity: 0.5;
|
||||
background: $motion-primary;
|
||||
}
|
||||
|
||||
.start-trim-background .trim-background-mask {
|
||||
border-top-left-radius: $border-radius;
|
||||
border-bottom-left-radius: $border-radius;
|
||||
}
|
||||
|
||||
.end-trim-background .trim-background-mask {
|
||||
border-top-right-radius: $border-radius;
|
||||
border-bottom-right-radius: $border-radius;
|
||||
}
|
||||
|
||||
.trim-line {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 0px;
|
||||
height: 100%;
|
||||
border: 1px solid $motion-tertiary;
|
||||
}
|
||||
|
||||
.playhead-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.playhead {
|
||||
/*
|
||||
Even though playhead is just a line, it is 100% width (the width of the waveform)
|
||||
so that we can use transform: translateX() using percentages.
|
||||
*/
|
||||
width: 100%;
|
||||
border-left: 1px solid $motion-primary;
|
||||
border-top: none;
|
||||
border-bottom: none;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.start-trim-line {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.end-trim-line {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.trim-handle {
|
||||
position: absolute;
|
||||
left: calc(-$trim-handle-width / 2);
|
||||
width: $trim-handle-width;
|
||||
height: $trim-handle-height;
|
||||
filter: hue-rotate(180deg);
|
||||
}
|
||||
|
||||
.trim-handle img {
|
||||
position: absolute;
|
||||
width: $trim-handle-width;
|
||||
height: $trim-handle-height;
|
||||
|
||||
/* Make sure image dragging isn't triggered */
|
||||
user-select: none;
|
||||
user-drag: none;
|
||||
-webkit-user-drag: none; /* Autoprefixer doesn't seem to work for this */
|
||||
|
||||
transition: 0.2s;
|
||||
}
|
||||
|
||||
.top-trim-handle {
|
||||
top: -$trim-handle-height;
|
||||
}
|
||||
|
||||
.bottom-trim-handle {
|
||||
bottom: -$trim-handle-height;
|
||||
}
|
||||
|
||||
.top-trim-handle img {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* Increase handle size when anywhere on draggable area is hovered */
|
||||
.trim-background:hover img {
|
||||
transform: scale($hover-scale);
|
||||
}
|
||||
|
||||
.trim-background:hover .top-trim-handle img {
|
||||
transform: rotate(180deg) scale($hover-scale);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import Box from '../box/box.jsx';
|
||||
import styles from './audio-selector.css';
|
||||
import handleIcon from './icon--handle.svg';
|
||||
|
||||
const AudioSelector = props => (
|
||||
<div
|
||||
className={styles.absolute}
|
||||
ref={props.containerRef}
|
||||
onMouseDown={props.onNewSelectionMouseDown}
|
||||
onTouchStart={props.onNewSelectionMouseDown}
|
||||
>
|
||||
{props.trimStart === null ? null : (
|
||||
<Box
|
||||
className={classNames(styles.absolute, styles.trimBackground, styles.startTrimBackground)}
|
||||
style={{
|
||||
left: `${props.trimStart * 100}%`,
|
||||
width: `${100 * (props.trimEnd - props.trimStart)}%`
|
||||
}}
|
||||
>
|
||||
<Box className={classNames(styles.absolute, styles.trimBackgroundMask)} />
|
||||
<Box
|
||||
className={classNames(styles.trimLine, styles.startTrimLine)}
|
||||
onMouseDown={props.onTrimStartMouseDown}
|
||||
onTouchStart={props.onTrimStartMouseDown}
|
||||
>
|
||||
<Box className={classNames(styles.trimHandle, styles.topTrimHandle, styles.startTrimHandle)}>
|
||||
<img src={handleIcon} />
|
||||
</Box>
|
||||
<Box className={classNames(styles.trimHandle, styles.bottomTrimHandle, styles.startTrimHandle)}>
|
||||
<img src={handleIcon} />
|
||||
</Box>
|
||||
</Box>
|
||||
<Box
|
||||
className={classNames(styles.trimLine, styles.endTrimLine)}
|
||||
onMouseDown={props.onTrimEndMouseDown}
|
||||
onTouchStart={props.onTrimEndMouseDown}
|
||||
>
|
||||
<Box className={classNames(styles.trimHandle, styles.topTrimHandle, styles.endTrimHandle)}>
|
||||
<img src={handleIcon} />
|
||||
</Box>
|
||||
<Box className={classNames(styles.trimHandle, styles.bottomTrimHandle, styles.endTrimHandle)}>
|
||||
<img src={handleIcon} />
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
{props.playhead ? (
|
||||
<div className={styles.playheadContainer}>
|
||||
<div
|
||||
className={classNames(styles.trimLine, styles.playhead)}
|
||||
style={{
|
||||
transform: `translateX(${100 * props.playhead}%)`
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
|
||||
AudioSelector.propTypes = {
|
||||
containerRef: PropTypes.func,
|
||||
onNewSelectionMouseDown: PropTypes.func.isRequired,
|
||||
onTrimEndMouseDown: PropTypes.func.isRequired,
|
||||
onTrimStartMouseDown: PropTypes.func.isRequired,
|
||||
playhead: PropTypes.number,
|
||||
trimEnd: PropTypes.number,
|
||||
trimStart: PropTypes.number
|
||||
};
|
||||
|
||||
export default AudioSelector;
|
||||
@@ -8,15 +8,13 @@ import Label from '../forms/label.jsx';
|
||||
import Input from '../forms/input.jsx';
|
||||
|
||||
import BufferedInputHOC from '../forms/buffered-input-hoc.jsx';
|
||||
import AudioTrimmer from '../../containers/audio-trimmer.jsx';
|
||||
import AudioSelector from '../../containers/audio-selector.jsx';
|
||||
import IconButton from '../icon-button/icon-button.jsx';
|
||||
|
||||
import styles from './sound-editor.css';
|
||||
|
||||
import playIcon from '../record-modal/icon--play.svg';
|
||||
import stopIcon from '../record-modal/icon--stop-playback.svg';
|
||||
import trimIcon from './icon--trim.svg';
|
||||
import trimConfirmIcon from './icon--trim-confirm.svg';
|
||||
import redoIcon from './icon--redo.svg';
|
||||
import undoIcon from './icon--undo.svg';
|
||||
import echoIcon from './icon--echo.svg';
|
||||
@@ -103,7 +101,11 @@ const messages = defineMessages({
|
||||
});
|
||||
|
||||
const SoundEditor = props => (
|
||||
<div className={styles.editorContainer}>
|
||||
<div
|
||||
className={styles.editorContainer}
|
||||
ref={props.setRef}
|
||||
onMouseDown={props.onContainerClick}
|
||||
>
|
||||
<div className={styles.row}>
|
||||
<div className={styles.inputGroup}>
|
||||
<Label text={props.intl.formatMessage(messages.sound)}>
|
||||
@@ -161,12 +163,13 @@ const SoundEditor = props => (
|
||||
height={160}
|
||||
width={600}
|
||||
/>
|
||||
<AudioTrimmer
|
||||
<AudioSelector
|
||||
playhead={props.playhead}
|
||||
trimEnd={props.trimEnd}
|
||||
trimStart={props.trimStart}
|
||||
onSetTrimEnd={props.onSetTrimEnd}
|
||||
onSetTrimStart={props.onSetTrimStart}
|
||||
onPlay={props.onPlay}
|
||||
onSetTrim={props.onSetTrim}
|
||||
onStop={props.onStop}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -248,8 +251,8 @@ SoundEditor.propTypes = {
|
||||
chunkLevels: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
intl: intlShape,
|
||||
name: PropTypes.string.isRequired,
|
||||
onActivateTrim: PropTypes.func,
|
||||
onChangeName: PropTypes.func.isRequired,
|
||||
onContainerClick: PropTypes.func.isRequired,
|
||||
onEcho: PropTypes.func.isRequired,
|
||||
onFaster: PropTypes.func.isRequired,
|
||||
onLouder: PropTypes.func.isRequired,
|
||||
@@ -257,13 +260,13 @@ SoundEditor.propTypes = {
|
||||
onRedo: PropTypes.func.isRequired,
|
||||
onReverse: PropTypes.func.isRequired,
|
||||
onRobot: PropTypes.func.isRequired,
|
||||
onSetTrimEnd: PropTypes.func,
|
||||
onSetTrimStart: PropTypes.func,
|
||||
onSetTrim: PropTypes.func,
|
||||
onSlower: PropTypes.func.isRequired,
|
||||
onSofter: PropTypes.func.isRequired,
|
||||
onStop: PropTypes.func.isRequired,
|
||||
onUndo: PropTypes.func.isRequired,
|
||||
playhead: PropTypes.number,
|
||||
setRef: PropTypes.func,
|
||||
trimEnd: PropTypes.number,
|
||||
trimStart: PropTypes.number
|
||||
};
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import bindAll from 'lodash.bindall';
|
||||
import AudioSelectorComponent from '../components/audio-trimmer/audio-selector.jsx';
|
||||
import {getEventXY} from '../lib/touch-utils';
|
||||
|
||||
const MIN_LENGTH = 0.01;
|
||||
const MIN_DURATION = 500;
|
||||
|
||||
class AudioSelector extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
bindAll(this, [
|
||||
'handleNewSelectionMouseDown',
|
||||
'handleTrimStartMouseDown',
|
||||
'handleTrimEndMouseDown',
|
||||
'handleTrimStartMouseMove',
|
||||
'handleTrimEndMouseMove',
|
||||
'handleTrimStartMouseUp',
|
||||
'handleTrimEndMouseUp',
|
||||
'storeRef'
|
||||
]);
|
||||
|
||||
this.state = {
|
||||
trimStart: props.trimStart,
|
||||
trimEnd: props.trimEnd
|
||||
};
|
||||
|
||||
this.clickStartTime = 0;
|
||||
}
|
||||
componentWillReceiveProps (newProps) {
|
||||
if (newProps.trimStart === this.props.trimStart) return;
|
||||
this.setState({
|
||||
trimStart: newProps.trimStart,
|
||||
trimEnd: newProps.trimEnd
|
||||
});
|
||||
}
|
||||
clearSelection () {
|
||||
this.props.onSetTrim(null, null);
|
||||
}
|
||||
handleNewSelectionMouseDown (e) {
|
||||
this.initialX = getEventXY(e).x;
|
||||
const {width, left} = this.containerElement.getBoundingClientRect();
|
||||
this.initialTrimEnd = (this.initialX - left) / width;
|
||||
this.initialTrimStart = this.initialTrimEnd;
|
||||
this.props.onSetTrim(this.initialTrimStart, this.initialTrimEnd);
|
||||
|
||||
this.clickStartTime = Date.now();
|
||||
|
||||
window.addEventListener('mousemove', this.handleTrimEndMouseMove);
|
||||
window.addEventListener('mouseup', this.handleTrimEndMouseUp);
|
||||
window.addEventListener('touchmove', this.handleTrimEndMouseMove);
|
||||
window.addEventListener('touchend', this.handleTrimEndMouseUp);
|
||||
}
|
||||
handleTrimStartMouseMove (e) {
|
||||
const containerSize = this.containerElement.getBoundingClientRect().width;
|
||||
const dx = (getEventXY(e).x - this.initialX) / containerSize;
|
||||
const newTrim = Math.max(0, Math.min(1, this.initialTrimStart + dx));
|
||||
if (newTrim > this.initialTrimEnd) {
|
||||
this.setState({
|
||||
trimStart: this.initialTrimEnd,
|
||||
trimEnd: newTrim
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
trimStart: newTrim
|
||||
});
|
||||
}
|
||||
e.preventDefault();
|
||||
}
|
||||
handleTrimEndMouseMove (e) {
|
||||
const containerSize = this.containerElement.getBoundingClientRect().width;
|
||||
const dx = (getEventXY(e).x - this.initialX) / containerSize;
|
||||
const newTrim = Math.min(1, Math.max(0, this.initialTrimEnd + dx));
|
||||
if (newTrim < this.initialTrimStart) {
|
||||
this.setState({
|
||||
trimStart: newTrim,
|
||||
trimEnd: this.initialTrimStart
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
trimEnd: newTrim
|
||||
});
|
||||
}
|
||||
e.preventDefault();
|
||||
}
|
||||
handleTrimStartMouseUp () {
|
||||
window.removeEventListener('mousemove', this.handleTrimStartMouseMove);
|
||||
window.removeEventListener('mouseup', this.handleTrimStartMouseUp);
|
||||
window.removeEventListener('touchmove', this.handleTrimStartMouseMove);
|
||||
window.removeEventListener('touchend', this.handleTrimStartMouseUp);
|
||||
this.props.onSetTrim(this.state.trimStart, this.state.trimEnd);
|
||||
}
|
||||
handleTrimEndMouseUp () {
|
||||
window.removeEventListener('mousemove', this.handleTrimEndMouseMove);
|
||||
window.removeEventListener('mouseup', this.handleTrimEndMouseUp);
|
||||
window.removeEventListener('touchmove', this.handleTrimEndMouseMove);
|
||||
window.removeEventListener('touchend', this.handleTrimEndMouseUp);
|
||||
// If the selection was made quickly (tooFast) and is small (tooShort),
|
||||
// deselect instead. This allows click-to-deselect even if you drag
|
||||
// a little bit by accident. It also allows very quickly making a
|
||||
// selection, as long as it is above a minimum length.
|
||||
const tooFast = (Date.now() - this.clickStartTime) < MIN_DURATION;
|
||||
const tooShort = (this.state.trimEnd - this.state.trimStart) < MIN_LENGTH;
|
||||
if (tooFast && tooShort) {
|
||||
this.clearSelection();
|
||||
} else {
|
||||
this.props.onSetTrim(this.state.trimStart, this.state.trimEnd);
|
||||
}
|
||||
}
|
||||
handleTrimStartMouseDown (e) {
|
||||
this.initialX = getEventXY(e).x;
|
||||
this.initialTrimStart = this.props.trimStart;
|
||||
this.initialTrimEnd = this.props.trimEnd;
|
||||
window.addEventListener('mousemove', this.handleTrimStartMouseMove);
|
||||
window.addEventListener('mouseup', this.handleTrimStartMouseUp);
|
||||
window.addEventListener('touchmove', this.handleTrimStartMouseMove);
|
||||
window.addEventListener('touchend', this.handleTrimStartMouseUp);
|
||||
e.stopPropagation();
|
||||
}
|
||||
handleTrimEndMouseDown (e) {
|
||||
this.initialX = getEventXY(e).x;
|
||||
this.initialTrimEnd = this.props.trimEnd;
|
||||
this.initialTrimStart = this.props.trimStart;
|
||||
window.addEventListener('mousemove', this.handleTrimEndMouseMove);
|
||||
window.addEventListener('mouseup', this.handleTrimEndMouseUp);
|
||||
window.addEventListener('touchmove', this.handleTrimEndMouseMove);
|
||||
window.addEventListener('touchend', this.handleTrimEndMouseUp);
|
||||
e.stopPropagation();
|
||||
}
|
||||
storeRef (el) {
|
||||
this.containerElement = el;
|
||||
}
|
||||
render () {
|
||||
return (
|
||||
<AudioSelectorComponent
|
||||
containerRef={this.storeRef}
|
||||
playhead={this.props.playhead}
|
||||
trimEnd={this.state.trimEnd}
|
||||
trimStart={this.state.trimStart}
|
||||
onNewSelectionMouseDown={this.handleNewSelectionMouseDown}
|
||||
onTrimEndMouseDown={this.handleTrimEndMouseDown}
|
||||
onTrimStartMouseDown={this.handleTrimStartMouseDown}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AudioSelector.propTypes = {
|
||||
onSetTrim: PropTypes.func,
|
||||
playhead: PropTypes.number,
|
||||
trimEnd: PropTypes.number,
|
||||
trimStart: PropTypes.number
|
||||
};
|
||||
|
||||
export default AudioSelector;
|
||||
@@ -23,13 +23,14 @@ class SoundEditor extends React.Component {
|
||||
'handlePlay',
|
||||
'handleStopPlaying',
|
||||
'handleUpdatePlayhead',
|
||||
'handleActivateTrim',
|
||||
'handleUpdateTrimEnd',
|
||||
'handleUpdateTrimStart',
|
||||
'handleDelete',
|
||||
'handleUpdateTrim',
|
||||
'handleEffect',
|
||||
'handleUndo',
|
||||
'handleRedo',
|
||||
'submitNewSamples'
|
||||
'handleContainerClick',
|
||||
'setRef'
|
||||
]);
|
||||
this.state = {
|
||||
chunkLevels: computeChunkedRMS(this.props.samples),
|
||||
@@ -40,6 +41,8 @@ class SoundEditor extends React.Component {
|
||||
|
||||
this.redoStack = [];
|
||||
this.undoStack = [];
|
||||
|
||||
this.ref = null;
|
||||
}
|
||||
componentDidMount () {
|
||||
this.audioBufferPlayer = new AudioBufferPlayer(this.props.samples, this.props.sampleRate);
|
||||
@@ -49,6 +52,10 @@ class SoundEditor extends React.Component {
|
||||
this.redoStack = [];
|
||||
this.undoStack = [];
|
||||
this.resetState(newProps.samples, newProps.sampleRate);
|
||||
this.setState({
|
||||
trimStart: null,
|
||||
trimEnd: null
|
||||
});
|
||||
}
|
||||
}
|
||||
componentWillUnmount () {
|
||||
@@ -59,9 +66,7 @@ class SoundEditor extends React.Component {
|
||||
this.audioBufferPlayer = new AudioBufferPlayer(samples, sampleRate);
|
||||
this.setState({
|
||||
chunkLevels: computeChunkedRMS(samples),
|
||||
playhead: null,
|
||||
trimStart: null,
|
||||
trimEnd: null
|
||||
playhead: null
|
||||
});
|
||||
}
|
||||
submitNewSamples (samples, sampleRate, skipUndo) {
|
||||
@@ -106,6 +111,7 @@ class SoundEditor extends React.Component {
|
||||
return false; // Update failed
|
||||
}
|
||||
handlePlay () {
|
||||
this.audioBufferPlayer.stop();
|
||||
this.audioBufferPlayer.play(
|
||||
this.state.trimStart || 0,
|
||||
this.state.trimEnd || 1,
|
||||
@@ -125,31 +131,36 @@ class SoundEditor extends React.Component {
|
||||
handleChangeName (name) {
|
||||
this.props.vm.renameSound(this.props.soundIndex, name);
|
||||
}
|
||||
handleActivateTrim () {
|
||||
if (this.state.trimStart === null && this.state.trimEnd === null) {
|
||||
this.setState({trimEnd: 0.95, trimStart: 0.05});
|
||||
handleDelete () {
|
||||
const {samples, sampleRate} = this.copyCurrentBuffer();
|
||||
const sampleCount = samples.length;
|
||||
const startIndex = Math.floor(this.state.trimStart * sampleCount);
|
||||
const endIndex = Math.floor(this.state.trimEnd * sampleCount);
|
||||
const firstPart = samples.slice(0, startIndex);
|
||||
const secondPart = samples.slice(endIndex, sampleCount);
|
||||
const newLength = firstPart.length + secondPart.length;
|
||||
let newSamples;
|
||||
if (newLength === 0) {
|
||||
newSamples = new Float32Array(1);
|
||||
} else {
|
||||
const {samples, sampleRate} = this.copyCurrentBuffer();
|
||||
const sampleCount = samples.length;
|
||||
const startIndex = Math.floor(this.state.trimStart * sampleCount);
|
||||
const endIndex = Math.floor(this.state.trimEnd * sampleCount);
|
||||
if (endIndex > startIndex) { // Strictly greater to prevent 0 sample sounds
|
||||
const clippedSamples = samples.slice(startIndex, endIndex);
|
||||
this.submitNewSamples(clippedSamples, sampleRate);
|
||||
} else {
|
||||
// Just clear the trim state, it cannot be completed
|
||||
this.setState({
|
||||
trimStart: null,
|
||||
trimEnd: null
|
||||
});
|
||||
}
|
||||
newSamples = new Float32Array(newLength);
|
||||
newSamples.set(firstPart, 0);
|
||||
newSamples.set(secondPart, firstPart.length);
|
||||
}
|
||||
this.submitNewSamples(newSamples, sampleRate);
|
||||
this.setState({
|
||||
trimStart: null,
|
||||
trimEnd: null
|
||||
});
|
||||
}
|
||||
handleUpdateTrimEnd (trimEnd) {
|
||||
this.setState({trimEnd});
|
||||
}
|
||||
handleUpdateTrimStart (trimStart) {
|
||||
this.setState({trimStart});
|
||||
handleUpdateTrim (trimStart, trimEnd) {
|
||||
this.setState({trimStart, trimEnd});
|
||||
this.handleStopPlaying();
|
||||
}
|
||||
effectFactory (name) {
|
||||
return () => this.handleEffect(name);
|
||||
@@ -184,6 +195,13 @@ class SoundEditor extends React.Component {
|
||||
this.undoStack.push(this.copyCurrentBuffer());
|
||||
this.submitNewSamples(samples, sampleRate, true);
|
||||
this.handlePlay();
|
||||
setRef (element) {
|
||||
this.ref = element;
|
||||
}
|
||||
handleContainerClick (e) {
|
||||
// If the click is on the sound editor's div (and not any other element), delesect
|
||||
if (e.target === this.ref) {
|
||||
this.handleUpdateTrim(null, null);
|
||||
}
|
||||
}
|
||||
render () {
|
||||
@@ -195,10 +213,12 @@ class SoundEditor extends React.Component {
|
||||
chunkLevels={this.state.chunkLevels}
|
||||
name={this.props.name}
|
||||
playhead={this.state.playhead}
|
||||
setRef={this.setRef}
|
||||
trimEnd={this.state.trimEnd}
|
||||
trimStart={this.state.trimStart}
|
||||
onActivateTrim={this.handleActivateTrim}
|
||||
onChangeName={this.handleChangeName}
|
||||
onContainerClick={this.handleContainerClick}
|
||||
onDelete={this.handleDelete}
|
||||
onEcho={this.effectFactory(effectTypes.ECHO)}
|
||||
onFaster={this.effectFactory(effectTypes.FASTER)}
|
||||
onLouder={this.effectFactory(effectTypes.LOUDER)}
|
||||
@@ -206,8 +226,7 @@ class SoundEditor extends React.Component {
|
||||
onRedo={this.handleRedo}
|
||||
onReverse={this.effectFactory(effectTypes.REVERSE)}
|
||||
onRobot={this.effectFactory(effectTypes.ROBOT)}
|
||||
onSetTrimEnd={this.handleUpdateTrimEnd}
|
||||
onSetTrimStart={this.handleUpdateTrimStart}
|
||||
onSetTrim={this.handleUpdateTrim}
|
||||
onSlower={this.effectFactory(effectTypes.SLOWER)}
|
||||
onSofter={this.effectFactory(effectTypes.SOFTER)}
|
||||
onStop={this.handleStopPlaying}
|
||||
|
||||
Reference in new issue
Block a user