Reorganising component folder structure
This commit is contained in:
153
src/js/components/Fields/AddSeedField.js
Executable file
153
src/js/components/Fields/AddSeedField.js
Executable file
@ -0,0 +1,153 @@
|
||||
|
||||
import React, { PropTypes } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { bindActionCreators } from 'redux'
|
||||
import { Link, hashHistory } from 'react-router'
|
||||
|
||||
import ArtistSentence from './../ArtistSentence'
|
||||
import * as helpers from '../../helpers'
|
||||
import * as coreActions from '../../services/core/actions'
|
||||
import * as uiActions from '../../services/ui/actions'
|
||||
import * as spotifyActions from '../../services/spotify/actions'
|
||||
|
||||
class AddSeedField extends React.Component{
|
||||
|
||||
constructor(props){
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
value: ''
|
||||
}
|
||||
this.id = helpers.generateGuid()
|
||||
this.timer = null
|
||||
this.handleClick = this.handleClick.bind(this)
|
||||
}
|
||||
|
||||
componentDidMount(){
|
||||
window.addEventListener("click", this.handleClick, false)
|
||||
|
||||
if (!this.props.genres){
|
||||
this.props.spotifyActions.getGenres()
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount(){
|
||||
window.removeEventListener("click", this.handleClick, false)
|
||||
}
|
||||
|
||||
handleClick(e){
|
||||
if ($(e.target).closest('.add-seed-field').length <= 0){
|
||||
this.props.spotifyActions.clearAutocompleteResults(this.id)
|
||||
}
|
||||
}
|
||||
|
||||
handleChange(e,value){
|
||||
var self = this
|
||||
|
||||
// update our local state
|
||||
this.setState({value: value})
|
||||
|
||||
// start a timer to perform the actual search
|
||||
// this provides a wee delay between key presses to avoid request spamming
|
||||
clearTimeout(this.timer)
|
||||
this.timer = setTimeout(
|
||||
function(){
|
||||
self.setState({searching: true})
|
||||
self.props.spotifyActions.getAutocompleteResults(self.id,value,['artist','track','genre'])
|
||||
},
|
||||
500
|
||||
)
|
||||
}
|
||||
|
||||
handleSelect(e,item){
|
||||
this.setState({value: ''})
|
||||
this.props.onSelect(e,item.uri)
|
||||
this.props.spotifyActions.clearAutocompleteResults(this.id)
|
||||
|
||||
// Add our selected item to our global index
|
||||
switch (helpers.uriType(item.uri)){
|
||||
|
||||
case 'artist':
|
||||
this.props.coreActions.artistsLoaded(item);
|
||||
break;
|
||||
|
||||
case 'track':
|
||||
this.props.coreActions.tracksLoaded(item);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
results(){
|
||||
if (this.props.results === undefined){
|
||||
return null;
|
||||
} else if (this.props.results[this.id] === undefined){
|
||||
return null;
|
||||
} else {
|
||||
return this.props.results[this.id];
|
||||
}
|
||||
}
|
||||
|
||||
renderResults(type){
|
||||
var results = this.results()
|
||||
if (!results || typeof(results[type]) === 'undefined' || results[type].length <= 0) return null
|
||||
|
||||
// only show the first 3
|
||||
var items = results[type].slice(0,3)
|
||||
|
||||
return (
|
||||
<div className="type">
|
||||
<h4 className="grey-text">{type}</h4>
|
||||
{
|
||||
items.map(item => {
|
||||
return (
|
||||
<div className="result" key={item.uri} onClick={e => this.handleSelect(e,item)}>
|
||||
{item.name}
|
||||
{type == 'tracks' ? <span className="grey-text"> <ArtistSentence artists={item.artists} nolinks /></span> : null}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render(){
|
||||
var className = "field autocomplete-field add-seed-field"
|
||||
if (this.results() && this.results().loading){
|
||||
className += " loading"
|
||||
}
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className="input">
|
||||
<input
|
||||
type="text"
|
||||
value={this.state.value}
|
||||
onChange={e => this.handleChange(e,e.target.value)}
|
||||
placeholder={this.props.placeholder ? this.props.placeholder : "Start typing..."} />
|
||||
</div>
|
||||
<div className="results">
|
||||
{this.renderResults('artists')}
|
||||
{this.renderResults('tracks')}
|
||||
{this.renderResults('genres')}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
return {
|
||||
genres: (state.spotify.genres ? state.spotify.genres : null),
|
||||
results: (state.spotify.autocomplete_results ? state.spotify.autocomplete_results : {})
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
coreActions: bindActionCreators(coreActions, dispatch),
|
||||
uiActions: bindActionCreators(uiActions, dispatch),
|
||||
spotifyActions: bindActionCreators(spotifyActions, dispatch)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(AddSeedField)
|
||||
153
src/js/components/Fields/AddTunabilityField.js
Executable file
153
src/js/components/Fields/AddTunabilityField.js
Executable file
@ -0,0 +1,153 @@
|
||||
|
||||
import React, { PropTypes } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { bindActionCreators } from 'redux'
|
||||
import { Link, hashHistory } from 'react-router'
|
||||
|
||||
import ArtistSentence from './../ArtistSentence'
|
||||
import * as helpers from '../../helpers'
|
||||
import * as coreActions from '../../services/core/actions'
|
||||
import * as uiActions from '../../services/ui/actions'
|
||||
import * as spotifyActions from '../../services/spotify/actions'
|
||||
|
||||
class AddSeedField extends React.Component{
|
||||
|
||||
constructor(props){
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
value: ''
|
||||
}
|
||||
this.id = helpers.generateGuid()
|
||||
this.timer = null
|
||||
this.handleClick = this.handleClick.bind(this)
|
||||
}
|
||||
|
||||
componentDidMount(){
|
||||
window.addEventListener("click", this.handleClick, false)
|
||||
|
||||
if (!this.props.genres){
|
||||
this.props.spotifyActions.getGenres()
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount(){
|
||||
window.removeEventListener("click", this.handleClick, false)
|
||||
}
|
||||
|
||||
handleClick(e){
|
||||
if ($(e.target).closest('.add-seed-field').length <= 0){
|
||||
this.props.spotifyActions.clearAutocompleteResults(this.id)
|
||||
}
|
||||
}
|
||||
|
||||
handleChange(e,value){
|
||||
var self = this
|
||||
|
||||
// update our local state
|
||||
this.setState({value: value})
|
||||
|
||||
// start a timer to perform the actual search
|
||||
// this provides a wee delay between key presses to avoid request spamming
|
||||
clearTimeout(this.timer)
|
||||
this.timer = setTimeout(
|
||||
function(){
|
||||
self.setState({searching: true})
|
||||
self.props.spotifyActions.getAutocompleteResults(self.id,value,['artist','track','genre'])
|
||||
},
|
||||
500
|
||||
)
|
||||
}
|
||||
|
||||
handleSelect(e,item){
|
||||
this.setState({value: ''})
|
||||
this.props.onSelect(e,item.uri)
|
||||
this.props.spotifyActions.clearAutocompleteResults(this.id)
|
||||
|
||||
// Add our selected item to our global index
|
||||
switch (helpers.uriType(item.uri)){
|
||||
|
||||
case 'artist':
|
||||
this.props.coreActions.artistsLoaded(item);
|
||||
break;
|
||||
|
||||
case 'track':
|
||||
this.props.coreActions.tracksLoaded(item);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
results(){
|
||||
if (typeof(this.props.results) === 'undefined'){
|
||||
return null
|
||||
} else if (typeof(this.props.results[this.id]) === 'undefined'){
|
||||
return null
|
||||
} else {
|
||||
return this.props.results[this.id]
|
||||
}
|
||||
}
|
||||
|
||||
renderResults(type){
|
||||
var results = this.results()
|
||||
if (!results || typeof(results[type]) === 'undefined' || results[type].length <= 0) return null
|
||||
|
||||
// only show the first 3
|
||||
var items = results[type].slice(0,3)
|
||||
|
||||
return (
|
||||
<div className="type">
|
||||
<h4 className="grey-text">{type}</h4>
|
||||
{
|
||||
items.map(item => {
|
||||
return (
|
||||
<div className="result" key={item.uri} onClick={e => this.handleSelect(e,item)}>
|
||||
{item.name}
|
||||
{type == 'tracks' ? <span className="grey-text"> <ArtistSentence artists={item.artists} nolinks /></span> : null}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render(){
|
||||
var className = "field autocomplete-field add-seed-field"
|
||||
if (this.results() && this.results().loading){
|
||||
className += " loading"
|
||||
}
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className="input">
|
||||
<input
|
||||
type="text"
|
||||
value={this.state.value}
|
||||
onChange={e => this.handleChange(e,e.target.value)}
|
||||
placeholder={this.props.placeholder ? this.props.placeholder : "Start typing..."} />
|
||||
</div>
|
||||
<div className="results">
|
||||
{this.renderResults('artists')}
|
||||
{this.renderResults('tracks')}
|
||||
{this.renderResults('genres')}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
return {
|
||||
genres: (state.ui.genres ? state.ui.genres : null),
|
||||
results: (state.spotify.autocomplete_results ? state.spotify.autocomplete_results : {})
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
coreActions: bindActionCreators(coreActions, dispatch),
|
||||
uiActions: bindActionCreators(uiActions, dispatch),
|
||||
spotifyActions: bindActionCreators(spotifyActions, dispatch)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(AddSeedField)
|
||||
82
src/js/components/Fields/ConfirmationButton.js
Executable file
82
src/js/components/Fields/ConfirmationButton.js
Executable file
@ -0,0 +1,82 @@
|
||||
|
||||
import React, { PropTypes } from 'react'
|
||||
import FontAwesome from 'react-fontawesome'
|
||||
|
||||
export default class ConfirmationButton extends React.Component{
|
||||
|
||||
constructor(props){
|
||||
super(props);
|
||||
this.state = {
|
||||
timing_out: false,
|
||||
confirming: false
|
||||
}
|
||||
this.confirming = false;
|
||||
this.unconfirmTimer = false;
|
||||
}
|
||||
|
||||
componentWillUnmount(){
|
||||
clearTimeout(this.unconfirmTimer );
|
||||
}
|
||||
|
||||
handleClick(e){
|
||||
if (this.state.confirming){
|
||||
this.setState({ confirming: false });
|
||||
this.props.onConfirm();
|
||||
} else {
|
||||
this.setState({ confirming: true });
|
||||
}
|
||||
}
|
||||
|
||||
handleMouseEnter(e){
|
||||
this.setState({ timing_out: false });
|
||||
clearTimeout(this.unconfirmTimer );
|
||||
}
|
||||
|
||||
handleMouseLeave(e){
|
||||
if (this.state.confirming){
|
||||
this.setState({ timing_out: true });
|
||||
this.unconfirmTimer = setTimeout(
|
||||
function(){
|
||||
this.setState({ confirming: false });
|
||||
}.bind(this),
|
||||
2000
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
render(){
|
||||
|
||||
var className = 'button';
|
||||
var content = this.props.content;
|
||||
|
||||
if (this.state.confirming){
|
||||
className += ' confirming';
|
||||
content = this.props.confirmingContent;
|
||||
if (this.state.timing_out){
|
||||
className += ' timing-out';
|
||||
}
|
||||
}
|
||||
|
||||
if (this.props.working){
|
||||
className += ' working';
|
||||
|
||||
if (this.props.workingContent){
|
||||
content = this.props.workingContent;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.props.className){
|
||||
className += ' '+this.props.className;
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
className={className}
|
||||
onClick={ (e) => this.handleClick(e) }
|
||||
onMouseLeave={ (e) => this.handleMouseLeave(e) }
|
||||
onMouseEnter={ (e) => this.handleMouseEnter(e) }>
|
||||
{ content }
|
||||
</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
92
src/js/components/Fields/DropdownField.js
Executable file
92
src/js/components/Fields/DropdownField.js
Executable file
@ -0,0 +1,92 @@
|
||||
|
||||
import React, { PropTypes } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { bindActionCreators } from 'redux'
|
||||
import FontAwesome from 'react-fontawesome'
|
||||
|
||||
export default class DropdownField extends React.Component{
|
||||
|
||||
constructor(props){
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
expanded: false
|
||||
}
|
||||
this.handleClick = this.handleClick.bind(this)
|
||||
}
|
||||
|
||||
componentDidMount(){
|
||||
window.addEventListener("click", this.handleClick, false)
|
||||
}
|
||||
|
||||
componentWillUnmount(){
|
||||
window.removeEventListener("click", this.handleClick, false)
|
||||
}
|
||||
|
||||
handleClick(e){
|
||||
// TODO: remove dependency on jQuery and explore the performance of this functionality
|
||||
if ($(e.target).closest('.dropdown-field').data('key') != this.props.name.replace(' ','_').toLowerCase() && this.state.expanded){
|
||||
this.setState({ expanded: false })
|
||||
}
|
||||
}
|
||||
|
||||
handleChange(value){
|
||||
this.setState({ expanded: !this.state.expanded })
|
||||
return this.props.handleChange(value )
|
||||
}
|
||||
|
||||
handleToggle(){
|
||||
this.setState({ expanded: !this.state.expanded })
|
||||
}
|
||||
|
||||
render(){
|
||||
if (!this.props.options){
|
||||
return null;
|
||||
}
|
||||
|
||||
var className = 'dropdown-field';
|
||||
if (this.state.expanded){
|
||||
className += ' expanded';
|
||||
}
|
||||
if (this.props.no_status_icon){
|
||||
className += ' no-status-icon';
|
||||
}
|
||||
if (this.props.button){
|
||||
className += ' buttonify';
|
||||
}
|
||||
var current_value = this.props.options[0].value
|
||||
if (this.props.value){
|
||||
current_value = this.props.value;
|
||||
}
|
||||
|
||||
var icon = <FontAwesome name="check" />
|
||||
if (this.props.reverse !== undefined){
|
||||
if (this.props.reverse){
|
||||
icon = <FontAwesome className="reverse" name="caret-down" />
|
||||
} else {
|
||||
icon = <FontAwesome className="reverse" name="caret-up" />
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className} data-key={this.props.name.replace(' ','_').toLowerCase()}>
|
||||
<div className={"label"+(this.props.button ? " button "+this.props.button : "")} onClick={ () => this.handleToggle() }>
|
||||
{this.props.icon ? <span><FontAwesome name={this.props.icon} /> </span> : null}
|
||||
<span className="text">{ this.props.name }</span>
|
||||
</div>
|
||||
<div className="options">
|
||||
{
|
||||
this.props.options.map(option => {
|
||||
return (
|
||||
<div className="option" key={ option.value } onClick={ e => this.handleChange(option.value) }>
|
||||
{!this.props.no_status_icon && (option.value == current_value) ? icon : null}
|
||||
{option.label}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
49
src/js/components/Fields/Dropzone.js
Executable file
49
src/js/components/Fields/Dropzone.js
Executable file
@ -0,0 +1,49 @@
|
||||
|
||||
import React, { PropTypes } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { bindActionCreators } from 'redux'
|
||||
|
||||
import Icon from './../Icon'
|
||||
|
||||
export default class Dropzone extends React.Component{
|
||||
|
||||
constructor(props){
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
hover: false
|
||||
}
|
||||
|
||||
this.handleMouseOver = this.handleMouseOver.bind(this)
|
||||
this.handleMouseOut = this.handleMouseOut.bind(this)
|
||||
}
|
||||
|
||||
componentWillMount(){
|
||||
window.addEventListener("mouseover", this.handleMouseOver, false);
|
||||
window.addEventListener("mouseout", this.handleMouseOut, false);
|
||||
}
|
||||
|
||||
componentWillUnmount(){
|
||||
window.removeEventListener("mouseover", this.handleMouseOver, false);
|
||||
window.removeEventListener("mouseout", this.handleMouseOut, false);
|
||||
}
|
||||
|
||||
handleMouseOver(e){
|
||||
this.setState({hover: true})
|
||||
}
|
||||
|
||||
handleMouseOut(e){
|
||||
this.setState({hover: false})
|
||||
}
|
||||
|
||||
render(){
|
||||
if (!this.props.data ) return null
|
||||
|
||||
return (
|
||||
<div className={this.state.hover?"dropzone hover":"dropzone"} onMouseUp={ e => this.props.handleMouseUp(e) }>
|
||||
<Icon className="white" name={ this.props.data.icon } />
|
||||
<span className="title">{ this.props.data.title }</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
94
src/js/components/Fields/Dropzones.js
Executable file
94
src/js/components/Fields/Dropzones.js
Executable file
@ -0,0 +1,94 @@
|
||||
|
||||
import React, { PropTypes } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { bindActionCreators } from 'redux'
|
||||
|
||||
import Dropzone from './Dropzone'
|
||||
|
||||
import * as uiActions from '../../services/ui/actions'
|
||||
import * as mopidyActions from '../../services/mopidy/actions'
|
||||
import * as spotifyActions from '../../services/spotify/actions'
|
||||
|
||||
class Dropzones extends React.Component{
|
||||
|
||||
constructor(props){
|
||||
super(props);
|
||||
|
||||
this._zones = [
|
||||
{
|
||||
title: 'Add to queue',
|
||||
icon: 'play',
|
||||
action: 'enqueue'
|
||||
},
|
||||
{
|
||||
title: 'Play next',
|
||||
icon: 'play',
|
||||
action: 'enqueue_next'
|
||||
},
|
||||
{
|
||||
title: 'Add to playlist',
|
||||
icon: 'playlist',
|
||||
action: 'add_to_playlist',
|
||||
accepts: ['tltrack','track','album','playlist','artist']
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
handleMouseMove(e){
|
||||
if (!this.props.dragger || !this.props.dragger.active ) return null;
|
||||
this.props.uiActions.dragMove(e )
|
||||
}
|
||||
|
||||
handleMouseUp(e, index){
|
||||
var target = this._zones[index]
|
||||
var victims = this.props.dragger.victims
|
||||
var uris = []
|
||||
for(var i = 0; i < victims.length; i++){
|
||||
uris.push(victims[i].uri )
|
||||
}
|
||||
|
||||
switch(target.action){
|
||||
case 'enqueue':
|
||||
this.props.mopidyActions.enqueueURIs(uris, this.props.dragger.from_uri)
|
||||
break
|
||||
|
||||
case 'enqueue_next':
|
||||
this.props.mopidyActions.enqueueURIs(uris, this.props.dragger.from_uri, true)
|
||||
break
|
||||
|
||||
case 'add_to_playlist':
|
||||
this.props.uiActions.openModal('add_to_playlist',{tracks_uris: uris})
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
render(){
|
||||
if (!this.props.dragger || !this.props.dragger.active ) return null
|
||||
|
||||
return (
|
||||
<div className="dropzones">
|
||||
{
|
||||
this._zones.map((zone, index) => {
|
||||
return <Dropzone key={index} data={zone} handleMouseUp={ e => this.handleMouseUp(e, index) }/>
|
||||
})
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
return {
|
||||
dragger: state.ui.dragger
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
uiActions: bindActionCreators(uiActions, dispatch),
|
||||
spotifyActions: bindActionCreators(spotifyActions, dispatch),
|
||||
mopidyActions: bindActionCreators(mopidyActions, dispatch)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(Dropzones)
|
||||
74
src/js/components/Fields/FilterField.js
Executable file
74
src/js/components/Fields/FilterField.js
Executable file
@ -0,0 +1,74 @@
|
||||
|
||||
import React, { PropTypes } from 'react'
|
||||
import { Link } from 'react-router'
|
||||
import FontAwesome from 'react-fontawesome'
|
||||
|
||||
export default class FilterField extends React.Component{
|
||||
|
||||
constructor(props){
|
||||
super(props)
|
||||
this.state = {
|
||||
value: '',
|
||||
active: (this.props.slim_mode ? true : false)
|
||||
}
|
||||
|
||||
this.handleKeyUp = this.handleKeyUp.bind(this)
|
||||
}
|
||||
|
||||
componentWillMount(){
|
||||
window.addEventListener("keyup", this.handleKeyUp, false)
|
||||
}
|
||||
|
||||
componentWillUnmount(){
|
||||
window.removeEventListener("keyup", this.handleKeyUp, false)
|
||||
}
|
||||
|
||||
handleKeyUp(e){
|
||||
if (e.keyCode == 27 && !this.props.slim_mode){
|
||||
e.preventDefault()
|
||||
|
||||
this.setState({
|
||||
value: '',
|
||||
active: false
|
||||
})
|
||||
|
||||
this.handleChange('')
|
||||
}
|
||||
}
|
||||
|
||||
activate(){
|
||||
this.setState({active: true})
|
||||
}
|
||||
|
||||
handleChange(value){
|
||||
this.setState({
|
||||
value: value,
|
||||
active: (this.props.slim_mode ? true : (value != ''))
|
||||
})
|
||||
this.props.handleChange(value)
|
||||
}
|
||||
|
||||
handleBlur(){
|
||||
if (this.state.value == '' && !this.props.slim_mode){
|
||||
this.setState({active: false})
|
||||
}
|
||||
}
|
||||
|
||||
render(){
|
||||
return (
|
||||
<span className={"filter-field "+(this.state.active ? 'active' : '')} onClick={e => this.activate()}>
|
||||
<form>
|
||||
<FontAwesome name="search" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Filter"
|
||||
value={this.state.value}
|
||||
onFocus={e => this.activate()}
|
||||
onBlur={e => this.handleBlur()}
|
||||
onChange={e => this.handleChange(e.target.value)}
|
||||
/>
|
||||
</form>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
}
|
||||
67
src/js/components/Fields/FollowButton.js
Executable file
67
src/js/components/Fields/FollowButton.js
Executable file
@ -0,0 +1,67 @@
|
||||
|
||||
import React, { PropTypes } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { Link } from 'react-router'
|
||||
import { createStore, bindActionCreators } from 'redux'
|
||||
import FontAwesome from 'react-fontawesome'
|
||||
|
||||
import * as helpers from '../../helpers'
|
||||
import * as uiActions from '../../services/ui/actions'
|
||||
import * as spotifyActions from '../../services/spotify/actions'
|
||||
|
||||
class FollowButton extends React.Component{
|
||||
|
||||
constructor(props){
|
||||
super(props);
|
||||
}
|
||||
|
||||
remove(){
|
||||
this.props.spotifyActions.following(this.props.uri, 'DELETE');
|
||||
}
|
||||
|
||||
add(){
|
||||
this.props.spotifyActions.following(this.props.uri, 'PUT');
|
||||
}
|
||||
|
||||
render(){
|
||||
if (!this.props.uri){
|
||||
return false;
|
||||
}
|
||||
|
||||
var className = '';
|
||||
|
||||
// Inherit passed-down classes
|
||||
if (this.props.className){
|
||||
className += ' '+this.props.className;
|
||||
}
|
||||
|
||||
// Loader
|
||||
if (helpers.isLoading(this.props.load_queue,['/following','/followers','me/albums/contains/?ids=','me/albums/?ids='])){
|
||||
className += ' working';
|
||||
}
|
||||
|
||||
if (!this.props.spotify_authorized){
|
||||
return <button className={className+' disabled'} onClick={e => this.props.uiActions.createNotification({content: 'You must authorize Spotify first', type: 'warning'})}>{this.props.addText}</button>
|
||||
} else if (this.props.is_following === true){
|
||||
return <button className={className+' destructive'} onClick={e => this.remove()}>{this.props.removeText}</button>
|
||||
} else {
|
||||
return <button className={className} onClick={e => this.add()}>{this.props.addText}</button>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
return {
|
||||
load_queue: state.ui.load_queue,
|
||||
spotify_authorized: state.spotify.authorization
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
uiActions: bindActionCreators(uiActions, dispatch),
|
||||
spotifyActions: bindActionCreators(spotifyActions, dispatch)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(FollowButton)
|
||||
124
src/js/components/Fields/LastfmAuthenticationFrame.js
Executable file
124
src/js/components/Fields/LastfmAuthenticationFrame.js
Executable file
@ -0,0 +1,124 @@
|
||||
|
||||
import React, { PropTypes } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { createStore, bindActionCreators } from 'redux'
|
||||
import ReactGA from 'react-ga'
|
||||
import FontAwesome from 'react-fontawesome'
|
||||
|
||||
import Thumbnail from '../Thumbnail'
|
||||
|
||||
import * as uiActions from '../../services/ui/actions'
|
||||
import * as lastfmActions from '../../services/lastfm/actions'
|
||||
|
||||
class LastfmAuthenticationFrame extends React.Component{
|
||||
|
||||
constructor(props){
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
authorizing: false
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount(){
|
||||
let self = this;
|
||||
|
||||
// Listen for incoming messages from the authorization popup
|
||||
window.addEventListener('message', function(event){
|
||||
var data = JSON.parse(event.data);
|
||||
|
||||
// Only digest messages relevant to us
|
||||
if (data.origin == 'auth_lastfm'){
|
||||
self.handleMessage(event, data);
|
||||
}
|
||||
}, false);
|
||||
}
|
||||
|
||||
handleMessage(event, data){
|
||||
|
||||
// Only allow incoming data from our authorized authenticator proxy
|
||||
var authorization_domain = this.props.authorization_url.substring(0,this.props.authorization_url.indexOf('/',8))
|
||||
if (event.origin != authorization_domain){
|
||||
this.props.uiActions.createNotification({content: 'Authorization failed. '+event.origin+' is not the configured authorization_url.', type: 'bad'});
|
||||
return false
|
||||
}
|
||||
|
||||
// Bounced with an error
|
||||
if (data.error !== undefined){
|
||||
this.props.uiActions.createNotification({content: data.message, type: 'bad'});
|
||||
|
||||
// No errors? We're in!
|
||||
} else {
|
||||
this.props.lastfmActions.authorizationGranted(data)
|
||||
this.props.lastfmActions.getMe()
|
||||
}
|
||||
|
||||
// Turn off our authorizing switch
|
||||
this.setState({authorizing: false});
|
||||
}
|
||||
|
||||
startAuthorization(){
|
||||
|
||||
var self = this
|
||||
this.setState({authorizing: true})
|
||||
|
||||
// Open an authentication request window
|
||||
var url = this.props.authorization_url+'?action=authorize'
|
||||
var popup = window.open(url,"popup","height=580,width=350");
|
||||
popup.name = "LastfmAuthenticationWindow";
|
||||
|
||||
// Start timer to check our popup's state
|
||||
var timer = setInterval(checkPopup, 1000);
|
||||
function checkPopup(){
|
||||
|
||||
// Popup has been closed
|
||||
if (typeof(popup) !== 'undefined' && popup){
|
||||
if (popup.closed){
|
||||
self.setState({authorizing: false})
|
||||
clearInterval(timer);
|
||||
}
|
||||
|
||||
// Popup does not exist, so must have been blocked
|
||||
} else {
|
||||
self.props.uiActions.createNotification({content: 'Popup blocked. Please allow popups and try again.', type: 'bad'});
|
||||
self.setState({authorizing: false})
|
||||
clearInterval(timer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render(){
|
||||
if (this.state.authorizing){
|
||||
return (
|
||||
<button className="working">
|
||||
Authorizing...
|
||||
</button>
|
||||
)
|
||||
} else if (this.props.authorized){
|
||||
return (
|
||||
<button className="destructive" onClick={() => this.props.lastfmActions.revokeAuthorization()}>Log out</button>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<button className="primary" onClick={() => this.startAuthorization()}>Log in</button>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
return {
|
||||
authorization_url: state.lastfm.authorization_url,
|
||||
authorized: state.lastfm.session,
|
||||
authorizing: state.lastfm.authorizing
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
uiActions: bindActionCreators(uiActions, dispatch),
|
||||
lastfmActions: bindActionCreators(lastfmActions, dispatch)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(LastfmAuthenticationFrame)
|
||||
62
src/js/components/Fields/LastfmLoveButton.js
Executable file
62
src/js/components/Fields/LastfmLoveButton.js
Executable file
@ -0,0 +1,62 @@
|
||||
|
||||
import React, { PropTypes } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { Link } from 'react-router'
|
||||
import { createStore, bindActionCreators } from 'redux'
|
||||
import FontAwesome from 'react-fontawesome'
|
||||
|
||||
import * as helpers from '../../helpers'
|
||||
import * as uiActions from '../../services/ui/actions'
|
||||
import * as lastfmActions from '../../services/lastfm/actions'
|
||||
|
||||
class FollowButton extends React.Component{
|
||||
|
||||
constructor(props){
|
||||
super(props);
|
||||
}
|
||||
|
||||
remove(){
|
||||
this.props.lastfmActions.unloveTrack(this.props.uri, this.props.artist, this.props.track);
|
||||
}
|
||||
|
||||
add(){
|
||||
this.props.lastfmActions.loveTrack(this.props.uri, this.props.artist, this.props.track);
|
||||
}
|
||||
|
||||
render(){
|
||||
if (!this.props.uri){
|
||||
return false;
|
||||
}
|
||||
|
||||
var className = '';
|
||||
|
||||
// Inherit passed-down classes
|
||||
if (this.props.className){
|
||||
className += ' '+this.props.className;
|
||||
}
|
||||
|
||||
if (!this.props.lastfm_authorized){
|
||||
return <button className={className+' disabled'} onClick={e => this.props.uiActions.createNotification({content: 'You must authorize LastFM first', type: 'warning'})}>{this.props.addText}</button>
|
||||
} else if (this.props.is_loved && this.props.is_loved !== "0"){
|
||||
return <button className={className+' destructive'} onClick={e => this.remove()}>{this.props.removeText}</button>
|
||||
} else {
|
||||
return <button className={className} onClick={e => this.add()}>{this.props.addText}</button>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
return {
|
||||
load_queue: state.ui.load_queue,
|
||||
lastfm_authorized: state.lastfm.session
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
uiActions: bindActionCreators(uiActions, dispatch),
|
||||
lastfmActions: bindActionCreators(lastfmActions, dispatch)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(FollowButton)
|
||||
112
src/js/components/Fields/LatencyControl.js
Executable file
112
src/js/components/Fields/LatencyControl.js
Executable file
@ -0,0 +1,112 @@
|
||||
|
||||
import React, { PropTypes } from 'react';
|
||||
|
||||
export default class LatencyControl extends React.Component{
|
||||
|
||||
constructor(props){
|
||||
super(props);
|
||||
}
|
||||
|
||||
handleClick(e){
|
||||
var slider = e.target;
|
||||
if (slider.className != 'slider'){
|
||||
slider = slider.parentElement;
|
||||
}
|
||||
|
||||
var sliderX = e.clientX - slider.getBoundingClientRect().left;
|
||||
var sliderWidth = slider.getBoundingClientRect().width;
|
||||
|
||||
// Clicked left half
|
||||
if (sliderX < (sliderWidth/2)){
|
||||
var percent = Math.round((sliderX / (sliderWidth/2) ) * 100);
|
||||
|
||||
// Invert our percentage
|
||||
percent = 100 - percent;
|
||||
|
||||
var value = -(this.props.max * (percent / 100));
|
||||
|
||||
// Handle maximum value limits
|
||||
if (value < -this.props.max){
|
||||
value = -this.props.max;
|
||||
}
|
||||
|
||||
// Second half
|
||||
} else {
|
||||
|
||||
// Subtract half the slider's width from our click position
|
||||
sliderX = sliderX - (sliderWidth/2);
|
||||
var percent = Math.round((sliderX / (sliderWidth/2) ) * 100);
|
||||
|
||||
var value = this.props.max * (percent / 100);
|
||||
|
||||
// Handle maximum value limits
|
||||
if (value > this.props.max){
|
||||
value = this.props.max;
|
||||
}
|
||||
}
|
||||
|
||||
this.props.onChange(value);
|
||||
}
|
||||
|
||||
handleWheel(e){
|
||||
if (this.props.scrollWheel){
|
||||
|
||||
// Identify which direction we've scrolled (inverted)
|
||||
// This is simplified and doesn't consider momentum as it varies wildly
|
||||
// between browsers and devices
|
||||
var direction = (e.deltaY > 0 ? -1 : 1)
|
||||
var value = this.props.value;
|
||||
|
||||
value += direction * 5
|
||||
|
||||
if (value > this.props.max){
|
||||
value = this.props.max;
|
||||
} else if (value < -this.props.max){
|
||||
value = -this.props.max;
|
||||
}
|
||||
|
||||
this.props.onChange(value);
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
render(){
|
||||
|
||||
// Zero, or positive value
|
||||
if (this.props.value >= 0){
|
||||
var value = this.props.value;
|
||||
if (value > this.props.max){
|
||||
value = this.props.max;
|
||||
}
|
||||
var percentage = Math.round((value / this.props.max) * 100 / 2);
|
||||
var left = 50;
|
||||
var width = percentage;
|
||||
var negative = false;
|
||||
|
||||
// Negative value
|
||||
// We reverse it to a positive for easier maths and style rules
|
||||
} else {
|
||||
var value = -this.props.value;
|
||||
if (value < -this.props.max){
|
||||
value = -this.props.max;
|
||||
}
|
||||
var percentage = Math.round((value / this.props.max) * 100 / 2);
|
||||
var left = 50 - percentage;
|
||||
var width = percentage;
|
||||
var negative = true;
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="latency-control" onWheel={e => this.handleWheel(e)}>
|
||||
<div className="slider-wrapper">
|
||||
<div className="slider horizontal" onClick={e => this.handleClick(e)}>
|
||||
<div className="zero"></div>
|
||||
<div className="track">
|
||||
<div className={"progress "+(negative ? 'negative' : 'positive')} style={{ width: width+'%', left: left+'%' }}></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
178
src/js/components/Fields/PlaybackControls.js
Executable file
178
src/js/components/Fields/PlaybackControls.js
Executable file
@ -0,0 +1,178 @@
|
||||
|
||||
import React, { PropTypes } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { Link } from 'react-router'
|
||||
import { bindActionCreators } from 'redux'
|
||||
import FontAwesome from 'react-fontawesome'
|
||||
|
||||
import ProgressSlider from './ProgressSlider'
|
||||
import VolumeControl from './VolumeControl'
|
||||
import Dater from './../Dater'
|
||||
import ArtistSentence from './../ArtistSentence'
|
||||
import Thumbnail from './../Thumbnail'
|
||||
import Icon from './../Icon'
|
||||
|
||||
import * as helpers from '../../helpers'
|
||||
import * as uiActions from '../../services/ui/actions'
|
||||
import * as coreActions from '../../services/core/actions'
|
||||
import * as mopidyActions from '../../services/mopidy/actions'
|
||||
|
||||
class PlaybackControls extends React.Component{
|
||||
|
||||
constructor(props){
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
expanded: false
|
||||
}
|
||||
}
|
||||
|
||||
renderPlayButton(){
|
||||
var button = <a className="control play" onClick={() => this.props.mopidyActions.play()}><Icon name="play" /></a>
|
||||
if (this.props.play_state == 'playing'){
|
||||
button = <a className="control play" onClick={() => this.props.mopidyActions.pause()}><Icon name="pause" /></a>
|
||||
}
|
||||
return button;
|
||||
}
|
||||
|
||||
renderConsumeButton(){
|
||||
var button = <a className="control has-tooltip" onClick={() => this.props.mopidyActions.setConsume(true)}><FontAwesome name="fire" /><span className="tooltip">Consume</span></a>
|
||||
if (this.props.consume){
|
||||
button = <a className="control active has-tooltip" onClick={() => this.props.mopidyActions.setConsume(false)}><FontAwesome name="fire" /><span className="tooltip">Consume</span></a>
|
||||
}
|
||||
return button;
|
||||
}
|
||||
|
||||
renderRandomButton(){
|
||||
var button = <a className="control has-tooltip" onClick={() => this.props.mopidyActions.setRandom(true)}><FontAwesome name="random" /><span className="tooltip">Shuffle</span></a>
|
||||
if (this.props.random){
|
||||
button = <a className="control active has-tooltip" onClick={() => this.props.mopidyActions.setRandom(false)}><FontAwesome name="random" /><span className="tooltip">Shuffle</span></a>
|
||||
}
|
||||
return button;
|
||||
}
|
||||
|
||||
renderRepeatButton(){
|
||||
var button = <a className="control has-tooltip" onClick={() => this.props.mopidyActions.setRepeat(true)}><FontAwesome name="repeat" /><span className="tooltip">Repeat</span></a>
|
||||
if (this.props.repeat){
|
||||
button = <a className="control active has-tooltip" onClick={() => this.props.mopidyActions.setRepeat(false)}><FontAwesome name="repeat" /><span className="tooltip">Repeat</span></a>
|
||||
}
|
||||
return button;
|
||||
}
|
||||
|
||||
handleThumbnailClick(e){
|
||||
e.preventDefault();
|
||||
this.props.uiActions.openModal('kiosk_mode');
|
||||
}
|
||||
|
||||
render(){
|
||||
var images = false
|
||||
if (this.props.current_track && this.props.current_track.images){
|
||||
images = this.props.current_track.images
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={(this.state.expanded ? "expanded playback-controls" : "playback-controls")}>
|
||||
|
||||
{this.props.http_streaming_enabled && this.props.http_streaming_active && this.props.play_state == 'playing' ? <audio id="http-streamer" autoPlay preload="none">
|
||||
<source src={this.props.http_streaming_url} type={"audio/"+this.props.http_streaming_encoding} />
|
||||
</audio> : null}
|
||||
|
||||
{this.props.next_track && this.props.next_track.images ? <Thumbnail className="hide" size="large" images={this.props.next_track.images} /> : null}
|
||||
|
||||
<div className="current-track">
|
||||
<div className="thumbnail-wrapper" onClick={e => this.handleThumbnailClick(e)}>
|
||||
<Thumbnail size="small" images={images} />
|
||||
</div>
|
||||
<div className="title">
|
||||
{ this.props.current_track ? this.props.current_track.name : <span>-</span> }
|
||||
</div>
|
||||
<div className="artist">
|
||||
{ this.props.current_track ? <ArtistSentence artists={ this.props.current_track.artists } /> : <ArtistSentence /> }
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section className="playback">
|
||||
<a className="control previous" onClick={() => this.props.mopidyActions.previous()}>
|
||||
<Icon name="back" />
|
||||
</a>
|
||||
{ this.renderPlayButton() }
|
||||
<a className="control stop" onClick={() => this.props.mopidyActions.stop()}>
|
||||
<Icon name="stop" />
|
||||
</a>
|
||||
<a className="control next" onClick={() => this.props.mopidyActions.next()}>
|
||||
<Icon name="skip" />
|
||||
</a>
|
||||
</section>
|
||||
|
||||
<section className="settings">
|
||||
{this.renderConsumeButton()}
|
||||
{this.renderRandomButton()}
|
||||
{this.renderRepeatButton()}
|
||||
</section>
|
||||
|
||||
<section className="progress">
|
||||
<ProgressSlider />
|
||||
<span className="current">{ this.props.time_position ? <Dater type="length" data={this.props.time_position} /> : '-' }</span>
|
||||
<span className="total">{ this.props.current_track ? <Dater type="length" data={this.props.current_track.length} /> : '-' }</span>
|
||||
</section>
|
||||
|
||||
<section className="volume">
|
||||
<VolumeControl
|
||||
scrollWheel
|
||||
volume={this.props.volume}
|
||||
mute={this.props.mute}
|
||||
onVolumeChange={percent => this.props.mopidyActions.setVolume(percent)}
|
||||
onMuteChange={mute => this.props.mopidyActions.setMute(mute)}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section className="triggers">
|
||||
<a className="control expanded-controls" onClick={() => this.setState({expanded: !this.state.expanded})}>
|
||||
{this.state.expanded ? <FontAwesome name="chevron-down" /> : <FontAwesome name="chevron-up" />}
|
||||
</a>
|
||||
<a className={"control sidebar-toggle"+(this.props.sidebar_open ? ' open' : '')} onClick={() => this.props.uiActions.toggleSidebar()}>
|
||||
<FontAwesome className="open" name="bars" />
|
||||
</a>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Export our component
|
||||
*
|
||||
* We also integrate our global store, using connect()
|
||||
**/
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
return {
|
||||
http_streaming_active: state.core.http_streaming_active,
|
||||
http_streaming_enabled: state.core.http_streaming_enabled,
|
||||
http_streaming_encoding: state.core.http_streaming_encoding,
|
||||
http_streaming_url: state.core.http_streaming_url,
|
||||
current_track: (state.core.current_track && state.core.tracks[state.core.current_track.uri] !== undefined ? state.core.tracks[state.core.current_track.uri] : null),
|
||||
next_track: (state.core.next_track_uri && state.core.tracks[state.core.next_track_uri] !== undefined ? state.core.tracks[state.core.next_track_uri] : null),
|
||||
radio_enabled: (state.ui.radio && state.ui.radio.enabled ? true : false),
|
||||
play_state: state.mopidy.play_state,
|
||||
time_position: state.mopidy.time_position,
|
||||
consume: state.mopidy.consume,
|
||||
repeat: state.mopidy.repeat,
|
||||
random: state.mopidy.random,
|
||||
volume: state.mopidy.volume,
|
||||
mute: state.mopidy.mute,
|
||||
sidebar_open: state.ui.sidebar_open
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
coreActions: bindActionCreators(coreActions, dispatch),
|
||||
uiActions: bindActionCreators(uiActions, dispatch),
|
||||
mopidyActions: bindActionCreators(mopidyActions, dispatch)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(PlaybackControls)
|
||||
70
src/js/components/Fields/ProgressSlider.js
Executable file
70
src/js/components/Fields/ProgressSlider.js
Executable file
@ -0,0 +1,70 @@
|
||||
|
||||
import React, { PropTypes } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { bindActionCreators } from 'redux'
|
||||
|
||||
import * as mopidyActions from '../../services/mopidy/actions'
|
||||
|
||||
class ProgressSlider extends React.Component{
|
||||
|
||||
constructor(props){
|
||||
super(props);
|
||||
}
|
||||
|
||||
handleClick(e){
|
||||
var slider = e.target;
|
||||
if (slider.className != 'slider' ) slider = slider.parentElement;
|
||||
|
||||
var sliderX = e.clientX - slider.getBoundingClientRect().left;
|
||||
var sliderWidth = slider.getBoundingClientRect().width;
|
||||
var percent = (sliderX / sliderWidth ).toFixed(2);
|
||||
|
||||
if (this.props.connected && this.props.current_track){
|
||||
var destination_time = this.props.current_track.duration * percent;
|
||||
this.props.mopidyActions.setTimePosition(destination_time);
|
||||
this.setState({ animating: false });
|
||||
}
|
||||
}
|
||||
|
||||
render(){
|
||||
var percent = 0;
|
||||
if (this.props.connected && this.props.current_track){
|
||||
percent = this.props.time_position / this.props.current_track.duration;
|
||||
percent = percent * 100;
|
||||
if (percent > 1000){
|
||||
percent = 100;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={"progress slider horizontal "+this.props.play_state} onClick={ (e) => this.handleClick(e) } >
|
||||
<div className="track">
|
||||
<div className="progress" style={{ width: (percent)+'%' }}></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Export our component
|
||||
*
|
||||
* We also integrate our global store, using connect()
|
||||
**/
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
return {
|
||||
current_track: (state.core.current_track && state.core.tracks[state.core.current_track.uri] !== undefined ? state.core.tracks[state.core.current_track.uri] : null),
|
||||
connected: state.mopidy.connected,
|
||||
time_position: state.mopidy.time_position,
|
||||
play_state: state.mopidy.play_state
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
mopidyActions: bindActionCreators(mopidyActions, dispatch)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ProgressSlider)
|
||||
98
src/js/components/Fields/SearchForm.js
Executable file
98
src/js/components/Fields/SearchForm.js
Executable file
@ -0,0 +1,98 @@
|
||||
|
||||
import React, { PropTypes } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { hashHistory } from 'react-router'
|
||||
import { createStore, bindActionCreators } from 'redux'
|
||||
import FontAwesome from 'react-fontawesome'
|
||||
|
||||
import * as helpers from '../../helpers'
|
||||
import * as uiActions from '../../services/ui/actions'
|
||||
|
||||
class SearchForm extends React.Component{
|
||||
|
||||
constructor(props){
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
query: '',
|
||||
in_focus: false
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount(){
|
||||
if (this.props.query){
|
||||
var query = this.props.query.replace("search:","");
|
||||
this.setState({query: query});
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(newProps){
|
||||
if (newProps.query && newProps.query != this.state.query && !this.state.in_focus){
|
||||
this.setState({query: newProps.query.replace("search:","")});
|
||||
}
|
||||
}
|
||||
|
||||
handleSubmit(e){
|
||||
e.preventDefault()
|
||||
|
||||
// check for uri type matching
|
||||
switch (helpers.uriType(this.state.query)){
|
||||
|
||||
case 'album':
|
||||
hashHistory.push(global.baseURL+'album/'+encodeURIComponent(this.state.query))
|
||||
break
|
||||
|
||||
case 'artist':
|
||||
hashHistory.push(global.baseURL+'artist/'+encodeURIComponent(this.state.query))
|
||||
break
|
||||
|
||||
case 'playlist':
|
||||
hashHistory.push(global.baseURL+'playlist/'+encodeURIComponent(this.state.query))
|
||||
break
|
||||
|
||||
default:
|
||||
var available_views = ["artist","album","playlist","track"];
|
||||
var view_defined = false;
|
||||
var query = this.state.query;
|
||||
|
||||
for (var i = 0; i < available_views.length; i++){
|
||||
if (query.startsWith(available_views[i]+':')){
|
||||
view_defined = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!view_defined){
|
||||
query = "all:"+query;
|
||||
}
|
||||
|
||||
hashHistory.push(global.baseURL+'search/search:'+query);
|
||||
break
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
render(){
|
||||
return (
|
||||
<form className="search-form" onSubmit={e => this.handleSubmit(e)}>
|
||||
<label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search..."
|
||||
onChange={ e => this.setState({ query: e.target.value }) }
|
||||
onFocus={e => this.setState({in_focus: true})}
|
||||
onBlur={e => this.setState({in_focus: false})}
|
||||
value={ this.state.query } />
|
||||
</label>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
uiActions: bindActionCreators(uiActions, dispatch)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapDispatchToProps)(SearchForm)
|
||||
139
src/js/components/Fields/SpotifyAuthenticationFrame.js
Executable file
139
src/js/components/Fields/SpotifyAuthenticationFrame.js
Executable file
@ -0,0 +1,139 @@
|
||||
|
||||
import React, { PropTypes } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { createStore, bindActionCreators } from 'redux'
|
||||
import ReactGA from 'react-ga'
|
||||
import FontAwesome from 'react-fontawesome'
|
||||
|
||||
import Thumbnail from '../Thumbnail'
|
||||
|
||||
import * as uiActions from '../../services/ui/actions'
|
||||
import * as spotifyActions from '../../services/spotify/actions'
|
||||
|
||||
class SpotifyAuthenticationFrame extends React.Component{
|
||||
|
||||
constructor(props){
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
authorizing: false
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount(){
|
||||
let self = this;
|
||||
|
||||
// Listen for incoming messages from the authorization popup
|
||||
window.addEventListener('message', function(event){
|
||||
var data = JSON.parse(event.data);
|
||||
|
||||
// Only digest messages relevant to us
|
||||
if (data.origin == 'auth_spotify'){
|
||||
self.handleMessage(event, data);
|
||||
}
|
||||
}, false);
|
||||
}
|
||||
|
||||
handleMessage(event, data){
|
||||
|
||||
// Only allow incoming data from our authorized authenticator proxy
|
||||
var authorization_domain = this.props.authorization_url.substring(0,this.props.authorization_url.indexOf('/',8))
|
||||
if (event.origin != authorization_domain){
|
||||
this.props.uiActions.createNotification({content: 'Authorization failed. '+event.origin+' is not the configured authorization_url.', type: 'bad'});
|
||||
return false
|
||||
}
|
||||
|
||||
// Spotify bounced with an error
|
||||
if (data.error !== undefined){
|
||||
this.props.uiActions.createNotification({content: data.error, type: 'bad'});
|
||||
|
||||
// No errors? We're in!
|
||||
} else {
|
||||
this.props.spotifyActions.authorizationGranted(data)
|
||||
this.props.spotifyActions.getMe()
|
||||
}
|
||||
|
||||
// Turn off our authorizing switch
|
||||
this.setState({authorizing: false})
|
||||
}
|
||||
|
||||
startAuthorization(){
|
||||
|
||||
var self = this
|
||||
this.setState({authorizing: true})
|
||||
|
||||
// Open an authentication request window (to spotify)
|
||||
var url = this.props.authorization_url+'?action=authorize'
|
||||
var scopes = [
|
||||
'playlist-modify-private',
|
||||
'playlist-modify-public',
|
||||
'playlist-read-private',
|
||||
'playlist-modify-private',
|
||||
'user-library-read',
|
||||
'user-library-modify',
|
||||
'user-follow-modify',
|
||||
'user-follow-read',
|
||||
'user-read-email',
|
||||
'user-top-read',
|
||||
'user-read-currently-playing',
|
||||
'user-read-playback-state',
|
||||
'playlist-read-collaborative',
|
||||
'ugc-image-upload' // playlist image uploading
|
||||
]
|
||||
var popup = window.open(url+'&scope='+scopes.join('%20'),"popup","height=580,width=350");
|
||||
|
||||
// Start timer to check our popup's state
|
||||
var timer = setInterval(checkPopup, 1000);
|
||||
function checkPopup(){
|
||||
|
||||
// Popup has been closed
|
||||
if (typeof(popup) !== 'undefined' && popup){
|
||||
if (popup.closed){
|
||||
self.setState({authorizing: false})
|
||||
clearInterval(timer);
|
||||
}
|
||||
|
||||
// Popup does not exist, so must have been blocked
|
||||
} else {
|
||||
self.props.uiActions.createNotification({content: 'Popup blocked. Please allow popups and try again.', type: 'bad'});
|
||||
self.setState({authorizing: false})
|
||||
clearInterval(timer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render(){
|
||||
if (this.state.authorizing){
|
||||
return (
|
||||
<button className="working">
|
||||
Authorizing...
|
||||
</button>
|
||||
)
|
||||
} else if (this.props.authorized){
|
||||
return (
|
||||
<button className="destructive" onClick={() => this.props.spotifyActions.revokeAuthorization()}>Log out</button>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<button className="primary" onClick={() => this.startAuthorization()}>Log in</button>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
return {
|
||||
authorization_url: state.spotify.authorization_url,
|
||||
authorized: state.spotify.authorization,
|
||||
authorizing: state.spotify.authorizing
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
uiActions: bindActionCreators(uiActions, dispatch),
|
||||
spotifyActions: bindActionCreators(spotifyActions, dispatch)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(SpotifyAuthenticationFrame)
|
||||
48
src/js/components/Fields/TextField.js
Executable file
48
src/js/components/Fields/TextField.js
Executable file
@ -0,0 +1,48 @@
|
||||
|
||||
import React from 'react'
|
||||
|
||||
export default class TextField extends React.Component{
|
||||
|
||||
constructor(props){
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
in_focus: false,
|
||||
value: (this.props.value ? this.props.value : '')
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(newProps){
|
||||
if (!this.state.in_focus){
|
||||
this.setState({value: newProps.value})
|
||||
}
|
||||
}
|
||||
|
||||
handleChange(e){
|
||||
this.setState({value: e.target.value});
|
||||
}
|
||||
|
||||
handleFocus(e){
|
||||
this.setState({in_focus: true});
|
||||
}
|
||||
|
||||
handleBlur(e){
|
||||
this.setState({in_focus: false});
|
||||
if (this.state.value !== this.props.value){
|
||||
this.props.onChange(this.state.value);
|
||||
}
|
||||
}
|
||||
|
||||
render(){
|
||||
return (
|
||||
<input
|
||||
className={this.props.className ? this.props.className : ''}
|
||||
type="text"
|
||||
onChange={e => this.handleChange(e)}
|
||||
onFocus={e => this.handleFocus(e)}
|
||||
onBlur={e => this.handleBlur(e)}
|
||||
value={this.state.value}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
86
src/js/components/Fields/VolumeControl.js
Executable file
86
src/js/components/Fields/VolumeControl.js
Executable file
@ -0,0 +1,86 @@
|
||||
|
||||
import React, { PropTypes } from 'react'
|
||||
import FontAwesome from 'react-fontawesome'
|
||||
|
||||
export default class VolumeControl extends React.Component{
|
||||
|
||||
constructor(props){
|
||||
super(props)
|
||||
}
|
||||
|
||||
handleClick(e){
|
||||
var slider = e.target;
|
||||
if (slider.className != 'slider' ) slider = slider.parentElement;
|
||||
|
||||
var sliderX = e.clientX - slider.getBoundingClientRect().left;
|
||||
var sliderWidth = slider.getBoundingClientRect().width;
|
||||
var percent = Math.round((sliderX / sliderWidth ) * 100);
|
||||
|
||||
if (percent > 100){
|
||||
percent = 100
|
||||
} else if (percent < 0){
|
||||
percent = 0
|
||||
}
|
||||
|
||||
this.props.onVolumeChange(percent);
|
||||
}
|
||||
|
||||
handleWheel(e){
|
||||
if (this.props.scrollWheel){
|
||||
|
||||
// Identify which direction we've scrolled (inverted)
|
||||
// This is simplified and doesn't consider momentum as it varies wildly
|
||||
// between browsers and devices
|
||||
var direction = (e.deltaY > 0 ? -1 : 1)
|
||||
var percent = this.props.volume;
|
||||
|
||||
percent += direction * 5
|
||||
|
||||
if (percent > 100){
|
||||
percent = 100
|
||||
} else if (percent < 0){
|
||||
percent = 0
|
||||
}
|
||||
|
||||
this.props.onVolumeChange(percent);
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
renderMuteButton(){
|
||||
if (this.props.mute){
|
||||
return (
|
||||
<a className="control mute-control has-tooltip" onClick={() => this.props.onMuteChange(false)}>
|
||||
<FontAwesome className="red-text" name="volume-off" />
|
||||
<span className="tooltip">Unmute</span>
|
||||
</a>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<a className="control mute-control has-tooltip" onClick={() => this.props.onMuteChange(true)}>
|
||||
<FontAwesome className="muted" name="volume-off" />
|
||||
<span className="tooltip">Mute</span>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
render(){
|
||||
var className = "volume-control"
|
||||
if (this.props.mute){
|
||||
className += " muted"
|
||||
}
|
||||
return (
|
||||
<span className={className} onWheel={e => this.handleWheel(e)}>
|
||||
{this.renderMuteButton()}
|
||||
<div className="slider-wrapper">
|
||||
<div className="slider horizontal" onClick={e => this.handleClick(e)}>
|
||||
<div className="track">
|
||||
<div className="progress" style={{ width: this.props.volume+'%' }}></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user