diff --git a/.htaccess b/.htaccess index 7352b65c..ccaa4ef3 100755 --- a/.htaccess +++ b/.htaccess @@ -1,5 +1,4 @@ - RewriteEngine On RewriteCond %{REQUEST_URI} !/production/ [NC] -RewriteRule (.*) /production/$1 [NC,L] \ No newline at end of file +RewriteRule (.*) /production/$1 [NC,L] diff --git a/src/js/actions/index.js b/src/js/actions/index.js index fec22d6a..c632dc3a 100755 --- a/src/js/actions/index.js +++ b/src/js/actions/index.js @@ -1,4 +1,3 @@ import albumActions from './albumActions' -import mopidy from './mopidy' diff --git a/src/js/actions/mopidy.js b/src/js/actions/mopidy.js deleted file mode 100755 index a4f49050..00000000 --- a/src/js/actions/mopidy.js +++ /dev/null @@ -1,68 +0,0 @@ - -/** - * Actions and Action Creators - **/ - -export function updateStatus( online ){ - return { - type: 'STATUS', - online: online - } -} - -export function updateState( state ){ - return { - type: 'STATE', - state: state - } -} - -export function updateConsume( consume ){ - return { - type: 'CONSUME', - consume: consume - } -} - -export function updateRandom( random ){ - return { - type: 'RANDOM', - random: random - } -} - -export function updateRepeat( repeat ){ - return { - type: 'REPEAT', - repeat: repeat - } -} - -export function updateTlTracks( tracks ){ - return { - type: 'TRACKLIST', - tracks: tracks - } -} - -export function updateCurrentTlTrack( tltrack ){ - return { - type: 'TRACKINFOCUS', - trackInFocus: tltrack - } -} - -export function updateVolume( volume ){ - return { - type: 'VOLUME', - volume: volume - } -} - -export function changeTrack( tlid ){ - return { - type: 'CHANGE_TRACK', - tlid: tlid - } -} - diff --git a/src/js/bootstrap.js b/src/js/bootstrap.js index f5553dcf..fa909f90 100755 --- a/src/js/bootstrap.js +++ b/src/js/bootstrap.js @@ -1,10 +1,16 @@ console.info('Bootstrapping...'); -import { createStore } from 'redux' +import { createStore, applyMiddleware } from 'redux' +import thunk from 'redux-thunk' import reducer from './reducers/index' +import mopidyMiddleware from './services/mopidy/middleware' // create our global store -let store = createStore( reducer, {} ); +let store = createStore( + reducer, + {}, + applyMiddleware( thunk, mopidyMiddleware ) +); export default store; diff --git a/src/js/components/App.js b/src/js/components/App.js index a57152d2..30817054 100755 --- a/src/js/components/App.js +++ b/src/js/components/App.js @@ -6,8 +6,7 @@ import { Link } from 'react-router' import { connect } from 'react-redux' import * as actions from '../actions/index' -import Services from '../services/Services' -import MopidyService from '../services/MopidyService' +import * as mopidyActions from '../services/mopidy/actions' /** @@ -19,6 +18,10 @@ class App extends React.Component{ super(props); } + componentDidMount(){ + this.props.mopidyActions.connect(); + } + render(){ return ( @@ -32,7 +35,6 @@ class App extends React.Component{
  • Authorize
  • {this.props.children} - ); } @@ -50,7 +52,8 @@ const mapStateToProps = (state, ownProps) => { const mapDispatchToProps = (dispatch) => { return { - actions: bindActionCreators(actions, dispatch) + actions: bindActionCreators(actions, dispatch), + mopidyActions: bindActionCreators(mopidyActions, dispatch) } } diff --git a/src/js/components/Player.js b/src/js/components/Player.js new file mode 100755 index 00000000..5d99a1b4 --- /dev/null +++ b/src/js/components/Player.js @@ -0,0 +1,76 @@ + +import React, { PropTypes } from 'react' +import { connect } from 'react-redux' +import { bindActionCreators } from 'redux' + +import FontAwesome from 'react-fontawesome' +import * as actions from '../services/mopidy/actions' + +class Player extends React.Component{ + + constructor(props) { + super(props); + } + + renderControls(){ + + var playButton = this.props.actions.instruct('playback.play')}> + if( this.props.mopidy.state == 'playing' ){ + playButton = this.props.actions.instruct('playback.pause')}> + } + + var consumeButton = this.props.actions.instruct('tracklist.setConsume', [true])}>Consume + if( this.props.mopidy.consume ){ + consumeButton = this.props.actions.instruct('tracklist.setConsume', [false])}>Un-Consume + } + + var randomButton = this.props.actions.instruct('tracklist.setRandom', [true])}>Random + if( this.props.mopidy.random ){ + randomButton = this.props.actions.instruct('tracklist.setRandom', [false])}>Un-Random + } + + var repeatButton = this.props.actions.instruct('tracklist.setRepeat', [true])}>Repeat + if( this.props.mopidy.repeat ){ + repeatButton = this.props.actions.instruct('tracklist.setRepeat', [false])}>Un-Repeat + } + + return ( +
    + { playButton } + this.props.actions.instruct('playback.previous')}> + this.props.actions.instruct('playback.next')}> + { consumeButton } + { randomButton } + { repeatButton } + { this.props.mopidy.volume } +
    + ); + } + + render(){ + return ( +
    + { this.renderControls() } +
    + ); + } +} + + +/** + * Export our component + * + * We also integrate our global store, using connect() + **/ + +const mapStateToProps = (state, ownProps) => { + return state; +} + +const mapDispatchToProps = (dispatch) => { + return { + actions: bindActionCreators(actions, dispatch) + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(Player) \ No newline at end of file diff --git a/src/js/components/Queue.js b/src/js/components/Queue.js index 30c6e21e..bd45dc31 100755 --- a/src/js/components/Queue.js +++ b/src/js/components/Queue.js @@ -3,10 +3,11 @@ import React, { PropTypes } from 'react' import { connect } from 'react-redux' import { bindActionCreators } from 'redux' -import Services from '../services/Services' +import FontAwesome from 'react-fontawesome' import TrackList from '../components/TrackList' import Track from '../components/Track' -import * as actions from '../actions/mopidy' +import Player from '../components/Player' +import * as actions from '../services/mopidy/actions' class Queue extends React.Component{ @@ -39,6 +40,7 @@ class Queue extends React.Component{ { this.renderTrackInFocus() }

    Other tracks

    { this.renderTrackList() } + ); } diff --git a/src/js/components/TrackList.js b/src/js/components/TrackList.js index 1c5767bf..e500de16 100755 --- a/src/js/components/TrackList.js +++ b/src/js/components/TrackList.js @@ -2,10 +2,9 @@ import React, { PropTypes } from 'react' import { connect } from 'react-redux' import { bindActionCreators } from 'redux' -import * as actions from '../actions/mopidy' +import * as actions from '../services/mopidy/actions' import Track from './Track' -import MopidyService from '../services/MopidyService' class TrackList extends React.Component{ diff --git a/src/js/reducers/index.js b/src/js/reducers/index.js index 89e850a9..5e0f3653 100755 --- a/src/js/reducers/index.js +++ b/src/js/reducers/index.js @@ -7,7 +7,7 @@ // import all of our application reducers import album from './album' -import mopidy from './mopidy' +import mopidy from '../services/mopidy/reducer' import { combineReducers } from 'redux' diff --git a/src/js/reducers/mopidy.js b/src/js/reducers/mopidy.js deleted file mode 100755 index e74a3d82..00000000 --- a/src/js/reducers/mopidy.js +++ /dev/null @@ -1,57 +0,0 @@ - -export default function reducer(mopidy = {}, action){ - switch (action.type) { - - case 'STATUS': - return Object.assign({}, mopidy, { - online: action.online - }); - - case 'STATE': - return Object.assign({}, mopidy, { - state: action.state - }); - - case 'CONSUME': - return Object.assign({}, mopidy, { - consume: action.consume - }); - - case 'RANDOM': - return Object.assign({}, mopidy, { - random: action.random - }); - - case 'REPEAT': - return Object.assign({}, mopidy, { - repeat: action.repeat - }); - - case 'TRACKLIST': - return Object.assign({}, mopidy, { - tracks: action.tracks - }); - - case 'TRACKINFOCUS': - console.log( action ); - return Object.assign({}, mopidy, { - trackInFocus: action.trackInFocus - }); - - case 'VOLUME': - return Object.assign({}, mopidy, { - volume: action.volume - }); - - case 'CHANGE_TRACK': - return Object.assign({}, mopidy, { - tlid: action.tlid - }); - - default: - return mopidy - } -} - - - diff --git a/src/js/services/MopidyService.js b/src/js/services/MopidyService.js index 6d0446dc..83663429 100755 --- a/src/js/services/MopidyService.js +++ b/src/js/services/MopidyService.js @@ -5,7 +5,7 @@ import { connect } from 'react-redux' import { bindActionCreators } from 'redux' import FontAwesome from 'react-fontawesome' -import * as actions from '../actions/mopidy' +import * as actions from '../services/mopidy/actions' /** @@ -16,14 +16,21 @@ import * as actions from '../actions/mopidy' class MopidyService extends React.Component{ constructor( props ){ - super(props) - + super(props); +/* this.connection = new Mopidy({ - webSocketUrl: "ws://music.james:6680/mopidy/ws", + webSocketUrl: "ws://tv.barnsley.nz:6680/mopidy/ws", + //webSocketUrl: "ws://music.james:6680/mopidy/ws", callingConvention: 'by-position-or-by-name' }); this.connection.on( (type, data) => this.handleMessage( type, data ) ); + */ + } + + componentDidMount(){ + + this.props.actions.connect(); } handleMessage( type, data ){ diff --git a/src/js/services/mopidy/actions.js b/src/js/services/mopidy/actions.js new file mode 100755 index 00000000..fd3972c2 --- /dev/null +++ b/src/js/services/mopidy/actions.js @@ -0,0 +1,25 @@ + +/** + * Actions and Action Creators + **/ + +export function connect(){ + return { + type: 'CONNECT' + } +} + +export function disconnect(){ + return { + type: 'DISCONNECT' + } +} + +export function instruct( call, value ){ + return { + type: 'INSTRUCT', + call: call, + value: value + } +} + diff --git a/src/js/services/mopidy/middleware.js b/src/js/services/mopidy/middleware.js new file mode 100755 index 00000000..ba0ba0ab --- /dev/null +++ b/src/js/services/mopidy/middleware.js @@ -0,0 +1,153 @@ + +import Mopidy from 'mopidy' +import actions from './actions' + +const MopidyMiddleware = (function(){ + + var socket = null; + + const onOpen = (ws, store, token) => evt => { + console.log('opened') + //Send a handshake, or authenticate with remote end + + //Tell the store we're connected + //store.dispatch(actions.connected()); + } + + const onClose = (ws, store) => evt => { + //Tell the store we've disconnected + //store.dispatch(actions.disconnected()); + } + + const handleMessage = (ws, store, type, data) => { + + switch( type ){ + + case 'state:online': + //store.dispatch( actions.updateStatus( true ) ); + instruct( ws, store, 'playback.getState' ); + instruct( ws, store, 'playback.getVolume' ); + instruct( ws, store, 'tracklist.getConsume' ); + instruct( ws, store, 'tracklist.getRandom' ); + instruct( ws, store, 'tracklist.getRepeat' ); + instruct( ws, store, 'tracklist.getTlTracks' ); + instruct( ws, store, 'playback.getCurrentTlTrack' ); + break; + + case 'state:offline': + store.dispatch( actions.updateStatus( false ) ); + break; + + case 'event:tracklistChanged': + instruct( ws, store, 'tracklist.getTlTracks' ); + break; + + //case 'event:trackPlaybackEnded': + case 'event:playbackStateChanged': + case 'event:trackPlaybackStarted': + instruct( ws, store, 'playback.getState' ); + instruct( ws, store, 'playback.getCurrentTlTrack' ); + break; + + case 'event:volumeChanged': + store.dispatch( actions.updateVolume( data.volume ) ); + break; + + case 'event:optionsChanged': + instruct( ws, store, 'tracklist.getConsume' ); + instruct( ws, store, 'tracklist.getRandom' ); + instruct( ws, store, 'tracklist.getRepeat' ); + break; + + default: + //console.log( 'MopidyService: Unhandled event', type, message ); + } + } + + + function doStuff( ws, store ){ + console.log('doing stuff', store); + ws.playback.getState() + .then( + response => { + store.dispatch({ type: 'STATE', state: response }) + }, + error => { + console.error( error ); + } + ); + } + + + /** + * Get something from Mopidy + * + * Sends request to Mopidy server, and updates our local storage on return + * @param object ws Websocket + * @param string model Mopidy model (playback, tracklist, etc) + * @param string property the property to get (TlTracks, Consume, etc) + **/ + const instruct = ( ws, store, call, value = false ) => { + console.log('Mopidy: '+call, value); + var callParts = call.split('.'); + var model = callParts[0]; + var method = callParts[1]; + var property = method; + property = property.replace('get',''); + property = property.replace('set',''); + + if( value ){ + ws[model][method]( value ) + .then( + response => store.dispatch({ type: property, model: model, data: response }), + error => console.error( error ) + ); + }else{ + ws[model][method]() + .then( + response => store.dispatch({ type: property, model: model, data: response }), + error => console.error( error ) + ); + } + } + + return store => next => action => { + + switch(action.type) { + + //The user wants us to connect + case 'CONNECT': + + if(socket != null) socket.close(); + store.dispatch({ type: 'CONNECTING' }); + + socket = new Mopidy({ + webSocketUrl: "ws://tv.barnsley.nz:6680/mopidy/ws", + callingConvention: 'by-position-or-by-name' + }); + + socket.on( (type, data) => handleMessage( socket, store, type, data ) ); + + break; + + //The user wants us to disconnect + case 'DISCONNECT': + if(socket != null) socket.close(); + socket = null; + store.dispatch({ type: 'DISCONNECTED' }); + break; + + //Send the 'SEND_MESSAGE' action down the websocket to the server + case 'INSTRUCT': + instruct( socket, store, action.call, action.value ) + break; + + //This action is irrelevant to us, pass it on to the next middleware + default: + return next(action); + } + } + +})(); + +export default MopidyMiddleware \ No newline at end of file diff --git a/src/js/services/mopidy/reducer.js b/src/js/services/mopidy/reducer.js new file mode 100755 index 00000000..8a2cec12 --- /dev/null +++ b/src/js/services/mopidy/reducer.js @@ -0,0 +1,60 @@ + +export default function reducer(mopidy = {}, action){ + switch (action.type) { + + case 'CONNECTING': + return Object.assign({}, mopidy, { connected: false, connecting: true }); + + case 'CONNECTED': + return Object.assign({}, mopidy, { connected: true, connecting: false }); + + case 'DISCONNECTED': + return Object.assign({}, mopidy, { connected: false, connecting: false }); + + case 'State': + return Object.assign({}, mopidy, { + state: action.data + }); + + case 'Consume': + return Object.assign({}, mopidy, { + consume: action.data + }); + + case 'Random': + return Object.assign({}, mopidy, { + random: action.data + }); + + case 'Repeat': + return Object.assign({}, mopidy, { + repeat: action.data + }); + + case 'TlTracks': + return Object.assign({}, mopidy, { + tracks: action.data + }); + + case 'CurrentTlTrack': + return Object.assign({}, mopidy, { + trackInFocus: action.data + }); + + case 'Volume': + return Object.assign({}, mopidy, { + volume: action.volume + }); + + case 'CHANGE_TRACK': + return Object.assign({}, mopidy, { + tlid: action.tlid + }); + + default: + return mopidy + } +} + + +