summaryrefslogtreecommitdiff
path: root/animism-align/frontend/app/views/editor/sidebar
diff options
context:
space:
mode:
Diffstat (limited to 'animism-align/frontend/app/views/editor/sidebar')
-rw-r--r--animism-align/frontend/app/views/editor/sidebar/components/script.component.js22
-rw-r--r--animism-align/frontend/app/views/editor/sidebar/components/tableOfContents.component.js30
-rw-r--r--animism-align/frontend/app/views/editor/sidebar/components/waveUpload.component.js191
-rw-r--r--animism-align/frontend/app/views/editor/sidebar/sidebar.container.js44
-rw-r--r--animism-align/frontend/app/views/editor/sidebar/sidebar.css90
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);
+}