Merge branch 'master' of github.com:jaedb/Iris

This commit is contained in:
James Barnsley
2017-02-07 07:27:40 +13:00
24 changed files with 194 additions and 101 deletions

View File

@ -15,7 +15,7 @@ __version__ = '2.11.3'
#
# Loads config and gets the party started. Initiates any additional frontends, etc.
##
class IrisExtension( ext.Extension ):
class Extension( ext.Extension ):
dist_name = 'Mopidy-Iris'
ext_name = 'iris'
@ -26,9 +26,11 @@ class IrisExtension( ext.Extension ):
return config.read(conf_file)
def get_config_schema(self):
schema = super(IrisExtension, self).get_config_schema()
schema['debug'] = config.Boolean()
schema = config.ConfigSchema(self.ext_name)
schema['enabled'] = config.Boolean()
schema['pusherport'] = config.String()
schema['country'] = config.String()
schema['locale'] = config.String()
return schema
def setup(self, registry):

View File

@ -1,4 +1,5 @@
[iris]
enabled = true
debug = false
pusherport = 6681
pusherport = 6681
country = NZ
locale = en_NZ

View File

@ -189,6 +189,17 @@ class IrisFrontend(pykka.ThreadingActor, CoreListener):
def get_spotify_token( self ):
return self.spotify_token
# get our config values
def get_config( self ):
all_config = self.config
config = {
"spotify_username": all_config['spotify']['username'],
"country": all_config['iris']['country'],
"locale": all_config['iris']['locale']
}
return config
##
# Get Spotmop version, and check for updates

View File

@ -188,6 +188,15 @@ class PusherWebsocketHandler(tornado.websocket.WebSocketHandler):
{ 'error': 'Could not send to that connection, does not exist' }
)
# fetch our pusher connections
elif messageJson['action'] == 'get_config':
send_message(
self.connectionid,
'response',
messageJson['request_id'],
{ 'config': self.frontend.get_config() }
)
# fetch our pusher connections
elif messageJson['action'] == 'get_connections':
@ -266,7 +275,7 @@ class PusherWebsocketHandler(tornado.websocket.WebSocketHandler):
{ 'version': version }
)
# get system version and check for upgrade
# perform upgrade
elif messageJson['action'] == 'upgrade':
version = self.frontend.get_version()
upgrade_successful = self.frontend.perform_upgrade()
@ -290,7 +299,7 @@ class PusherWebsocketHandler(tornado.websocket.WebSocketHandler):
{ 'error': 'Unhandled action' }
)
logger.debug( 'Pusher: Message received from '+ self.connectionid )
logger.debug( 'Pusher: Unhandled message received from '+ self.connectionid )
# connection closed
def on_close(self):

View File

@ -37,7 +37,7 @@ setup(
],
entry_points={
'mopidy.ext': [
'iris = mopidy_iris:IrisExtension',
'iris = mopidy_iris:Extension',
],
},
)

2
src/js/bootstrap.js vendored
View File

@ -49,8 +49,6 @@ var initialState = {
},
spotify: {
connected: false,
country: 'NZ',
locale: 'en_NZ',
me: false
},
ui: {

View File

@ -74,7 +74,9 @@ export default class CreatePlaylistModal extends React.Component{
<span className="label">Public</span>
</label>
</div>
<button type="submit" className="primary centered" disabled={!this.state.submit_enabled}>Save</button>
<div className="actions centered-text">
<button type="submit" className="primary wide" disabled={!this.state.submit_enabled}>Save</button>
</div>
</form>
</div>
)

View File

@ -53,7 +53,9 @@ export default class EditPlaylistModal extends React.Component{
<span className="label">Public</span>
</label>
</div>
<button type="submit" className="primary centered" disabled={!this.state.submit_enabled}>Save</button>
<div className="actions centered-text">
<button type="submit" className="primary wide" disabled={!this.state.submit_enabled}>Save</button>
</div>
</form>
</div>
)

View File

@ -24,13 +24,12 @@ export default class EditRadioModal extends React.Component{
this.props.spotifyActions.resolveRadioSeeds(this.props.radio)
}
save(){
this.props.pusherActions.startRadio(this.state.seeds)
this.props.uiActions.closeModal()
}
stop(){
this.props.pusherActions.stopRadio()
handleSubmit(e){
if (this.state.enabled){
this.props.pusherActions.startRadio(this.state.seeds)
}else{
this.props.pusherActions.stopRadio()
}
this.props.uiActions.closeModal()
}
@ -68,7 +67,7 @@ export default class EditRadioModal extends React.Component{
renderSeeds(){
var seeds = []
if (this.state.enabled && this.state.seeds){
if (this.state.seeds){
for (var i = 0; i < this.state.seeds.length; i++){
var uri = this.state.seeds[i]
if (uri){
@ -98,37 +97,35 @@ export default class EditRadioModal extends React.Component{
}
return (
<div className="list">
{
seeds.map((seed,index) => {
return (
<div className="list-item" key={seed.uri}>
{seed.unresolved ? <span className="grey-text">{seed.uri}</span> : <span>{seed.name}</span> }
<span className="grey-text">&nbsp;({seed.type})</span>
<FontAwesome name="close" className="pull-right destructive" onClick={() => this.removeSeed(seed.uri)} />
</div>
)
})
}
<div>
<div className="list">
{
seeds.map((seed,index) => {
return (
<div className="list-item" key={seed.uri}>
{seed.unresolved ? <span className="grey-text">{seed.uri}</span> : <span>{seed.name}</span> }
<span className="grey-text">&nbsp;({seed.type})</span>
<FontAwesome name="close" className="pull-right destructive" onClick={() => this.removeSeed(seed.uri)} />
</div>
)
})
}
</div>
</div>
)
}
renderActions(){
if (this.state.enabled){
return (
<span>
<button className="primary wide" onClick={e => this.save()}>Save</button>
<button className="destructive wide" onClick={e => this.stop()}>Stop</button>
</span>
)
}else{
return (
<span>
<button className="primary wide" onClick={e => this.save()}>Start</button>
</span>
)
}
renderAddSeeds(){
return (
<div className="field no-top-margin">
<input
type="text"
placeholder="Add comma-separated URIs"
onChange={e => this.setState({uri: e.target.value})}
value={this.state.uri} />
<button type="button" className="discrete" onClick={e => this.addSeed()}><FontAwesome name="plus" /></button>
</div>
)
}
render(){
@ -136,22 +133,23 @@ export default class EditRadioModal extends React.Component{
<div>
<h4>Edit radio</h4>
<h3>Current seeds</h3>
{this.renderSeeds()}
<form>
<h3>Add seeds</h3>
<div className="field">
<input
type="text"
placeholder="Comma-separated URIs"
onChange={e => this.setState({uri: e.target.value})}
value={this.state.uri} />
<button className="discrete" onClick={e => this.addSeed()}><FontAwesome name="check" /></button>
<form onSubmit={e => this.handleSubmit(e)}>
<div className="field checkbox white">
<label>
<input
type="checkbox"
name="enabled"
checked={ this.state.enabled }
onChange={ e => this.setState({ enabled: !this.state.enabled })} />
<span className="label">Radio mode enabled</span>
</label>
</div>
{this.state.enabled ? this.renderSeeds() : null}
{this.state.enabled ? this.renderAddSeeds() : null}
<div className="actions centered-text">
{this.renderActions()}
<button type="submit" className="primary wide">Save</button>
</div>
</form>
</div>

View File

@ -39,16 +39,17 @@ import LibraryLocalDirectory from './views/library/LibraryLocalDirectory'
// setup our analytics tracking
ReactGA.initialize('UA-64701652-3');
function logPageView() {
function handleUpdate() {
ReactGA.set({ page: window.location.hash })
ReactGA.pageview(window.location.hash)
$(window).scrollTop(0)
}
global.baseURL = '/'
ReactDOM.render(
<Provider store={store}>
<Router history={hashHistory} onUpdate={logPageView}>
<Router history={hashHistory} onUpdate={handleUpdate}>
<Route path={global.baseURL} component={App}>
<IndexRoute component={Queue} />

View File

@ -1,7 +1,8 @@
var pusherActions = require('./actions.js')
var uiActions = require('../ui/actions.js')
var helpers = require('../../helpers.js')
var uiActions = require('../ui/actions.js')
var pusherActions = require('./actions.js')
var spotifyActions = require('../spotify/actions.js')
const PusherMiddleware = (function(){
@ -103,12 +104,6 @@ const PusherMiddleware = (function(){
socket.onopen = () => {
store.dispatch({ type: 'PUSHER_CONNECTED', connection: connection });
store.dispatch({ type: 'PUSHER_SET_USERNAME', username: connection.username });
request({ action: 'get_radio' })
.then(
response => {
store.dispatch({ type: 'RADIO', data: response.data })
}
)
};
socket.onmessage = (message) => {
@ -119,10 +114,29 @@ const PusherMiddleware = (function(){
break;
case 'PUSHER_CONNECTED':
request({ action: 'get_config' })
.then(
response => {
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))
}
var spotify = store.getState().spotify
if (!spotify.country || !spotify.locale){
store.dispatch({ type: 'SPOTIFY_SET_CONFIG', config: response.data.config })
}
}
)
request({ action: 'get_version' })
.then(
response => {
store.dispatch({ type: 'VERSION', data: response.data })
store.dispatch({ type: 'VERSION', version: response.data.version })
}
)
request({ action: 'get_radio' })
.then(
response => {
store.dispatch({ type: 'RADIO', radio: response.data.radio })
}
)
return next(action);
@ -141,7 +155,7 @@ const PusherMiddleware = (function(){
}else{
store.dispatch( uiActions.createNotification('Upgrade failed, please upgrade manually','bad') )
}
store.dispatch({ type: 'VERSION', data: response.data })
store.dispatch({ type: 'VERSION', version: response.data.version })
}
)
return next(action);

View File

@ -39,7 +39,7 @@ export default function reducer(pusher = {}, action){
case 'VERSION':
return Object.assign({}, pusher, {
version: action.data.version,
version: action.version,
upgrading: false
});

View File

@ -1,4 +1,5 @@
var uiActions = require('../../services/ui/actions.js')
var helpers = require('../../helpers.js')
/**
@ -39,6 +40,7 @@ const sendRequest = ( dispatch, getState, endpoint, method = 'GET', data = false
},
(xhr, status, error) => {
console.error( endpoint+' failed', xhr.responseText)
dispatch(uiActions.createNotification(xhr.responseText,'bad'))
reject(error)
}
)
@ -98,6 +100,7 @@ function refreshToken( dispatch, getState ){
},
error => {
dispatch({ type: 'SPOTIFY_DISCONNECTED' })
dispatch(uiActions.createNotification('Could not refresh token','bad'))
console.error('Could not refresh token', error)
reject(error)
}
@ -124,6 +127,7 @@ function refreshToken( dispatch, getState ){
},
error => {
dispatch({ type: 'SPOTIFY_DISCONNECTED' })
dispatch(uiActions.createNotification('Could not refresh token','bad'))
console.error('Could not refresh token', error)
reject(error)
}

View File

@ -93,8 +93,8 @@ const SpotifyMiddleware = (function(){
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

@ -12,6 +12,9 @@ export default function reducer(spotify = {}, action){
case 'SPOTIFY_DISCONNECTED':
return Object.assign({}, spotify, { connected: false, connecting: false })
case 'SPOTIFY_SET_CONFIG':
return Object.assign({}, spotify, action.config)
case 'PUSHER_SPOTIFY_TOKEN':
if( spotify.authorized ) return spotify;
return Object.assign({}, spotify, {

View File

@ -209,9 +209,9 @@ const UIMiddleware = (function(){
next(action)
break
case 'PUSHER_VERSION':
if( action.data.version.upgrade_available )
store.dispatch( uiActions.createNotification( 'Version '+action.data.version.latest+' is available. See settings to upgrade.' ) )
case 'VERSION':
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,6 +13,9 @@ export default function reducer(ui = {}, action){
case 'UI_SET':
return Object.assign({}, ui, action.data)
case 'CONFIG':
return Object.assign({}, ui, { config: action.config })
case 'TOGGLE_SIDEBAR':
var new_state = !ui.sidebar_open
if( typeof(action.new_state) !== 'undefined' ) new_state = action.new_state
@ -118,7 +121,7 @@ export default function reducer(ui = {}, action){
case 'RADIO':
case 'START_RADIO':
return Object.assign({}, ui, { seeds_resolved: false }, { radio: action.data.radio })
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 })

View File

@ -34,7 +34,8 @@ class Settings extends React.Component{
resetAllSettings(){
localStorage.clear();
window.location.reload(true);
window.location = '#'
window.location.reload(true)
return false;
}
@ -80,21 +81,33 @@ class Settings extends React.Component{
}
renderSpotifyUser(){
if( this.props.spotify.me && this.props.spotify.authorized ){
var user = null
if (this.props.spotify.me && this.props.spotify.authorized){
user = this.props.spotify.me
} else if (this.props.ui.config && this.props.ui.config.spotify_username){
if (this.props.ui.users && typeof(this.props.ui.users['spotify:user:'+this.props.ui.config.spotify_username]) !== 'undefined'){
user = this.props.ui.users['spotify:user:'+this.props.ui.config.spotify_username]
}
}
if (user){
return (
<Link className="user" to={global.baseURL+'user/'+this.props.spotify.me.uri}>
<Thumbnail circle={true} size="small" images={this.props.spotify.me.images} />
<Link className="user" to={global.baseURL+'user/'+user.uri}>
<Thumbnail circle={true} size="small" images={user.images} />
<span className="user-name">
{ this.props.spotify.me.display_name ? this.props.spotify.me.display_name : this.props.spotify.me.id }
{user.display_name ? user.display_name : user.username}
{!this.props.spotify.authorized ? <span className="grey-text">&nbsp;(As defined in config file)</span> : null}
</span>
</Link>
)
}else{
} else {
return (
<Link className="user">
<Thumbnail circle={true} size="small" />
<span className="user-name">
Default user <span className="grey-text">(As defined in mopidy.config)</span>
Default user
<span className="grey-text">&nbsp;(As defined in config file)</span>
</span>
</Link>
)

View File

@ -32,7 +32,8 @@ main {
background: transparent;
font-size: 14px;
font-weight: 600;
padding: 14px 10px;
padding: 13px 10px;
line-height: 24px;
text-transform: uppercase;
}
}

View File

@ -133,7 +133,6 @@
&.edit_radio {
form {
padding-top: 60px;
.field {
position: relative;
margin-top: 10px;
@ -141,7 +140,11 @@
position: absolute;
top: 5px;
right: 0;
padding: 10px 12px;
padding: 14px;
}
input[type="text"]{
font-size: 14px;
padding: 16px 18px;
}
}
}

View File

@ -6,7 +6,8 @@
left: 0;
right: 0;
text-align: center;
z-index: 100;
z-index: 99;
pointer-events: none;
.notification {
display: inline-block;
@ -15,6 +16,7 @@
padding: 12px 32px 12px 18px;
margin: 0 0.5px;
color: #FFFFFF;
pointer-events: all;
&:first-child {
border-top-left-radius: 50px;

View File

@ -115,6 +115,8 @@ h4 {
.no-bottom-padding { padding-bottom: 0 !important; }
.no-right-padding { padding-right: 0 !important; }
.no-left-padding { padding-left: 0 !important; }
.no-top-margin { margin-top: 0 !important; }
.no-bottom-margin { margin-bottom: 0 !important; }
.top-padding { padding-top: 20px; }
.bottom-padding { padding-bottom: 20px; }

View File

@ -47,6 +47,10 @@ input[type="submit"] {
&.large {
padding: 16px 24px;
@include responsive( $bp_medium ){
padding: 10px 20px;
}
}
&.primary {

View File

@ -84,9 +84,7 @@
@include responsive( $bp_medium ){
.intro {
padding: 50px 30px 0 30px;
margin: 0 0 140px 0;
padding: 50px 30px 60px 30px;
.parallax {
height: 100%;
@ -107,26 +105,48 @@
.heading-wrapper {
height: auto;
position: static;
.heading,
.sub-views {
position: relative;
.heading {
position: static;
bottom: auto;
left: auto;
h1 {
position: relative;
text-align: center;
padding: 15px 0;
}
.sub-views {
position: absolute;
bottom: 0;
left: 20px;
right: 20px;
}
}
}
.details-wrapper {
padding: 0;
position: absolute;
position: relative;
left: 0;
right: 0;
padding: 30px;
background: $faint_grey;
z-index: 2;
padding: 20px 0;
z-index: 3;
.actions {
text-align: center;
margin: 0;
float: none;
button {
margin: 0 3px;
}
}
.details {
clear: both;
display: none;
}
}
}