Merge branch 'websocket'

This commit is contained in:
James Barnsley
2017-02-21 21:56:58 +13:00
28 changed files with 1026 additions and 1024 deletions

10
src/assets/icons/connection.svg Executable file
View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="16px" height="16px" viewBox="0 0 16 16" enable-background="new 0 0 16 16" xml:space="preserve">
<line fill="none" stroke="#000000" stroke-miterlimit="10" x1="5.25" y1="4.19" x2="5.25" y2="13.217"/>
<polyline points="2.716,6.29 5.25,3.065 7.784,6.29 "/>
<line fill="none" stroke="#000000" stroke-miterlimit="10" x1="10.75" y1="12.092" x2="10.75" y2="3.065"/>
<polyline points="13.284,9.992 10.75,13.217 8.216,9.992 "/>
</svg>

After

Width:  |  Height:  |  Size: 803 B

13
src/assets/icons/server.svg Executable file
View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="16px" height="16px" viewBox="0 0 16 16" enable-background="new 0 0 16 16" xml:space="preserve">
<ellipse fill="none" stroke="#000000" stroke-miterlimit="10" cx="8" cy="4.306" rx="4.438" ry="1.292"/>
<path fill="none" stroke="#000000" stroke-miterlimit="10" d="M12.438,6.915c0,0.714-1.986,1.292-4.438,1.292
S3.563,7.629,3.563,6.915"/>
<path fill="none" stroke="#000000" stroke-miterlimit="10" d="M12.438,9.523c0,0.714-1.986,1.292-4.438,1.292
s-4.438-0.578-4.438-1.292"/>
<path fill="none" stroke="#000000" stroke-miterlimit="10" d="M3.563,4.306v7.825c0,0.713,1.986,1.292,4.438,1.292
s4.438-0.579,4.438-1.292V4.306"/>
</svg>

After

Width:  |  Height:  |  Size: 1007 B

30
src/assets/icons/websocket.svg Executable file
View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="16px" height="16px" viewBox="0 0 16 16" enable-background="new 0 0 16 16" xml:space="preserve">
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.376,13.476c-0.57-0.571-1.143-1.142-1.714-1.717
c-0.055-0.055-0.114-0.063-0.185-0.062c-0.467,0-0.933,0-1.401,0c-1.578,0-3.156,0-4.735,0c-0.068,0-0.131,0.005-0.188-0.054
c-0.466-0.471-0.935-0.938-1.404-1.404c-0.051-0.051-0.047-0.078,0.001-0.124c0.168-0.164,0.336-0.326,0.496-0.496
c0.074-0.082,0.113-0.063,0.18,0.006c0.354,0.36,0.716,0.717,1.07,1.078c0.064,0.062,0.125,0.092,0.217,0.092
c0.774-0.005,1.549-0.003,2.323-0.004c0.044,0,0.088,0.017,0.132-0.023c-0.035-0.038-0.07-0.077-0.109-0.115
C9.285,9.879,8.512,9.101,7.732,8.33c-0.081-0.082-0.079-0.125,0.003-0.2C7.889,7.988,8.039,7.84,8.178,7.684
c0.083-0.09,0.125-0.088,0.212-0.001c0.77,0.779,1.547,1.553,2.323,2.327c0.034,0.035,0.061,0.087,0.133,0.097
c0-0.048,0-0.091,0-0.134c0-0.769-0.002-1.538,0.002-2.308c0-0.096-0.03-0.162-0.099-0.228c-0.35-0.345-0.692-0.698-1.047-1.04
C9.613,6.31,9.595,6.258,9.699,6.165c0.172-0.157,0.328-0.332,0.491-0.498c0.07-0.036,0.093,0.03,0.128,0.064
c0.444,0.442,0.887,0.889,1.333,1.329c0.072,0.072,0.103,0.141,0.103,0.243c-0.004,1.114-0.002,2.229-0.006,3.344
c0,0.109,0.02,0.152,0.144,0.149c0.52-0.008,1.041-0.007,1.562,0c0.117,0.002,0.144-0.034,0.144-0.146
c-0.003-1.378-0.002-2.756-0.002-4.133c0-0.085-0.022-0.145-0.083-0.205C12.88,5.683,12.25,5.054,11.623,4.42
c-0.075-0.076-0.113-0.083-0.192-0.001c-0.365,0.375-0.738,0.745-1.112,1.116c-0.035,0.033-0.058,0.1-0.128,0.063
c-0.009-0.012-0.016-0.025-0.026-0.035C9.152,4.551,8.138,3.54,7.127,2.526C7.067,2.467,7.005,2.459,6.93,2.459
C4.904,2.46,2.877,2.46,0.851,2.461c-0.044,0-0.096-0.021-0.142,0.024C0.743,2.522,0.775,2.559,0.81,2.592
c0.537,0.538,1.076,1.074,1.61,1.614c0.072,0.072,0.142,0.1,0.242,0.098c1.169-0.002,2.337-0.001,3.507-0.001
c0.074,0,0.138,0.004,0.197,0.065c0.393,0.399,0.788,0.795,1.188,1.188c0.071,0.068,0.051,0.101-0.007,0.158
C6.95,6.309,6.354,6.904,5.76,7.502c-0.063,0.064-0.096,0.065-0.159,0c-0.39-0.396-0.783-0.789-1.178-1.178
C4.365,6.268,4.345,6.211,4.346,6.132c0.003-0.268-0.002-0.536,0.003-0.804c0.002-0.096-0.024-0.123-0.121-0.122
C3.697,5.21,3.166,5.212,2.635,5.205C2.518,5.204,2.503,5.246,2.503,5.348c0.004,0.512,0.004,1.023,0,1.535
c0,0.095,0.027,0.162,0.095,0.229c0.991,0.986,1.977,1.979,2.969,2.962c0.096,0.094,0.082,0.137-0.005,0.223
c-0.366,0.356-0.722,0.723-1.088,1.08c-0.078,0.076-0.079,0.114,0,0.191c0.63,0.623,1.257,1.252,1.881,1.881
c0.067,0.067,0.134,0.093,0.229,0.093c2.893-0.001,5.789-0.001,8.682-0.002c0.047,0,0.095,0.017,0.146-0.018
C15.397,13.505,15.389,13.489,15.376,13.476z"/>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

2
src/js/bootstrap.js vendored
View File

@ -37,7 +37,7 @@ var initialState = {
connected: false,
port: 6681,
username: 'Anonymous',
connections: [],
connections: {},
version: {
current: '0.0.0'
}

View File

@ -15,27 +15,34 @@ class SendAuthorizationModal extends React.Component{
super(props)
}
handleClick(e, connectionid){
handleClick(e, connection_id){
e.preventDefault()
this.props.pusherActions.sendAuthorization( connectionid, this.props.authorization, this.props.me )
this.props.pusherActions.sendAuthorization( connection_id, this.props.authorization, this.props.me )
this.props.uiActions.closeModal()
return false;
}
render(){
var connections = []
for (var connection_id in this.props.connections){
if (this.props.connections.hasOwnProperty(connection_id)) {
connections.push(this.props.connections[connection_id])
}
}
return (
<div>
<h4 className="no-bottom-padding">Share Spotify authentication</h4>
<h3 className="grey-text">Send your authentication tokens to another client. When the recipient client imports this, their Iris will have full access to your Spotify account ({this.props.me.id}).</h3>
<div className="list small pusher-connection-list">
{
this.props.connections.map( (connection, index) => {
connections.map( (connection, index) => {
// don't list OUR connection
if (connection.connectionid == this.props.connectionid) return null
if (connection.connection_id == this.props.connection_id) return null
return (
<div className='list-item connection' key={connection.connectionid} onClick={ e => this.handleClick(e, connection.connectionid) }>
<div className='list-item connection' key={connection.connection_id} onClick={ e => this.handleClick(e, connection.connection_id) }>
{ connection.username }
&nbsp;
<span className="grey-text">({ connection.ip })</span>
@ -53,7 +60,7 @@ const mapStateToProps = (state, ownProps) => {
return {
me: state.spotify.me,
authorization: state.spotify.authorization,
connectionid: state.pusher.connectionid,
connection_id: state.pusher.connection_id,
connections: state.pusher.connections
}
}

View File

@ -28,20 +28,28 @@ class PusherConnectionList extends React.Component{
render(){
if( !this.props.connected ) return <div className="pusher-connection-list grey-text">Not connected</div>
if( typeof(this.props.connections) == 'undefined' || this.props.connections.length <= 0 ) return <div className="pusher-connection-list grey-text">No connections</div>;
var connections = []
for (var connection_id in this.props.connections){
if (this.props.connections.hasOwnProperty(connection_id)) {
connections.push(this.props.connections[connection_id])
}
}
if (connections.length <= 0) return <div className="pusher-connection-list grey-text">No connections</div>;
return (
<div className="pusher-connection-list">
{
this.props.connections.map( (connection, index) => {
connections.map( (connection, index) => {
var is_me = false;
if( connection.connectionid == this.props.connectionid ) is_me = true;
if( connection.connection_id == this.props.connection_id ) is_me = true;
return (
<div className={ is_me ? 'connection cf me': 'connection cf'} key={connection.connectionid}>
<div className={ is_me ? 'connection cf me': 'connection cf'} key={connection.connection_id}>
<div className="col w30">{ connection.username } { is_me ? <span>(you)</span> : null }</div>
<div className="col w70">
{ connection.ip }
<span className="grey-text"> ({ connection.connectionid })</span>
<span className="grey-text"> ({ connection.connection_id })</span>
</div>
</div>
);
@ -55,7 +63,7 @@ class PusherConnectionList extends React.Component{
const mapStateToProps = (state, ownProps) => {
return {
connected: state.pusher.connected,
connectionid: state.pusher.connectionid,
connection_id: state.pusher.connection_id,
connections: state.pusher.connections
}
}

View File

@ -23,8 +23,7 @@ const localstorageMiddleware = (function(){
if( !pusher ) pusher = {};
Object.assign(
pusher,{
username: action.connection.username,
connectionid: action.connection.connectionid
connection_id: action.connection_id
}
);
localStorage.setItem('pusher', JSON.stringify(pusher));
@ -37,9 +36,9 @@ const localstorageMiddleware = (function(){
localStorage.setItem('pusher', JSON.stringify(pusher));
break;
case 'PUSHER_USERNAME':
case 'PUSHER_USERNAME_CHANGED':
var stored_pusher = JSON.parse( localStorage.getItem('pusher') )
var pusher = Object.assign({}, stored_pusher, { username: action.data.username })
var pusher = Object.assign({}, stored_pusher, { username: action.username })
localStorage.setItem('pusher', JSON.stringify(pusher))
break;

View File

@ -37,7 +37,7 @@ export function startUpgrade(){
export function getConnections(){
return {
type: 'GET_CONNECTIONS'
type: 'PUSHER_GET_CONNECTIONS'
}
}
@ -50,30 +50,33 @@ export function instruct( data = null ){
export function sendAuthorization( recipient_connectionid, authorization, me ){
return {
type: 'PUSHER_SEND_AUTHORIZATION',
recipient_connectionid: recipient_connectionid,
authorization: authorization,
me: me
type: 'PUSHER_DELIVER_MESSAGE',
to: recipient_connectionid,
message: {
type: 'spotify_authorization',
authorization: authorization,
me: me
}
}
}
export function startRadio( uris ){
return {
type: 'START_RADIO',
type: 'PUSHER_START_RADIO',
uris: uris
}
}
export function stopRadio(){
return {
type: 'STOP_RADIO'
type: 'PUSHER_STOP_RADIO'
}
}
export function debug( data = null ){
export function debug( message = null ){
return {
type: 'PUSHER_DEBUG',
data: data
message: message
}
}

View File

@ -23,66 +23,43 @@ const PusherMiddleware = (function(){
// handle all manner of socket messages
const handleMessage = (ws, store, message) => {
//console.log('handleMessage', message)
// response to a request [we] made
if (typeof(message.request_id) !== 'undefined' && message.request_id){
if (typeof( deferredRequests[ message.request_id ]) !== 'undefined' ){
deferredRequests[ message.request_id ].resolve( message )
} else {
console.error('Pusher: Response with no matching request', message);
}
switch (message.action){
case 'response':
if (typeof( deferredRequests[ message.request_id ]) !== 'undefined' ){
deferredRequests[ message.request_id ].resolve( message )
} else {
console.error('Pusher: Response with no matching request', message);
}
break
case 'broadcast':
if (message.type ){
var type = message.type.toUpperCase()
} else if (message.data.type ){
var type = message.data.type.toUpperCase()
} else {
var type = 'UNRECOGNISED_BROADCAST'
}
store.dispatch({ type: type, data: message.data })
break
// general message
// this can be client-client, server-client or a broadcast to many clients
} else {
message.type = 'PUSHER_'+message.type.toUpperCase()
store.dispatch(message)
}
}
const request = (data) => {
const request = (method, data = {}) => {
return new Promise( (resolve, reject) => {
// send the payload
data.request_id = helpers.generateGuid()
socket.send( JSON.stringify(data) )
var request_id = helpers.generateGuid()
var message = {
method: method,
data: data,
request_id: request_id
}
socket.send( JSON.stringify(message) )
// add query to our deferred responses
deferredRequests[ data.request_id ] = {
deferredRequests[request_id] = {
resolve: resolve,
reject: reject
}
})
}
/**
* 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_INSTRUCT':
request( action.data )
.then(
response => {
store.dispatch({ type: 'PUSHER_INSTRUCT', data: response.data })
}
)
break
case 'PUSHER_CONNECT':
if(socket != null) socket.close();
@ -91,7 +68,7 @@ const PusherMiddleware = (function(){
var state = store.getState();
var connection = {
clientid: helpers.generateGuid(),
connectionid: helpers.generateGuid(),
connection_id: helpers.generateGuid(),
username: 'Anonymous'
}
if( state.pusher.username ) connection.username = state.pusher.username;
@ -99,60 +76,68 @@ const PusherMiddleware = (function(){
socket = new WebSocket(
'ws://'+state.mopidy.host+':'+state.mopidy.port+'/iris/ws',
[ connection.clientid, connection.connectionid, connection.username ]
[ connection.clientid, connection.connection_id, connection.username ]
);
socket.onopen = () => {
store.dispatch({ type: 'PUSHER_CONNECTED', connection: connection });
store.dispatch({ type: 'PUSHER_SET_USERNAME', username: connection.username });
};
socket.onmessage = (message) => {
var message = JSON.parse(message.data);
handleMessage( socket, store, message )
};
socket.onclose = () => {
store.dispatch({
type: 'PUSHER_DISCONNECTED'
})
// attempt to reconnect ever 5 seconds
setTimeout(() => {
store.dispatch(pusherActions.connect())
}, 5000);
};
break;
case 'PUSHER_CONNECTED':
request({ action: 'get_config' })
request('get_config')
.then(
response => {
if (response.data.error){
console.error(response.data.error)
if (response.error){
console.error(response.error)
return false
}
store.dispatch({ type: 'CONFIG', config: response.data.config })
if (response.data.config.spotify_username){
store.dispatch(spotifyActions.getUser('spotify:user:'+response.data.config.spotify_username))
response.type = 'PUSHER_CONFIG'
store.dispatch(response)
if (response.config.spotify_username){
store.dispatch(spotifyActions.getUser('spotify:user:'+response.config.spotify_username))
}
var spotify = store.getState().spotify
if (!spotify.country || !spotify.locale){
store.dispatch({ type: 'SPOTIFY_SET_CONFIG', config: response.data.config })
store.dispatch({ type: 'SPOTIFY_SET_CONFIG', config: response.config })
}
}
)
request({ action: 'get_version' })
request('get_version')
.then(
response => {
if (response.data.error){
console.error(response.data.error)
if (response.error){
console.error(response.error)
return false
}
store.dispatch({ type: 'VERSION', data: response.data })
response.type = 'PUSHER_VERSION'
store.dispatch(response)
}
)
request({ action: 'get_radio' })
request('get_radio')
.then(
response => {
if (response.data.error){
console.error(response.data.error)
if (response.error){
console.error(response.error)
return false
}
store.dispatch({ type: 'RADIO', data: response.data })
response.type = 'PUSHER_RADIO'
store.dispatch(response)
}
)
@ -161,145 +146,123 @@ const PusherMiddleware = (function(){
return next(action);
break;
case 'ERROR':
store.dispatch( uiActions.createNotification(action.data.source+': '+action.data.message,'bad') )
break;
case 'PUSHER_GET_QUEUE_METADATA':
request({ action: 'get_queue_metadata'})
case 'PUSHER_INSTRUCT':
request( action )
.then(
response => {
store.dispatch({ type: 'QUEUE_METADATA', data: response.data })
store.dispatch({ type: 'PUSHER_INSTRUCT', data: response.data })
}
)
break
case 'PUSHER_DELIVER_MESSAGE':
request('deliver_message', action)
.then(
response => {
store.dispatch( uiActions.createNotification('Message delivered') )
}
)
break
case 'PUSHER_GET_QUEUE_METADATA':
request('get_queue_metadata')
.then(
response => {
response.type = 'QUEUE_METADATA'
store.dispatch(response)
}
)
break;
case 'PUSHER_ADD_QUEUE_METADATA':
request({ action: 'add_queue_metadata', tlids: action.tlids, added_from: action.from_uri })
request('add_queue_metadata', {
tlids: action.tlids,
added_from: action.from_uri,
added_by: store.getState().pusher.username
})
break;
case 'START_UPGRADE':
request({ action: 'upgrade' })
.then(
response => {
if (response.data.error){
console.error(response.data.error)
return false
}
if (response.data.upgrade_successful){
store.dispatch( uiActions.createNotification('Upgrade complete') )
}else{
store.dispatch( uiActions.createNotification('Upgrade failed, please upgrade manually','bad') )
}
store.dispatch({ type: 'VERSION', data: response.data })
case 'PUSHER_START_UPGRADE':
request('upgrade')
.then(
response => {
if (response.error){
console.error(response.error)
return false
}
)
if (response.upgrade_successful){
store.dispatch( uiActions.createNotification('Upgrade complete') )
}else{
store.dispatch( uiActions.createNotification('Upgrade failed, please upgrade manually','bad') )
}
response.type = 'PUSHER_VERSION'
store.dispatch(response)
}
)
return next(action);
break;
case 'PUSHER_SET_USERNAME':
request({ action: 'set_username', username: action.username })
.then(
response => {
if (response.data.error){
console.error(response.data.error)
return false
}
store.dispatch({ type: 'PUSHER_USERNAME', data: { username: response.data.username }})
request('set_username', {
username: action.username
})
.then(
response => {
if (response.error){
console.error(response.error)
return false
}
)
response.type = 'PUSHER_USERNAME_CHANGED'
store.dispatch(response)
}
)
return next(action);
break;
case 'GET_CONNECTIONS':
case 'NEW_CONNECTION':
request({ action: 'get_connections' })
.then(
response => {
if (response.data.error){
console.error(response.data.error)
return false
}
store.dispatch({ type: 'CONNECTIONS', data: response.data })
case 'PUSHER_GET_CONNECTIONS':
request('get_connections')
.then(
response => {
if (response.error){
console.error(response.error)
return false
}
)
response.type = 'PUSHER_CONNECTIONS'
store.dispatch(response)
}
)
return next(action);
break
case 'PUSHER_DEBUG':
request( action.data )
.then(
response => {
if (response.data.error){
console.error(response.data.error)
return false
}
store.dispatch({ type: 'DEBUG', response: response.data })
}
)
break;
case 'PUSHER_SEND_AUTHORIZATION':
request({
action: 'send_authorization',
recipient_connectionid: action.recipient_connectionid,
authorization: action.authorization,
me: action.me
})
request( action.message.method, action.message.data )
.then(
response => {
if (response.data.error){
console.error(response.data.error)
return false
}
store.dispatch( uiActions.createNotification('Authorization sent') )
response => {
store.dispatch({type: 'DEBUG', response: response})
}
)
break;
case 'PUSHER_SEND_AUTHORIZATION':
if( window.confirm('Spotify authorization for user '+action.data.me.id+' received. Do you want to import?') ){
case 'PUSHER_SPOTIFY_AUTHORIZATION':
if( window.confirm('Spotify authorization for user '+action.me.id+' received. Do you want to import?') ){
// remove any existing authentication
store.dispatch({ type: 'SPOTIFY_AUTHORIZATION_REVOKED' })
// import our new authentication
store.dispatch({ type: 'SPOTIFY_ME_LOADED', data: action.data.me })
store.dispatch({ type: 'SPOTIFY_AUTHORIZATION_GRANTED', data: action.data.authorization })
store.dispatch({ type: 'SPOTIFY_ME_LOADED', data: action.me })
store.dispatch({ type: 'SPOTIFY_AUTHORIZATION_GRANTED', data: action.authorization })
}else{
console.log('Authorization ignored')
}
break
case 'START_RADIO':
request({
action: 'broadcast',
data: {
type: 'browser_notification',
title: 'Radio started',
body: store.getState().pusher.username +' started radio mode',
icon: ''
}
})
.then(
response => {
if (response.data.error){
console.error(response.data.error)
return false
}
store.dispatch(uiActions.createNotification('Starting radio...'))
}
)
case 'PUSHER_START_RADIO':
store.dispatch(uiActions.createNotification('Starting radio...'))
var data = {
action: 'start_radio',
seed_artists: [],
seed_genres: [],
seed_tracks: []
@ -319,38 +282,27 @@ const PusherMiddleware = (function(){
}
}
request( data )
request( 'start_radio', data )
.then(response => {
console.log(response)
})
break
case 'STOP_RADIO':
request({
action: 'broadcast',
data: {
type: 'browser_notification',
title: 'Radio stopped',
body: store.getState().pusher.username +' stopped radio mode',
icon: ''
}
})
.then(
response => {
if (response.data.error){
console.error(response.data.error)
return false
}
store.dispatch(uiActions.createNotification('Stopping radio'))
}
)
case 'PUSHER_STOP_RADIO':
store.dispatch(uiActions.createNotification('Stopping radio'))
var data = {
action: 'stop_radio',
seed_artists: [],
seed_genres: [],
seed_tracks: []
}
request( data )
// we don't need to wait for request, as change will be broadcast
request( 'stop_radio', data )
break
case 'PUSHER_BROWSER_NOTIFICATION':
store.dispatch(uiActions.createBrowserNotification(action))
break
// This action is irrelevant to us, pass it on to the next middleware

View File

@ -7,11 +7,11 @@ export default function reducer(pusher = {}, action){
return Object.assign({}, pusher, { connected: false, connecting: true });
case 'PUSHER_CONNECTED':
return Object.assign({}, pusher, {
connected: true,
connecting: false,
connectionid: action.connection.connectionid,
username: action.connection.username
return Object.assign({}, pusher, {
connected: true,
connecting: false,
connection_id: action.connection_id,
username: action.username
});
case 'PUSHER_DISCONNECTED':
@ -20,30 +20,34 @@ export default function reducer(pusher = {}, action){
case 'PUSHER_SET_PORT':
return Object.assign({}, pusher, { port: action.port });
case 'PUSHER_USERNAME':
return Object.assign({}, pusher, { username: action.data.username });
case 'PUSHER_USERNAME_CHANGED':
return Object.assign({}, pusher, { username: action.username });
case 'CONNECTIONS':
return Object.assign({}, pusher, { connections: action.data.connections });
case 'CONNECTION_UPDATED':
function byID(connection){
return connection.connectionid == action.data.connection.connectionid;
case 'PUSHER_CONNECTIONS':
var connections = {}
for (var i = 0; i < action.connections.length; i++){
connections[action.connections[i].connection_id] = action.connections[i]
}
var connection = pusher.connections.find(byID);
var index = pusher.connections.indexOf(connection);
var connections = Object.assign([], pusher.connections);
connections[index] = action.data.connection;
return Object.assign({}, pusher, { connections: connections });
case 'VERSION':
case 'PUSHER_CONNECTION_ADDED':
case 'PUSHER_CONNECTION_UPDATED':
var connections = Object.assign({}, pusher.connections)
connections[action.connection.connection_id] = action.connection
return Object.assign({}, pusher, { connections: connections });
case 'PUSHER_CONNECTION_REMOVED':
var connections = Object.assign({}, pusher.connections)
delete connections[action.connection.connection_id]
return Object.assign({}, pusher, { connections: connections });
case 'PUSHER_VERSION':
return Object.assign({}, pusher, {
version: action.data.version,
version: action.version,
upgrading: false
});
case 'START_UPGRADE':
case 'PUSHER_START_UPGRADE':
return Object.assign({}, pusher, { upgrading: true });
default:

View File

@ -119,14 +119,15 @@ function refreshToken( dispatch, getState ){
})
.then(
response => {
response.token_expiry = new Date().getTime() + ( response.expires_in * 1000 );
response.source = 'mopidy';
var token = response.spotify_token
token.token_expiry = new Date().getTime() + ( token.expires_in * 1000 );
token.source = 'mopidy';
dispatch({
type: 'SPOTIFY_TOKEN_REFRESHED',
provider: 'mopidy-spotify',
data: response
data: token
});
resolve(response);
resolve(token);
},
error => {
dispatch({ type: 'SPOTIFY_DISCONNECTED' })

View File

@ -87,13 +87,14 @@ const SpotifyMiddleware = (function(){
break
// when radio returns
case 'RADIO':
case 'PUSHER_RADIO_STARTED':
case 'PUSHER_RADIO_CHANGED':
next(action)
// only resolve if radio is enabled
if( action.data.radio.enabled ){
store.dispatch(spotifyActions.resolveRadioSeeds(action.data.radio))
if( action.radio.enabled ){
store.dispatch(spotifyActions.resolveRadioSeeds(action.radio))
}
break

View File

@ -215,6 +215,13 @@ export function closeModal(){
* Subtle info/tooltip messages
**/
export function createBrowserNotification( data ){
return {
type: 'BROWSER_NOTIFICATION',
data: data
}
}
export function createNotification( content, type = 'default' ){
return {
type: 'CREATE_NOTIFICATION',

View File

@ -21,7 +21,7 @@ const UIMiddleware = (function(){
break
case 'PUSHER_CONNECTED':
ReactGA.event({ category: 'Pusher', action: 'Connected', label: action.connection.username })
ReactGA.event({ category: 'Pusher', action: 'Connected', label: action.connection_id+'/'+action.username })
next(action)
break
@ -210,8 +210,8 @@ const UIMiddleware = (function(){
break
case 'VERSION':
if( action.data.version.upgrade_available )
store.dispatch( uiActions.createNotification( 'Version '+action.data.version.latest+' is available. See settings to upgrade.' ) )
if( action.version.upgrade_available )
store.dispatch( uiActions.createNotification( 'Version '+action.version.latest+' is available. See settings to upgrade.' ) )
next( action )
break

View File

@ -13,7 +13,7 @@ export default function reducer(ui = {}, action){
case 'UI_SET':
return Object.assign({}, ui, action.data)
case 'CONFIG':
case 'PUSHER_CONFIG':
return Object.assign({}, ui, { config: action.config })
case 'TOGGLE_SIDEBAR':
@ -132,23 +132,26 @@ export default function reducer(ui = {}, action){
});
case 'QUEUE_METADATA':
case 'QUEUE_METADATA_CHANGED':
var tracklist = Object.assign([], ui.current_tracklist)
for( var i = 0; i < tracklist.length; i++ ){
// load our metadata (if we have any for that tlid)
if (typeof(action.data.queue_metadata['tlid_'+tracklist[i].tlid]) !== 'undefined'){
if (typeof(action.queue_metadata['tlid_'+tracklist[i].tlid]) !== 'undefined'){
tracklist[i] = Object.assign(
{},
tracklist[i],
action.data.queue_metadata['tlid_'+tracklist[i].tlid],
action.queue_metadata['tlid_'+tracklist[i].tlid],
)
}
}
return Object.assign({}, ui, { current_tracklist: tracklist, queue_metadata: action.data.queue_metadata });
return Object.assign({}, ui, { current_tracklist: tracklist, queue_metadata: action.queue_metadata });
case 'RADIO':
case 'START_RADIO':
return Object.assign({}, ui, { seeds_resolved: false }, { radio: action.data.radio })
case 'PUSHER_RADIO':
case 'PUSHER_RADIO_STARTED':
case 'PUSHER_RADIO_CHANGED':
case 'PUSHER_RADIO_STOPPED':
return Object.assign({}, ui, { seeds_resolved: false }, { radio: action.radio })
case 'RADIO_SEEDS_RESOLVED':
var radio = Object.assign({}, ui.radio, { resolved_seeds: action.resolved_seeds })
@ -370,7 +373,7 @@ export default function reducer(ui = {}, action){
case 'USER_PLAYLISTS_LOADED':
var users = Object.assign([], ui.users)
var playlists_uris = []
if (users[action.key].playlists_uris) playlists_uris = users[action.key].playlists_uris
if (users[action.key] && users[action.key].playlists_uris) playlists_uris = users[action.key].playlists_uris
var artist = Object.assign(
{},

View File

@ -25,22 +25,7 @@ class Debug extends React.Component{
this.state = {
mopidy_call: 'playlists.asList',
mopidy_data: '{}',
pusher_data: '{}'
}
}
componentDidMount(){
if( this.props.connectionid ){
var data = {
action: "broadcast",
data: {
type: 'browser_notification',
title: "Title",
body: "Test notification",
icon: "http://lorempixel.com/100/100/nature/"
}
}
this.setState({ pusher_data: JSON.stringify(data) })
pusher_data: '{"method":"broadcast","data":{"type":"browser_notification","title":"Testing","body":"This is my message"}}'
}
}

View File

@ -11,6 +11,7 @@ import PusherConnectionList from '../components/PusherConnectionList'
import URISchemesList from '../components/URISchemesList'
import VersionManager from '../components/VersionManager'
import Header from '../components/Header'
import Icon from '../components/Icon'
import Thumbnail from '../components/Thumbnail'
import * as uiActions from '../services/ui/actions'
@ -56,25 +57,33 @@ class Settings extends React.Component{
}
renderConnectionStatus(service){
if( this.props[service].connected ){
if (service == 'Spotify' && !this.props[service.toLowerCase()].authorized){
return (
<span className="text green-text connection-status">
<FontAwesome name="check" />
<span className="red-text connection-status">
<FontAwesome name="exclamation-triangle" />
&nbsp;
Connected
Not connected
</span>
)
}else if( this.props[service].connecting ){
} else if (this.props[service.toLowerCase()].connected){
return (
<span className="text grey-text connection-status">
<span className="green-text connection-status">
<FontAwesome name="check" />
&nbsp;
{service}
</span>
)
} else if (this.props[service.toLowerCase()].connecting){
return (
<span className="grey-text connection-status">
<FontAwesome name="circle-o-notch" spin />
&nbsp;
Connecting
</span>
)
}else{
} else {
return (
<span className="text red-text connection-status">
<span className="red-text connection-status">
<FontAwesome name="exclamation-triangle" />
&nbsp;
Not connected
@ -99,7 +108,7 @@ class Settings extends React.Component{
<Link className="user" to={global.baseURL+'user/'+user.uri}>
<Thumbnail circle={true} size="small" images={user.images} />
<span className="user-name">
{user.display_name ? user.display_name : user.username}
{user.display_name ? user.display_name : user.id}
{!this.props.spotify.authorized ? <span className="grey-text">&nbsp;(limited access)</span> : null}
</span>
</Link>
@ -127,6 +136,31 @@ class Settings extends React.Component{
)
}
renderApplyButton(){
if (this.props.mopidy.host == this.state.mopidy_host && this.props.mopidy.port == this.state.mopidy_port) return null
return (
<div className="field">
<div className="name"></div>
<div className="input">
<button type="submit" className="secondary">Apply and reload</button>
</div>
</div>
)
}
serviceStatus(service){
if (this.props[service].connecting){
return <span className="grey-text">Connecting</span>
} else if (service == 'spotify' && !this.props[service].authorized){
return <span className="orange-text">Not authorized</span>
} else if (this.props[service].connected){
return <span className="green-text">Connected</span>
} else {
return <span className="red-text">Disconnected</span>
}
}
render(){
var actions = (
@ -142,14 +176,30 @@ class Settings extends React.Component{
<section>
<h4 className="underline">Mopidy</h4>
<form onSubmit={(e) => this.setMopidyConfig(e)}>
<div className="field">
<div className="name">Status</div>
<div className="input">
{ this.renderConnectionStatus('mopidy') }
</div>
<div className="status">
<div className="item">
<Icon name="server" />
<h4>Mopidy</h4>
{this.serviceStatus('mopidy')}
</div>
<div className="item">
<Icon name="connection" />
<h4>Pusher</h4>
{this.serviceStatus('pusher')}
</div>
<div className="item">
<Icon name="spotify" />
<h4>Spotify</h4>
{this.serviceStatus('spotify')}
</div>
</div>
<h4 className="underline">System</h4>
<form onSubmit={(e) => this.setMopidyConfig(e)}>
<div className="field">
<div className="name">Username</div>
<div className="input">
@ -177,22 +227,11 @@ class Settings extends React.Component{
onChange={ e => this.setState({ mopidy_port: e.target.value })}
value={ this.state.mopidy_port } />
</div>
</div>
<div className="field">
<div className="name"></div>
<div className="input">
<button type="submit" className="secondary">Apply</button>
</div>
</div>
{this.renderApplyButton()}
</form>
<h4 className="underline">Spotify</h4>
<div className="field">
<div className="name">Status</div>
<div className="input">
{ this.renderConnectionStatus('spotify') }
</div>
</div>
<form>
<div className="field">
<div className="name">Country</div>

View File

@ -45,5 +45,34 @@ main {
}
}
}
@include responsive( $bp_medium ){
.dropdown-field {
.label {
padding-left: 16px;
padding-right: 16px;
.text {
display: none;
}
.fa {
line-height: 24px;
}
}
}
.actions {
button {
font-size: 1px;
color: transparent;
vertical-align: top;
.fa {
font-size: 14px;
color: #000000;
vertical-align: middle;
}
}
}
}
}
}

View File

@ -167,6 +167,7 @@ h4 {
.grey-text { color: $mid_grey !important; }
.red-text { color: $red !important; }
.green-text { color: $green !important; }
.orange-text { color: $orange !important; }
.dark-text { color: $dark_grey !important; }
.centred-text { text-align: center; }

View File

@ -11,6 +11,7 @@ $red: #cf2d2d;
$green: #47af2a;
$blue: #32b5f2;
$yellow: #FFF39C;
$orange: #f16f19;
$bp_wide: 1000px;
$bp_medium: 800px;

View File

@ -3,6 +3,29 @@
section {
padding: 40px;
}
.status {
.item {
position: relative;
display: inline-block;
padding: 0 0 0 45px;
margin-right: 20px;
min-width: 80px;
.icon {
width: 35px;
height: 35px;
position: absolute;
top: 0;
left: 0;
}
h4 {
padding: 0;
margin: 0;
}
}
}
.field.current-user {
.text {