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
+
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 ) {