Pusher part one

This commit is contained in:
James Barnsley
2016-10-27 16:58:11 +13:00
parent c4fcf43b3c
commit 6de44cda53
10 changed files with 192 additions and 10 deletions

14
src/js/bootstrap.js vendored
View File

@ -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;

View File

@ -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 (

View File

@ -58,7 +58,7 @@ class SpotifyAuthenticationFrame extends React.Component{
<div>
<Thumbnail circle={true} size="small" images={this.props.spotify.me.images} />
Logged in as {this.props.spotify.me.display_name ? this.props.spotify.me.display_name : null }
<Link to={'/user/'+this.props.spotify.me.id}>{ this.props.spotify.me.id }</Link>
&nbsp;(<Link to={'/user/'+this.props.spotify.me.id}>{ this.props.spotify.me.id }</Link>)
</div>
)
}

View File

@ -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
}
}

View File

@ -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

View File

@ -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
}
}

View File

@ -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)
}

View File

@ -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 (
<div className="view settings-view">
@ -64,7 +69,19 @@ class Settings extends React.Component{
onChange={ e => this.setState({ mopidy_port: e.target.value })}
value={ this.state.mopidy_port } />
</label>
<button type="submit">Apply</button>
<button type="submit" className="secondary">Apply</button>
</form>
<h3 className="underline">Pusher</h3>
<form onSubmit={() => this.setPusherConfig()}>
<label>
<span className="label">Port</span>
<input
type="text"
onChange={ e => this.setState({ pusher_port: e.target.value })}
value={ this.state.pusher_port } />
</label>
<button type="submit" className="secondary">Apply</button>
</form>
<h3 className="underline">Spotify</h3>
@ -83,7 +100,7 @@ class Settings extends React.Component{
onChange={ e => this.setState({ spotify_locale: e.target.value })}
value={ this.state.spotify_locale } />
</label>
<button type="submit">Apply</button>
<button type="submit" className="secondary">Apply</button>
</form>
<SpotifyAuthenticationFrame />

View File

@ -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;

View File

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