Overhaul radio UI, fixes #93

This commit is contained in:
James Barnsley
2017-04-18 04:44:39 +12:00
parent 46c8288713
commit 79090edd2c
13 changed files with 169 additions and 87 deletions

View File

@ -260,9 +260,11 @@ class IrisCore(object):
# no uris means we can't play radio
if not uris:
self.radio['enabled'] = 0
return {
'status': 0,
'message': 'No recommendations found'
'message': 'No recommendations found',
'radio': self.radio
}
# if we got recommendations
@ -279,6 +281,31 @@ class IrisCore(object):
return self.get_radio({})
def update_radio(self, data):
self.radio = data
self.radio['enabled'] = 1;
uris = self.load_more_tracks()
# no uris means we can't play radio
if not uris:
self.radio['enabled'] = 0
return {
'status': 0,
'message': 'No recommendations found',
'radio': self.radio
}
# if we got recommendations
else:
self.core.tracklist.add( uris = uris )
self.broadcast({
'type': 'radio_updated',
'radio': self.radio
})
return self.get_radio({})
def stop_radio(self, data):
self.radio = {

BIN
src/assets/radio-overlay.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -69,6 +69,7 @@ class FullPlayer extends React.Component{
!this.props.current_track.album.images ){
return (
<span className="artwork">
{this.props.radio_enabled ? <img className="radio-overlay" src="assets/radio-overlay.png" /> : null}
<Thumbnail size="huge" />
</span>
)
@ -78,6 +79,7 @@ class FullPlayer extends React.Component{
if( this.props.current_track.album.uri ) link = '/album/'+this.props.current_track.album.uri
return (
<Link className="artwork" to={link} onContextMenu={e => this.handleContextMenu(e,this.props.current_track.album)}>
{this.props.radio_enabled ? <img className="radio-overlay" src="assets/radio-overlay.png" /> : null}
<Thumbnail size="huge" images={this.props.current_track.album.images} canZoom />
</Link>
)
@ -90,8 +92,10 @@ class FullPlayer extends React.Component{
{ this.renderArtwork() }
<div className="current-track">
<div className="title">{ this.props.current_track ? this.props.current_track.name : <span>-</span> }</div>
{ this.props.current_track ? <ArtistSentence artists={ this.props.current_track.artists } /> : <ArtistSentence /> }
<div className="title">
{this.props.current_track ? this.props.current_track.name : <span>-</span>}
</div>
{this.props.current_track ? <ArtistSentence artists={ this.props.current_track.artists } /> : <ArtistSentence />}
</div>
<div className="controls cf">
@ -135,6 +139,7 @@ class FullPlayer extends React.Component{
const mapStateToProps = (state, ownProps) => {
return {
radio_enabled: (state.ui.radio && state.ui.radio.enabled ? true : false),
current_track: state.ui.current_track,
play_state: state.mopidy.play_state,
time_position: state.mopidy.time_position,

View File

@ -12,7 +12,8 @@ export default class EditRadioModal extends React.Component{
this.state = {
enabled: false,
seeds: [],
uri: ''
uri: '',
error_message: null
}
}
@ -24,26 +25,39 @@ export default class EditRadioModal extends React.Component{
this.props.spotifyActions.resolveRadioSeeds(this.props.radio)
}
handleSubmit(e){
if (this.state.enabled){
this.props.pusherActions.startRadio(this.state.seeds)
}else{
this.props.pusherActions.stopRadio()
}
handleStart(e){
e.preventDefault()
this.props.pusherActions.startRadio(this.state.seeds)
this.props.uiActions.closeModal()
}
handleUpdate(e){
e.preventDefault()
this.props.pusherActions.updateRadio(this.state.seeds)
this.props.uiActions.closeModal()
}
handleStop(e){
e.preventDefault()
this.props.pusherActions.stopRadio()
this.props.uiActions.closeModal()
}
addSeed(){
if (this.state.uri == '') return null
if (this.state.uri == ''){
this.setState({error_message: 'Cannot be empty'})
return null
}
var seeds = Object.assign([],this.state.seeds)
var uris = this.state.uri.split(',')
for (var i = 0; i < uris.length; i++){
if (seeds.indexOf(uris[i]) <= -1){
seeds.push(uris[i])
if (seeds.indexOf(uris[i]) > -1){
this.setState({error_message: 'URI already added'})
} else {
this.props.uiActions.createNotification(uris[i]+' already added','bad')
seeds.push(uris[i])
this.setState({error_message: null})
}
}
@ -71,8 +85,8 @@ export default class EditRadioModal extends React.Component{
for (var i = 0; i < this.state.seeds.length; i++){
var uri = this.state.seeds[i]
if (uri){
if (helpers.uriType(uri) == 'artist' && this.props.artists){
if (this.props.artists.hasOwnProperty(uri)){
if (helpers.uriType(uri) == 'artist'){
if (this.props.artists && this.props.artists.hasOwnProperty(uri)){
seeds.push(this.props.artists[uri])
} else {
seeds.push({
@ -81,8 +95,8 @@ export default class EditRadioModal extends React.Component{
uri: uri
})
}
} else if (helpers.uriType(uri) == 'track' && this.props.tracks){
if (this.props.tracks.hasOwnProperty(uri)){
} else if (helpers.uriType(uri) == 'track'){
if (this.props.tracks && this.props.tracks.hasOwnProperty(uri)){
seeds.push(this.props.tracks[uri])
} else {
seeds.push({
@ -105,7 +119,9 @@ export default class EditRadioModal extends React.Component{
<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)} />
<button className="discrete remove-uri" onClick={e => this.removeSeed(seed.uri)}>
<FontAwesome name="close" />&nbsp;Remove
</button>
</div>
)
})
@ -115,41 +131,29 @@ export default class EditRadioModal extends React.Component{
)
}
renderAddSeeds(){
return (
<div className="field no-top-margin">
<input
type="text"
placeholder="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(){
return (
<div>
<h4>Edit radio</h4>
<h4>Manage radio</h4>
<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>
<form>
{this.renderSeeds()}
<div className="field no-top-margin">
<input
type="text"
placeholder="Comma-separated URIs"
onChange={e => this.setState({uri: e.target.value, error_message: null})}
value={this.state.uri} />
<button className="discrete add-uri" onClick={e => this.addSeed()}>
<FontAwesome name="plus" />&nbsp; Add
</button>
{this.state.error_message ? <span className="error">{this.state.error_message}</span> : null}
</div>
{this.state.enabled ? this.renderSeeds() : null}
{this.state.enabled ? this.renderAddSeeds() : null}
<div className="actions centered-text">
<button type="submit" className="primary wide">Save</button>
{this.state.enabled ? <button className="destructive wide" onClick={e => this.handleStop(e)}>Stop</button> : null}
{this.state.enabled ? <button className="primary wide" onClick={e => this.handleUpdate(e)}>Save</button> : <button className="primary wide" onClick={e => this.handleStart(e)}>Start</button>}
</div>
</form>
</div>

View File

@ -76,6 +76,13 @@ export function startRadio( uris ){
}
}
export function updateRadio( uris ){
return {
type: 'PUSHER_UPDATE_RADIO',
uris: uris
}
}
export function stopRadio(){
return {
type: 'PUSHER_STOP_RADIO'

View File

@ -243,7 +243,16 @@ const PusherMiddleware = (function(){
break
case 'PUSHER_START_RADIO':
store.dispatch(uiActions.createNotification('Starting radio...'))
case 'PUSHER_UPDATE_RADIO':
if (action.type == 'PUSHER_UPDATE_RADIO'){
var method = 'update_radio'
var process_text = 'Updating radio'
} else {
var method = 'start_radio'
var process_text = 'Starting radio'
}
store.dispatch(uiActions.startProcess('PUSHER_RADIO', process_text))
var data = {
seed_artists: [],
@ -264,19 +273,11 @@ const PusherMiddleware = (function(){
break;
}
}
// we don't need to wait for response, as change will be broadcast
request(store, 'start_radio', data)
break
case 'PUSHER_RADIO_STARTED':
var data = {
type: 'browser_notification',
title: 'Radio',
body: 'Radio mode started',
icon: (store.getState().ui.current_track ? helpers.getTrackIcon( store.getState().ui.current_track ) : false)
}
store.dispatch( pusherActions.deliverBroadcast(data) )
request(store, method, data)
.then(response => {
store.dispatch(uiActions.stopProcess('PUSHER_RADIO'))
})
break
case 'PUSHER_STOP_RADIO':
@ -292,22 +293,11 @@ const PusherMiddleware = (function(){
request(store, 'stop_radio', data)
break
case 'PUSHER_RADIO_STOPPED':
var data = {
type: 'browser_notification',
title: 'Radio',
body: 'Radio mode stopped',
icon: (store.getState().ui.current_track ? helpers.getTrackIcon( store.getState().ui.current_track ) : false)
}
store.dispatch( pusherActions.deliverBroadcast(data) )
break
case 'PUSHER_BROWSER_NOTIFICATION':
store.dispatch(uiActions.createBrowserNotification(action))
break
case 'PUSHER_RESTART':
// Hard reload. This doesn't strictly clear the cache, but our compiler's
// cache buster should handle that
window.location.reload(true);

View File

@ -528,13 +528,16 @@ export function resolveRadioSeeds( radio ){
artist_ids += helpers.getFromUri('artistid', radio.seed_artists[i])
}
sendRequest( dispatch, getState, 'artists/'+ artist_ids )
sendRequest( dispatch, getState, 'artists?ids='+ artist_ids )
.then( response => {
if (!(response instanceof Array)) response = [response]
dispatch({
type: 'ARTISTS_LOADED',
artists: response
})
if (response && response.artists){
dispatch({
type: 'ARTISTS_LOADED',
artists: response.artists
})
} else {
console.error('No Spotify artists returned', artist_ids)
}
})
}

View File

@ -62,6 +62,7 @@ class Queue extends React.Component{
<button onClick={e => this.props.uiActions.openModal('edit_radio')}>
<FontAwesome name="podcast" />&nbsp;
Radio
{this.props.radio && this.props.radio.enabled ? <span className="flag blue">On</span> : null}
</button>
<button onClick={e => hashHistory.push(global.baseURL+'queue-history')}>
<FontAwesome name="history" />&nbsp;

View File

@ -284,6 +284,23 @@
}
}
&.queue-track-list {
.list-item {
.artists {
padding-left: 0 !important;
&:before {
display: none !important;
}
}
.source,
.added {
display: none !important;
}
}
}
&.edit-mode {
.list-item {
padding-left: 40px !important;

View File

@ -137,21 +137,24 @@
.field {
position: relative;
margin-top: 10px;
button {
position: absolute;
top: 5px;
right: 0;
padding: 14px;
margin: 0;
min-width: 0;
color: #FFFFFF;
}
input[type="text"]{
font-size: 14px;
padding: 16px 18px;
}
}
}
button.add-uri,
button.remove-uri {
position: absolute;
top: 3px;
right: 0;
padding: 14px;
margin: 0;
min-width: 0;
color: #FFFFFF;
}
}
&.kiosk_mode {

View File

@ -52,6 +52,18 @@
}
}
.artwork {
position: relative;
.radio-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
z-index: 2;
}
}
&.mini-player {
position: fixed;
bottom: 0;

View File

@ -270,7 +270,6 @@ footer {
font-size: 12px;
display: inline-block;
padding: 2px 4px;
border-radius: 2px;
background: $light_grey;
color: $dark_grey;

View File

@ -198,6 +198,15 @@ input[type="submit"] {
}
}
.flag {
font-size: 10px;
margin-left: 3px;
padding: 1px 4px;
line-height: 1.2em;
vertical-align: top;
margin-top: 6px;
}
@include responsive( $bp_medium ){
min-width: 120px;
margin-right: 10px;
@ -336,6 +345,11 @@ input[type="submit"] {
}
}
}
.error {
color: $red;
display: inline-block;
}
}
@include responsive( $bp_medium ){