From 6de44cda53c7fad1d722d171df67ceacac472bd6 Mon Sep 17 00:00:00 2001 From: James Barnsley Date: Thu, 27 Oct 2016 16:58:11 +1300 Subject: [PATCH] Pusher part one --- src/js/bootstrap.js | 14 ++++- src/js/components/ConfirmationButton.js | 6 +- .../components/SpotifyAuthenticationFrame.js | 2 +- src/js/services/pusher/actions.js | 31 ++++++++++ src/js/services/pusher/middleware.js | 58 +++++++++++++++++++ src/js/services/pusher/reducer.js | 23 ++++++++ src/js/views/App.js | 4 ++ src/js/views/Settings.js | 21 ++++++- src/scss/global/_forms.scss | 22 +++++++ src/scss/global/_variables.scss | 21 +++++-- 10 files changed, 192 insertions(+), 10 deletions(-) create mode 100755 src/js/services/pusher/actions.js create mode 100755 src/js/services/pusher/middleware.js create mode 100755 src/js/services/pusher/reducer.js diff --git a/src/js/bootstrap.js b/src/js/bootstrap.js index 924e6aa4..011d618f 100755 --- a/src/js/bootstrap.js +++ b/src/js/bootstrap.js @@ -4,16 +4,19 @@ console.info('Bootstrapping...'); import { createStore, applyMiddleware, combineReducers } from 'redux' import ui from './services/ui/reducer' +import pusher from './services/pusher/reducer' import mopidy from './services/mopidy/reducer' import spotify from './services/spotify/reducer' import thunk from 'redux-thunk' +import pusherMiddleware from './services/pusher/middleware' import mopidyMiddleware from './services/mopidy/middleware' import spotifyMiddleware from './services/spotify/middleware' import localstorageMiddleware from './services/localstorage/middleware' let reducers = combineReducers({ ui, + pusher, mopidy, spotify }); @@ -26,6 +29,9 @@ var initialState = { volume: 0, progress: 0 }, + pusher: { + port: 6681 + }, spotify: { country: 'NZ', locale: 'en_NZ', @@ -44,6 +50,12 @@ if( localStorage.getItem('mopidy') ){ initialState.mopidy = Object.assign(initialState.mopidy, storedMopidy ); } +// if we've got a stored version of pusher state, load and merge +if( localStorage.getItem('pusher') ){ + var storedPusher = JSON.parse( localStorage.getItem('pusher') ); + initialState.pusher = Object.assign(initialState.pusher, storedPusher ); +} + // if we've got a stored version of spotify state, load and merge if( localStorage.getItem('spotify') ){ var storedSpotify = JSON.parse( localStorage.getItem('spotify') ); @@ -53,7 +65,7 @@ if( localStorage.getItem('spotify') ){ let store = createStore( reducers, initialState, - applyMiddleware( thunk, localstorageMiddleware, mopidyMiddleware, spotifyMiddleware ) + applyMiddleware( thunk, localstorageMiddleware, pusherMiddleware, mopidyMiddleware, spotifyMiddleware ) ); export default store; diff --git a/src/js/components/ConfirmationButton.js b/src/js/components/ConfirmationButton.js index 0d7bef12..920de246 100755 --- a/src/js/components/ConfirmationButton.js +++ b/src/js/components/ConfirmationButton.js @@ -7,6 +7,7 @@ export default class ConfirmationButton extends React.Component{ constructor(props) { super(props); this.state = { + timing_out: false, confirming: false } this.confirming = false; @@ -27,16 +28,18 @@ export default class ConfirmationButton extends React.Component{ } 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), - 1500 + 2000 ); } } @@ -49,6 +52,7 @@ export default class ConfirmationButton extends React.Component{ if( this.state.confirming ){ className += ' confirming'; content = this.props.confirmingContent; + if( this.state.timing_out ) className += ' timing-out'; } return ( diff --git a/src/js/components/SpotifyAuthenticationFrame.js b/src/js/components/SpotifyAuthenticationFrame.js index 867b02c0..78c94226 100755 --- a/src/js/components/SpotifyAuthenticationFrame.js +++ b/src/js/components/SpotifyAuthenticationFrame.js @@ -58,7 +58,7 @@ class SpotifyAuthenticationFrame extends React.Component{
Logged in as {this.props.spotify.me.display_name ? this.props.spotify.me.display_name : null } - { this.props.spotify.me.id } +  ({ this.props.spotify.me.id })
) } diff --git a/src/js/services/pusher/actions.js b/src/js/services/pusher/actions.js new file mode 100755 index 00000000..760659db --- /dev/null +++ b/src/js/services/pusher/actions.js @@ -0,0 +1,31 @@ + +/** + * Actions and Action Creators + **/ + +export function setConfig( config ){ + return { + type: 'PUSHER_SET_CONFIG', + config: config + } +} + +export function connect(){ + return { + type: 'PUSHER_CONNECT' + } +} + +export function disconnect(){ + return { + type: 'PUSHER_DISCONNECT' + } +} + +export function instruct( call, value ){ + return { + type: 'PUSHER_INSTRUCT', + call: call, + value: value + } +} \ No newline at end of file diff --git a/src/js/services/pusher/middleware.js b/src/js/services/pusher/middleware.js new file mode 100755 index 00000000..e7842058 --- /dev/null +++ b/src/js/services/pusher/middleware.js @@ -0,0 +1,58 @@ + +var actions = require('./actions.js') + +const PusherMiddleware = (function(){ + + // container for the actual Mopidy socket + var socket = null; + + // handle all manner of socket messages + const handleMessage = (ws, store, message) => { + switch( message.action ){ + default: + store.dispatch({ type: 'PUSHER_'+message.action.toUpperCase(), data: message.data }) + } + } + + /** + * Middleware + * + * This behaves like an action interceptor. We listen for specific actions + * and handle special functionality. If the action is not in our switch, then + * it just proceeds to the next middleware, or default functionality + **/ + return store => next => action => { + + switch(action.type) { + + case 'PUSHER_CONNECT': + + if(socket != null) socket.close(); + store.dispatch({ type: 'PUSHER_CONNECTING' }); + + var state = store.getState(); + + socket = new WebSocket( + 'ws://'+state.mopidy.host+':'+state.pusher.port+'/pusher' + ); + + socket.onopen = function(){ + store.dispatch({ type: 'PUSHER_CONNECTED' }); + }; + + socket.onmessage = function(message){ + var message = JSON.parse(message.data); + handleMessage( socket, store, message ) + }; + + break; + + // This action is irrelevant to us, pass it on to the next middleware + default: + return next(action); + } + } + +})(); + +export default PusherMiddleware \ No newline at end of file diff --git a/src/js/services/pusher/reducer.js b/src/js/services/pusher/reducer.js new file mode 100755 index 00000000..ce385195 --- /dev/null +++ b/src/js/services/pusher/reducer.js @@ -0,0 +1,23 @@ + +export default function reducer(pusher = {}, action){ + switch (action.type) { + + case 'PUSHER_CONNECTED': + return Object.assign({}, pusher, { connected: true, connecting: false }); + + case 'PUSHER_DISCONNECTED': + return Object.assign({}, pusher, { connected: false, connecting: false }); + + case 'PUSHER_SET_CONFIG': + return Object.assign({}, pusher, { + host: action.host, + port: action.port + }); + + default: + return pusher + } +} + + + diff --git a/src/js/views/App.js b/src/js/views/App.js index e7e3b31e..06261b16 100755 --- a/src/js/views/App.js +++ b/src/js/views/App.js @@ -9,6 +9,7 @@ import Sidebar from '../components/Sidebar' import ContextMenu from '../components/ContextMenu' import * as uiActions from '../services/ui/actions' +import * as pusherActions from '../services/pusher/actions' import * as mopidyActions from '../services/mopidy/actions' import * as spotifyActions from '../services/spotify/actions' @@ -25,7 +26,9 @@ class App extends React.Component{ } componentWillMount(){ + this.props.pusherActions.connect(); this.props.mopidyActions.connect(); + window.addEventListener("keyup", this.handleKeyUp, false); window.addEventListener("keydown", this.handleKeyDown, false); } @@ -94,6 +97,7 @@ const mapStateToProps = (state, ownProps) => { const mapDispatchToProps = (dispatch) => { return { uiActions: bindActionCreators(uiActions, dispatch), + pusherActions: bindActionCreators(pusherActions, dispatch), mopidyActions: bindActionCreators(mopidyActions, dispatch), spotifyActions: bindActionCreators(spotifyActions, dispatch) } diff --git a/src/js/views/Settings.js b/src/js/views/Settings.js index 9d06f146..7a9b5585 100755 --- a/src/js/views/Settings.js +++ b/src/js/views/Settings.js @@ -19,6 +19,7 @@ class Settings extends React.Component{ this.state = { mopidy_host: this.props.mopidy.host, mopidy_port: this.props.mopidy.port, + pusher_port: this.props.pusher.port, spotify_country: this.props.spotify.country, spotify_locale: this.props.spotify.locale }; @@ -38,6 +39,10 @@ class Settings extends React.Component{ this.props.spotifyActions.setConfig({ country: this.state.spotify_country, locale: this.state.spotify_locale }); } + setPusherConfig(){ + this.props.pusherActions.setConfig({ port: this.state.pusher_port }); + } + render(){ return (
@@ -64,7 +69,19 @@ class Settings extends React.Component{ onChange={ e => this.setState({ mopidy_port: e.target.value })} value={ this.state.mopidy_port } /> - + + + +

Pusher

+
this.setPusherConfig()}> + +

Spotify

@@ -83,7 +100,7 @@ class Settings extends React.Component{ onChange={ e => this.setState({ spotify_locale: e.target.value })} value={ this.state.spotify_locale } /> - + diff --git a/src/scss/global/_forms.scss b/src/scss/global/_forms.scss index 47dbe842..b3a2aadb 100755 --- a/src/scss/global/_forms.scss +++ b/src/scss/global/_forms.scss @@ -36,6 +36,14 @@ input[type="submit"] { } } + &.secondary { + background: #000000; + color: #FFFFFF; + &:hover { + background: lighten( #000000, 20% ); + } + } + &.confirming, &.destructive { background: $red; @@ -45,6 +53,20 @@ input[type="submit"] { } } + &.timing-out { + position: relative; + &::before { + @include animate_timeout(2s); + content: ''; + position: absolute; + top: 0; + left: 0; + bottom: 0; + width: 0; + background: rgba(255,255,255,0.15); + } + } + &[disabled], &[disabled="disabled"] { background: $light_grey !important; diff --git a/src/scss/global/_variables.scss b/src/scss/global/_variables.scss index 796332e9..21ebdb56 100755 --- a/src/scss/global/_variables.scss +++ b/src/scss/global/_variables.scss @@ -20,11 +20,22 @@ $red: #cf2d2d; } } -@mixin animate( $speed: 0.2s ){ - -webkit-transition: all $speed ease-in-out; - -moz-transition: all $speed ease-in-out; - -o-transition: all $speed ease-in-out; - transition: all $speed ease-in-out; +@mixin animate( $duration: 0.2s, $easing: ease-in-out ){ + -webkit-transition: all $duration $easing; + -moz-transition: all $duration $easing; + -o-transition: all $duration $easing; + transition: all $duration $easing; +} + +@keyframes fill_from_left { + from { width: 0%; } + to { width: 100%; } +} + +@mixin animate_timeout( $duration: 0.2s ){ + animation-name: fill_from_left; + animation-duration: $duration; + animation-timing-function: linear; } @mixin blur( $size: 10px ) {