diff options
Diffstat (limited to 'animism-align/frontend/app/views/editor/sidebar')
5 files changed, 377 insertions, 0 deletions
diff --git a/animism-align/frontend/app/views/editor/sidebar/components/script.component.js b/animism-align/frontend/app/views/editor/sidebar/components/script.component.js new file mode 100644 index 0000000..d23b3da --- /dev/null +++ b/animism-align/frontend/app/views/editor/sidebar/components/script.component.js @@ -0,0 +1,22 @@ +import React, { Component } from 'react' +// import { Link } from 'react-router-dom' +import { connect } from 'react-redux' + +import actions from 'app/actions' + +const Script = ({ text }) => { + if (text.loading) return null + return ( + <textarea + className='script' + onChange={e => actions.site.updateText(e.target.value)} + value={text} + /> + ) +} + +const mapStateToProps = state => ({ + text: state.align.text, +}) + +export default connect(mapStateToProps)(Script) diff --git a/animism-align/frontend/app/views/editor/sidebar/components/tableOfContents.component.js b/animism-align/frontend/app/views/editor/sidebar/components/tableOfContents.component.js new file mode 100644 index 0000000..239dc69 --- /dev/null +++ b/animism-align/frontend/app/views/editor/sidebar/components/tableOfContents.component.js @@ -0,0 +1,30 @@ +import React, { Component } from 'react' +// import { Link } from 'react-router-dom' +import { connect } from 'react-redux' + +import { ROMAN_NUMERALS } from 'app/constants' +import actions from 'app/actions' + +class TableOfContents extends Component { + render() { + const { loading, order, lookup } = this.props.annotation + if (loading || !order) return null + const sectionIds = order.filter(id => lookup[id].type === "section_heading") + return ( + <div className="sidebar-content toc"> + {!sectionIds.length && <div className="no-sections">No sections found</div>} + {sectionIds.map((id, i) => ( + <div key={id} onClick={() => actions.align.setScrollPosition(lookup[id].start_ts)}> + {ROMAN_NUMERALS[i]}{'. '}{lookup[id].text} + </div> + ))} + </div> + ) + } +} + +const mapStateToProps = state => ({ + annotation: state.annotation.index, +}) + +export default connect(mapStateToProps)(TableOfContents) diff --git a/animism-align/frontend/app/views/editor/sidebar/components/waveUpload.component.js b/animism-align/frontend/app/views/editor/sidebar/components/waveUpload.component.js new file mode 100644 index 0000000..3ca19f0 --- /dev/null +++ b/animism-align/frontend/app/views/editor/sidebar/components/waveUpload.component.js @@ -0,0 +1,191 @@ +import React, { Component } from 'react' +import { connect } from 'react-redux' +import extractPeaks from 'webaudio-peaks' + +import actions from 'app/actions' +import { formatSize, timestampHMS } from 'app/utils' +import { Loader } from 'app/common' + +class WaveUpload extends Component { + state = { + working: false, + status: "", + filename: "", + duration: 0, + } + + upload(e) { + e.preventDefault() + document.body.className = '' + const files = e.dataTransfer ? e.dataTransfer.files : e.target.files + let i + let file + for (i = 0; i < files.length; i++) { + file = files[i] + if (file && file.type.match('image.*')) break + } + if (!file) { + console.log('No file specified') + return + } + this.setState({ working: true, status: "Loading MP3...", filename: file.name, size: file.size, duration: 0 }) + const fileReader = new FileReader() + fileReader.onload = event => { + fileReader.onload = null + this.processAudioFile(file, event.target.result) + } + fileReader.readAsArrayBuffer(file) + } + + processAudioFile(audioFile, arrayBuffer) { + this.setState({ working: true, status: "Extracting peaks..." }) + var audioContext = new (window.AudioContext || window.webkitAudioContext)(); + audioContext.decodeAudioData(arrayBuffer, (audioBuffer) => { + // buffer, samplesPerPixel, isMono, startOffset, endOffset, bitResolution + this.setState({ duration: audioBuffer.duration }) + var peaks = extractPeaks(audioBuffer, 441, true); + console.log(peaks) + const array = Array.from(peaks.data[0]) + const peaksBlob = new Blob([ JSON.stringify(array) ], {type: "application/json"}); + this.uploadAudioAndPeaks(audioFile, peaksBlob) + }) + } + + uploadAudioAndPeaks(audioFile, peaksBlob) { + const { episode } = this.props + const updatedEpisode = { ...episode } + this.setState({ status: "Removing old files..." }) + this.destroyTaggedFile('peaks') + this.destroyTaggedFile('audio') + .then(() => { + return ( + this.uploadTaggedFile( + peaksBlob, + 'peaks', + 'episode-' + this.props.episode.id + '-peaks.json', + {} + ) + ) + }) + .then(peaksResult => { + updatedEpisode.settings.peaks = peaksResult + return ( + this.uploadTaggedFile( + audioFile, + 'audio', + this.state.filename, + { + size: this.state.size, + duration: this.state.duration, + } + ) + ) + }) + .then(audioResult => { + updatedEpisode.settings.audio = audioResult + return actions.episode.update(updatedEpisode) + }) + .then(res => { + this.setState({ + status: "Upload complete", + working: false, + filename: null, + duration: null, + size: null, + }) + }) + } + + uploadTaggedFile(file, tag, fn, meta) { + return new Promise((resolve, reject) => { + this.setState({ status: "Uploading " + tag + "..." }) + const uploadData = { + tag, + file, + __file_filename: fn, + username: this.props.currentUser.username, + } + // console.log(uploadData) + return actions.upload.upload(uploadData).then(data => { + // console.log(data) + resolve({ + ...data.res, + ...meta + }) + }) + }) + } + + destroyTaggedFile(tag) { + return new Promise((resolve, reject) => { + if (!this.props.episode.settings[tag]) { + return resolve(); + } + actions.upload.destroy(this.props.episode.settings[tag]) + .then(() => { + console.log('Destroy successful') + resolve() + }) + .catch(() => { + console.log('Error deleting the image') + reject() + }) + }) + } + + render() { + const { episode, peaks } = this.props + // console.log(episode) + return ( + <div className="sidebar-content wave-upload"> + {episode.settings.audio && ( + <div> + <small>{episode.settings.audio.fn}</small> + <small>{'Size: '}{formatSize(episode.settings.audio.size)}</small> + <small>{'Duration: '}{timestampHMS(episode.settings.audio.duration)}</small> + </div> + )} + {peaks.length && ( + <div> + Peaks: {peaks.length} + </div> + )} + <div className="uploadButton"> + <button> + <span> + {episode.settings.audio + ? "Upload a new audio file" + : "Upload an audio file" + } + </span> + </button> + <input + type="file" + accept="audio/mp3" + onChange={this.upload.bind(this)} + required={this.props.required} + /> + </div> + <small>Upload an MP3, encoded 192kbit constant bitrate, 44.1kHz stereo</small> + {this.state.status && ( + <div className='status'> + {this.state.working && <Loader />} + <div className='status-message'>{this.state.status}</div> + {this.state.filename && <small>{this.state.filename}</small>} + {this.state.size && <small>{'Size: '}{formatSize(this.state.size)}</small>} + {!!this.state.duration && <small>{'Duration: '}{timestampHMS(this.state.duration)}</small>} + </div> + )} + </div> + ) + } +} + +const mapStateToProps = state => ({ + peaks: state.align.peaks, + currentUser: state.auth.user, + project: state.site.project, + episode: state.site.episode, +}) + +export default connect(mapStateToProps)(WaveUpload) diff --git a/animism-align/frontend/app/views/editor/sidebar/sidebar.container.js b/animism-align/frontend/app/views/editor/sidebar/sidebar.container.js new file mode 100644 index 0000000..350505b --- /dev/null +++ b/animism-align/frontend/app/views/editor/sidebar/sidebar.container.js @@ -0,0 +1,44 @@ +import React, { Component } from 'react' +import { connect } from 'react-redux' +// import { Link } from 'react-router-dom' + +import './sidebar.css' + +import actions from 'app/actions' + +import Script from './components/script.component.js' +import TableOfContents from './components/tableOfContents.component.js' +import WaveUpload from './components/waveUpload.component.js' + +class Sidebar extends Component { + state = { + mode: "toc", + } + componentDidMount(){ + if (!this.props.peaks.length) { + this.setState({ mode: "wav" }) + } + } + + render() { + const { mode } = this.state + return ( + <div className='sidebar'> + <div className='buttons'> + <button className={mode === "txt" ? "active" : ""} onClick={() => this.setState({ mode: "txt" })}>text</button> + <button className={mode === "wav" ? "active" : ""} onClick={() => this.setState({ mode: "wav" })}>wav</button> + <button className={mode === "toc" ? "active" : ""} onClick={() => this.setState({ mode: "toc" })}>contents</button> + </div> + {mode === 'toc' && <TableOfContents />} + {mode === 'txt' && <Script />} + {mode === 'wav' && <WaveUpload />} + </div> + ) + } +} + +const mapStateToProps = state => ({ + peaks: state.align.peaks, +}) + +export default connect(mapStateToProps)(Sidebar) diff --git a/animism-align/frontend/app/views/editor/sidebar/sidebar.css b/animism-align/frontend/app/views/editor/sidebar/sidebar.css new file mode 100644 index 0000000..e4d0f61 --- /dev/null +++ b/animism-align/frontend/app/views/editor/sidebar/sidebar.css @@ -0,0 +1,90 @@ +/* Sidebar */ + +.sidebar { + position: absolute; + top: 0; + right: 0; + width: 15rem; + z-index: 8; + padding-top: 2rem; +} + +/* sidebar header */ + +.sidebar .buttons { + position: absolute; + top: 0; + right: 0; + width: 15rem; + height: 2rem; + z-index: 9; + display: flex; + justify-content: flex-end; + align-items: flex-start; + background: #111; +} +.sidebar .buttons button { + border: 0; + background: transparent; + margin: 0; + padding-left: 0.75rem; + padding-right: 0.75rem; +} +.sidebar .buttons button:hover { + background: #333; +} +.sidebar .buttons button.active { + background: #222; +} +.sidebar-content { + background: #222; + width: 15rem; + padding: 0.5rem 0; +} + +/* wavefile upload */ + +.sidebar-content.wave-upload { + padding: 0.5rem 0.75rem 1.5rem 0.75rem; +} + +.wave-upload .uploadButton { + position: relative; + text-align: center; + margin: 1rem 0; +} +.wave-upload small { + display: block; + text-align: center; +} +.wave-upload .status { + padding: 1rem 0 0 0; + text-align: center; +} +.wave-upload .circular-loader { + margin: 0 auto 1rem auto; +} +.wave-upload .status-message { +} +.wave-upload .status small { + display: block; + margin-top: 0.25rem; +} + +/* table of contents */ + +.toc div { + width: 15rem; + padding: 0.25rem 0.75rem; + cursor: pointer; +} +.toc div:hover { + background: #213; +} + + +/* script */ + +.sidebar textarea { + height: calc(100vh - 5.15rem); +} |
