diff --git a/package.json b/package.json index 9aa0fb197..44993ad3f 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "cookie-parser": "^1.3.5", "copy-to-clipboard": "^1.0.4", "cors": "^2.7.1", + "crypto-js": "^3.1.6", "css-loader": "^0.23.1", "debug": "^2.2.0", "dotenv": "^1.2.0", @@ -97,7 +98,6 @@ "serialize-javascript": "^1.0.0", "serve-favicon": "^2.2.1", "sitemap": "^1.5.0", - "sjcl": "~1.0.3", "strip-loader": "^0.1.2", "style-loader": "^0.13.1", "superagent": "^1.2.0", diff --git a/src/components/Audioplayer/RepeatButton/index.js b/src/components/Audioplayer/RepeatButton/index.js new file mode 100644 index 000000000..423e0a0c5 --- /dev/null +++ b/src/components/Audioplayer/RepeatButton/index.js @@ -0,0 +1,39 @@ +import React, { PropTypes } from 'react'; +import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger'; +import Tooltip from 'react-bootstrap/lib/Tooltip'; + +const style = require('../style.scss'); + +const RepeatButton = ({ shouldRepeat, onRepeatToggle }) => { + const tooltip = ( + + Repeats the current ayah on end... + + ); + + return ( +
+ + + + +
+ ); +}; + +RepeatButton.propTypes = { + shouldRepeat: PropTypes.bool.isRequired, + onRepeatToggle: PropTypes.func.isRequired +}; + +export default RepeatButton; diff --git a/src/components/Audioplayer/RepeatButton/spec.js b/src/components/Audioplayer/RepeatButton/spec.js new file mode 100644 index 000000000..a31ba8f56 --- /dev/null +++ b/src/components/Audioplayer/RepeatButton/spec.js @@ -0,0 +1,36 @@ +import React from 'react'; +import { mount } from 'enzyme'; + +import RepeatButton from './index'; + +let makeComponent, component, onRepeatToggle; + +describe('', () => { + beforeEach(() => { + makeComponent = (shouldRepeat) => { + onRepeatToggle = sinon.stub(); + + component = mount( + + ); + } + }); + + it('should indicate that shouldRepeat', () => { + makeComponent(true); + + expect(component.find('label').first().props().className).to.contain('repeat'); + }); + + it('should not indicate that shouldRepeat', () => { + makeComponent(false); + + expect(component.find('label').first().props().className).not.to.contain('repeat'); + }); + + it('should call onRepeatToggle when clicked', () => { + component.find('label').first().simulate('click'); + + expect(onRepeatToggle).to.have.been.called; + }); +}); diff --git a/src/components/Audioplayer/ScrollButton/index.js b/src/components/Audioplayer/ScrollButton/index.js new file mode 100644 index 000000000..1b4ccf98a --- /dev/null +++ b/src/components/Audioplayer/ScrollButton/index.js @@ -0,0 +1,39 @@ +import React, { PropTypes } from 'react'; +import OverlayTrigger from 'react-bootstrap/lib/OverlayTrigger'; +import Tooltip from 'react-bootstrap/lib/Tooltip'; + +const style = require('../style.scss'); + +const ScrollButton = ({ shouldScroll, onScrollToggle }) => { + const tooltip = ( + + Automatically scrolls to the currently playing ayah on transitions... + + ); + + return ( +
+ + + + +
+ ); +}; + +ScrollButton.propTypes = { + shouldScroll: PropTypes.bool.isRequired, + onScrollToggle: PropTypes.func.isRequired +}; + +export default ScrollButton; diff --git a/src/components/Audioplayer/ScrollButton/spec.js b/src/components/Audioplayer/ScrollButton/spec.js new file mode 100644 index 000000000..fac524805 --- /dev/null +++ b/src/components/Audioplayer/ScrollButton/spec.js @@ -0,0 +1,36 @@ +import React from 'react'; +import { mount } from 'enzyme'; + +import ScrollButton from './index'; + +let makeComponent, component, onScrollToggle; + +describe('', () => { + beforeEach(() => { + makeComponent = (shouldScroll) => { + onScrollToggle = sinon.stub(); + + component = mount( + + ); + } + }); + + it('should indicate that shouldScroll', () => { + makeComponent(true); + + expect(component.find('label').first().props().className).to.contain('scroll'); + }); + + it('should not indicate that shouldScroll', () => { + makeComponent(false); + + expect(component.find('label').first().props().className).not.to.contain('scroll'); + }); + + it('should call onScrollToggle when clicked', () => { + component.find('label').first().simulate('click'); + + expect(onScrollToggle).to.have.been.called; + }); +}); diff --git a/src/components/Audioplayer/Segments/index.js b/src/components/Audioplayer/Segments/index.js index c8035046e..38ca53d6f 100644 --- a/src/components/Audioplayer/Segments/index.js +++ b/src/components/Audioplayer/Segments/index.js @@ -1,219 +1,56 @@ import React, { Component, PropTypes } from 'react'; -import { decrypt } from 'sjcl'; +import Helmet from 'react-helmet'; + +import debug from 'helpers/debug'; export default class Segments extends Component { static propTypes = { - audio: PropTypes.object.isRequired, - segments: PropTypes.string.isRequired, + audio: PropTypes.object, + segments: PropTypes.object.isRequired, currentAyah: PropTypes.string, - currentWord: PropTypes.string, - onSetCurrentWord: PropTypes.func.isRequired - }; - - static defaultProps = { - currentWord: null - }; - - constructor() { - super(...arguments); // eslint-disable-line prefer-rest-params - this.secret = process.env.SEGMENTS_KEY; - this.currentWord = null; - } - - state = { - intervals: [], + currentTime: PropTypes.number }; - // LIFECYCLE METHODS - - componentDidMount() { - this.bindListeners(); - } - - componentWillReceiveProps(nextProps) { - const prevProps = this.props; - - if (prevProps.audio !== nextProps.audio) { - this.unbindListeners(prevProps); - - this.buildIntervals(nextProps); - this.bindListeners(nextProps); - } - } - - shouldComponentUpdate(nextProps, nextState) { - const prevProps = this.props; - const prevState = this.state; - + shouldComponentUpdate(nextProps) { return [ - prevProps.audio !== nextProps.audio, - prevProps.segments !== nextProps.segments, - prevState.intervals !== nextState.intervals, - prevProps.currentWord !== nextProps.currentWord, - nextProps.currentWord !== this.currentWord - ].some(b => b); - // TODO: I think we can just 'return false' here since there is nothing to actually render... - // oh wait, maybe i need it so that componentDidUpdate will run..., despite render() not - // actually being needed... dunno right now - } - - componentDidUpdate() { - if (this.currentWord !== this.props.currentWord) { // currentWord was changed by the user - if (this.props.currentWord !== null) { - const wordInterval = this.state.words[this.props.currentWord.split(/:/).pop()]; - // seek to the currentWord starting time and return - const timeToSeek = wordInterval.startTime + 0.001; - const isSeekable = this.props.audio.seekable && this.props.audio.seekable.length > 0; - const withinRange = !isSeekable ? - null : - (timeToSeek >= this.props.audio.seekable.start(0) && - timeToSeek <= this.props.audio.seekable.end(0)); - - if (isSeekable && withinRange) { // seek to it - this.props.audio.currentTime = timeToSeek; - } else { // seek to it after its ready - const seekToTime = () => { - this.props.audio.currentTime = timeToSeek; - this.props.audio.removeEventListener('canplay', seekToTime, false); - }; - this.props.audio.addEventListener('canplay', seekToTime); - } - } - - // but don't forget to set the change internally for next time - return this.setCurrentWord(this.props.currentWord, 'componentDidUpdate'); - } - - return false; - } - - componentWillUnmount() { - this.unbindListeners(); + this.props.audio !== nextProps.audio, + this.props.currentAyah !== nextProps.currentAyah, + this.props.currentTime !== nextProps.currentTime, + ].some(test => test); } - setCurrentWord(currentWord = null) { - // this is more immediately available but should eventually agree with props - this.currentWord = currentWord; - // calls the redux dispatch function passed down from the Audioplayer - this.props.onSetCurrentWord(currentWord); - } + render() { + const { segments, currentAyah, currentTime } = this.props; + const style = []; + let currentWord = null; - buildIntervals(props = this.props) { - let segments = null; - try { - segments = JSON.parse(decrypt(this.secret, new Buffer(props.segments, 'base64').toString())); - } catch (e) { - segments = []; - } + if (!Object.keys(segments).length) return