Merge branch 'develop'

This commit is contained in:
James Barnsley
2018-02-26 07:57:02 +13:00
19 changed files with 171 additions and 87 deletions

View File

@ -1 +1 @@
3.13.1
3.14.0

View File

@ -101,7 +101,16 @@ class IrisCore(object):
callback = kwargs.get('callback', None)
for connection in self.connections.itervalues():
connection['connection'].write_message( json_encode(data) )
send_to_this_connection = True
# Don't send the broadcast to the origin, naturally
if 'connection_id' in data:
if connection['connection_id'] == data["connection_id"]:
send_to_this_connection = False
if send_to_this_connection:
connection['connection'].write_message(json_encode(data))
response = {
'message': 'Broadcast to '+str(len(self.connections))+' connections'
@ -141,6 +150,7 @@ class IrisCore(object):
new_connection = {
'client': client,
'connection_id': connection_id,
'connection': connection
}
self.connections[connection_id] = new_connection

View File

@ -157,13 +157,13 @@ class App extends React.Component{
case 32: // spacebar
if (e.ctrlKey || e.metaKey){
this.props.mopidyActions.stop();
this.props.uiActions.createNotification('stop', 'shortcut', 'shortcut');
this.props.uiActions.createNotification({content: 'stop', type: 'shortcut', key: 'shortcut'});
} else if (this.props.play_state == 'playing'){
this.props.mopidyActions.pause();
this.props.uiActions.createNotification('pause', 'shortcut', 'shortcut');
this.props.uiActions.createNotification({content: 'pause', type: 'shortcut', key: 'shortcut'});
} else {
this.props.mopidyActions.play();
this.props.uiActions.createNotification('play', 'shortcut', 'shortcut');
this.props.uiActions.createNotification({content: 'play', type: 'shortcut', key: 'shortcut'});
}
break;
@ -179,7 +179,7 @@ class App extends React.Component{
case 40: // down
if ((e.ctrlKey || e.metaKey) && e.shiftKey){
this.props.mopidyActions.setMute(true);
this.props.uiActions.createNotification('volume-off', 'shortcut', 'shortcut');
this.props.uiActions.createNotification({content: 'volume-off', type: 'shortcut', key: 'shortcut'});
} else if (e.ctrlKey){
var volume = this.props.volume;
if (volume !== 'false'){
@ -191,7 +191,7 @@ class App extends React.Component{
if (this.props.mute){
this.props.mopidyActions.setMute(false);
}
this.props.uiActions.createNotification('volume-down', 'shortcut', 'shortcut');
this.props.uiActions.createNotification({content: 'volume-down', type: 'shortcut', key: 'shortcut'});
}
}
break;
@ -202,7 +202,7 @@ class App extends React.Component{
if (this.props.mute){
this.props.mopidyActions.setMute(false);
}
this.props.uiActions.createNotification('volume-up', 'shortcut', 'shortcut');
this.props.uiActions.createNotification({content: 'volume-up', type: 'shortcut', key: 'shortcut'});
} else if (e.ctrlKey || e.metaKey){
var volume = this.props.volume
if (volume !== 'false'){
@ -214,7 +214,7 @@ class App extends React.Component{
if (this.props.mute){
this.props.mopidyActions.setMute(false);
}
this.props.uiActions.createNotification('volume-up', 'shortcut', 'shortcut');
this.props.uiActions.createNotification({content: 'volume-up', type: 'shortcut', key: 'shortcut'});
}
}
break;
@ -226,20 +226,20 @@ class App extends React.Component{
new_position = 0;;
}
this.props.mopidyActions.seek(new_position);
this.props.uiActions.createNotification('fast-backward', 'shortcut', 'shortcut');
this.props.uiActions.createNotification({content: 'fast-backward', type: 'shortcut', key: 'shortcut'});
} else if (e.ctrlKey || e.metaKey){
this.props.mopidyActions.previous();
this.props.uiActions.createNotification('step-backward', 'shortcut', 'shortcut');
this.props.uiActions.createNotification({content: 'step-backward', type: 'shortcut', key: 'shortcut'});
}
break;
case 39: // right
if ((e.ctrlKey || e.metaKey) && e.shiftKey){
this.props.mopidyActions.seek(this.props.play_time_position + 30000);
this.props.uiActions.createNotification('fast-forward', 'shortcut', 'shortcut');
this.props.uiActions.createNotification({content: 'fast-forward', type: 'shortcut', key: 'shortcut'});
} else if (e.ctrlKey || e.metaKey){
this.props.mopidyActions.next();
this.props.uiActions.createNotification('step-forward', 'shortcut', 'shortcut');
this.props.uiActions.createNotification({content: 'step-forward', type: 'shortcut', key: 'shortcut'});
}
break;

View File

@ -307,7 +307,7 @@ class ContextMenu extends React.Component{
document.execCommand("copy");
temp.remove();
this.props.uiActions.createNotification("Copied "+this.props.menu.uris.length+" URIs" )
this.props.uiActions.createNotification({content: "Copied "+this.props.menu.uris.length+" URIs"});
this.props.uiActions.hideContextMenu()
}

View File

@ -41,7 +41,7 @@ class FollowButton extends React.Component{
}
if (!this.props.spotify_authorized){
return <button className={className+' disabled'} onClick={e => this.props.uiActions.createNotification('You must authorize Spotify first','warning')}>{this.props.addText}</button>
return <button className={className+' disabled'} onClick={e => this.props.uiActions.createNotification({content: 'You must authorize Spotify first', type: 'warning'})}>{this.props.addText}</button>
} else if (this.props.is_following === true){
return <button className={className+' destructive'} onClick={e => this.remove()}>{this.props.removeText}</button>
} else {

View File

@ -39,13 +39,13 @@ class LastfmAuthenticationFrame extends React.Component{
// Only allow incoming data from our authorized authenticator proxy
var authorization_domain = this.props.authorization_url.substring(0,this.props.authorization_url.indexOf('/',8))
if (event.origin != authorization_domain){
this.props.uiActions.createNotification('Authorization failed. '+event.origin+' is not the configured authorization_url.','bad')
this.props.uiActions.createNotification({content: 'Authorization failed. '+event.origin+' is not the configured authorization_url.', type: 'bad'});
return false
}
// Bounced with an error
if (data.error !== undefined){
this.props.uiActions.createNotification(data.message,'bad')
this.props.uiActions.createNotification({content: data.message, type: 'bad'});
// No errors? We're in!
} else {
@ -80,7 +80,7 @@ class LastfmAuthenticationFrame extends React.Component{
// Popup does not exist, so must have been blocked
} else {
self.props.uiActions.createNotification('Popup blocked. Please allow popups and try again.','bad')
self.props.uiActions.createNotification({content: 'Popup blocked. Please allow popups and try again.', type: 'bad'});
self.setState({authorizing: false})
clearInterval(timer);
}

View File

@ -36,7 +36,7 @@ class FollowButton extends React.Component{
}
if (!this.props.lastfm_authorized){
return <button className={className+' disabled'} onClick={e => this.props.uiActions.createNotification('You must authorize LastFM first','warning')}>{this.props.addText}</button>
return <button className={className+' disabled'} onClick={e => this.props.uiActions.createNotification({content: 'You must authorize LastFM first', type: 'warning'})}>{this.props.addText}</button>
} else if (this.props.is_loved && this.props.is_loved !== "0"){
return <button className={className+' destructive'} onClick={e => this.remove()}>{this.props.removeText}</button>
} else {

View File

@ -39,13 +39,13 @@ class SpotifyAuthenticationFrame extends React.Component{
// Only allow incoming data from our authorized authenticator proxy
var authorization_domain = this.props.authorization_url.substring(0,this.props.authorization_url.indexOf('/',8))
if (event.origin != authorization_domain){
this.props.uiActions.createNotification('Authorization failed. '+event.origin+' is not the configured authorization_url.','bad')
this.props.uiActions.createNotification({content: 'Authorization failed. '+event.origin+' is not the configured authorization_url.', type: 'bad'});
return false
}
// Spotify bounced with an error
if (data.error !== undefined){
this.props.uiActions.createNotification(data.error,'bad')
this.props.uiActions.createNotification({content: data.error, type: 'bad'});
// No errors? We're in!
} else {
@ -95,7 +95,7 @@ class SpotifyAuthenticationFrame extends React.Component{
// Popup does not exist, so must have been blocked
} else {
self.props.uiActions.createNotification('Popup blocked. Please allow popups and try again.','bad')
self.props.uiActions.createNotification({content: 'Popup blocked. Please allow popups and try again.', type: 'bad'});
self.setState({authorizing: false})
clearInterval(timer);
}

View File

@ -293,7 +293,7 @@ class TrackList extends React.Component{
var selected_tracks_indexes = helpers.arrayOf('index', selected_tracks);
if (selected_tracks.length <= 0){
return this.props.uiActions.createNotification('No tracks selected', 'bad');
return this.props.uiActions.createNotification({content: 'No tracks selected', type: 'bad'});
}
// Our parent handles playing

View File

@ -87,13 +87,7 @@ const CoreMiddleware = (function(){
nonInteraction: true
});
store.dispatch(uiActions.createNotification(
message,
'bad',
null,
null,
description
));
store.dispatch(uiActions.createNotification({content: message, type: 'bad', description: description}));
console.error(message, description, data);
break;
@ -176,7 +170,7 @@ const CoreMiddleware = (function(){
break
case 'PLAYLIST_TRACKS_ADDED':
store.dispatch(uiActions.createNotification('Added '+action.tracks_uris.length+' tracks to playlist'))
store.dispatch(uiActions.createNotification({content: 'Added '+action.tracks_uris.length+' tracks to playlist'}))
switch(helpers.uriSource(action.key)){
case 'spotify':
store.dispatch(spotifyActions.getPlaylist(action.key))

View File

@ -42,7 +42,10 @@ export function debug(call, value){
**/
export function changeTrack(tlid){
return instruct('playback.play', {tlid: tlid})
return {
type: 'MOPIDY_CHANGE_TRACK',
tlid: tlid
}
}
export function playURIs(uris, from_uri = null){
@ -85,7 +88,10 @@ export function playAlbum(uri){
}
export function removeTracks(tlids){
return instruct('tracklist.remove', {tlid: tlids})
return {
type: 'MOPIDY_REMOVE_TRACKS',
tlids: tlids
}
}
export function reorderTracklist(indexes, insert_before){
@ -104,7 +110,9 @@ export function clearTracklist(){
}
export function play(){
return instruct('playback.play')
return {
type: 'MOPIDY_TRIGGER_PLAY'
}
}
export function pause(){

View File

@ -274,26 +274,69 @@ const MopidyMiddleware = (function(){
* General playback
**/
case 'MOPIDY_NEXT':
case 'MOPIDY_TRIGGER_PLAY':
instruct(socket, store, 'playback.play');
var data = {
type: 'browser_notification',
title: 'Track skipped',
body: store.getState().pusher.username +' skipped this track',
type: 'notification',
notification_type: 'info',
content: store.getState().pusher.username +(store.getState().mopidy.play_state == 'paused' ? ' resumed' : ' started')+' playback',
icon: (store.getState().core.current_track ? helpers.getTrackIcon(store.getState().core.current_track, store.getState().core) : false)
}
store.dispatch(pusherActions.deliverBroadcast(data) )
store.dispatch(pusherActions.deliverBroadcast(data));
break
case 'MOPIDY_PAUSE':
var data = {
type: 'notification',
notification_type: 'info',
content: store.getState().pusher.username +' paused playback',
icon: (store.getState().core.current_track ? helpers.getTrackIcon(store.getState().core.current_track, store.getState().core) : false)
}
store.dispatch(pusherActions.deliverBroadcast(data));
break
case 'MOPIDY_NEXT':
var data = {
type: 'notification',
notification_type: 'info',
content: store.getState().pusher.username +' skipped <em>'+store.getState().core.current_track.name+'</em>',
icon: (store.getState().core.current_track ? helpers.getTrackIcon(store.getState().core.current_track, store.getState().core) : false)
}
store.dispatch(pusherActions.deliverBroadcast(data));
break
case 'MOPIDY_STOP':
var data = {
type: 'browser_notification',
title: 'Playback stopped',
body: store.getState().pusher.username +' stopped playback',
type: 'notification',
notification_type: 'info',
content: store.getState().pusher.username +' stopped playback',
icon: (store.getState().core.current_track ? helpers.getTrackIcon(store.getState().core.current_track, store.getState().core) : false)
}
store.dispatch(pusherActions.deliverBroadcast(data) )
store.dispatch(pusherActions.deliverBroadcast(data));
break
case 'MOPIDY_CHANGE_TRACK':
instruct(socket, store, 'playback.play', {tlid: action.tlid});
var broadcast_data = {
type: 'notification',
notification_type: 'info',
content: store.getState().pusher.username +' changed track'
}
store.dispatch(pusherActions.deliverBroadcast(broadcast_data));
break;
case 'MOPIDY_REMOVE_TRACKS':
instruct(socket, store, 'tracklist.remove', {tlid: action.tlids});
var broadcast_data = {
type: 'notification',
notification_type: 'info',
content: store.getState().pusher.username +' removed '+action.tlids.length+' tracks'
}
store.dispatch(pusherActions.deliverBroadcast(broadcast_data));
break;
case 'MOPIDY_PLAY_PLAYLIST':
// Clear tracklist (if set)
@ -326,7 +369,7 @@ const MopidyMiddleware = (function(){
.then(
response => {
if (response.tracks === undefined){
store.dispatch(uiActions.createNotification('Failed to load playlist tracks','bad'))
store.dispatch(uiActions.createNotification({content: 'Failed to load playlist tracks', type: 'bad'}));
} else {
var tracks_uris = helpers.arrayOf('uri',response.tracks)
store.dispatch(mopidyActions.playURIs(tracks_uris, action.uri))
@ -344,10 +387,18 @@ const MopidyMiddleware = (function(){
case 'MOPIDY_ENQUEUE_URIS':
if (!action.uris || action.uris.length <= 0){
this.props.uiActions.createNotification("No URIs to enqueue","warning");
this.props.uiActions.createNotification({content: "No URIs to enqueue", type: "warning"});
break;
}
var broadcast_data = {
type: 'notification',
notification_type: 'info',
content: store.getState().pusher.username +' is adding '+action.uris.length+' URIs to queue',
icon: (store.getState().core.current_track ? helpers.getTrackIcon(store.getState().core.current_track, store.getState().core) : false)
}
store.dispatch(pusherActions.deliverBroadcast(broadcast_data));
// split into batches
var uris = Object.assign([], action.uris)
var batches = []
@ -477,7 +528,7 @@ const MopidyMiddleware = (function(){
case 'MOPIDY_PLAY_URIS':
if (!action.uris || action.uris.length <= 0){
this.props.uiActions.createNotification("No URIs to play","warning");
this.props.uiActions.createNotification({content: "No URIs to play", type: "warning"});
break;
}
@ -491,7 +542,7 @@ const MopidyMiddleware = (function(){
store.dispatch(mopidyActions.clearTracklist());
}
var first_uri = action.uris[0]
var first_uri = action.uris[0];
// add our first track
instruct(socket, store, 'tracklist.add', { uri: first_uri, at_position: 0 })
@ -576,7 +627,7 @@ const MopidyMiddleware = (function(){
var uri_scheme = uri_schemes.shift();
if (uri_schemes_total <= 0){
store.dispatch(uiActions.createNotification('No sources selected', 'warning'));
store.dispatch(uiActions.createNotification({content: 'No sources selected', type: 'warning'}));
} else {
store.dispatch(uiActions.startProcess(
'MOPIDY_GET_SEARCH_RESULTS_PROCESSOR',
@ -1287,7 +1338,7 @@ const MopidyMiddleware = (function(){
hashHistory.push(global.baseURL+'playlist/'+encodeURIComponent(response.uri));
}
store.dispatch(uiActions.createNotification('Saved'));
store.dispatch(uiActions.createNotification({content: 'Saved'}));
})
});
break
@ -1337,7 +1388,7 @@ const MopidyMiddleware = (function(){
case 'MOPIDY_CREATE_PLAYLIST':
instruct(socket, store, 'playlists.create', { name: action.name, uri_scheme: action.scheme })
.then(response => {
store.dispatch(uiActions.createNotification('Created playlist'))
store.dispatch(uiActions.createNotification({content: 'Created playlist'}))
store.dispatch({
type: 'PLAYLIST_LOADED',
@ -1356,7 +1407,7 @@ const MopidyMiddleware = (function(){
case 'MOPIDY_DELETE_PLAYLIST':
instruct(socket, store, 'playlists.delete', { uri: action.uri })
.then(response => {
store.dispatch(uiActions.createNotification('Deleted playlist'))
store.dispatch(uiActions.createNotification({content: 'Deleted playlist'}))
store.dispatch({
type: 'MOPIDY_LIBRARY_PLAYLIST_DELETED',
key: action.uri

View File

@ -228,7 +228,7 @@ const PusherMiddleware = (function(){
request(store, 'deliver_message', action.data)
.then(
response => {
store.dispatch(uiActions.createNotification('Message delivered') )
store.dispatch(uiActions.createNotification({content: 'Message delivered'}));
},
error => {
store.dispatch(coreActions.handleException(
@ -278,9 +278,9 @@ const PusherMiddleware = (function(){
}
if (response.upgrade_successful){
store.dispatch(uiActions.createNotification('Upgrade complete') )
store.dispatch(uiActions.createNotification({content: 'Upgrade complete'}));
} else {
store.dispatch(uiActions.createNotification('Upgrade failed, please upgrade manually','bad') )
store.dispatch(uiActions.createNotification({content: 'Upgrade failed, please upgrade manually', type: 'bad'}));
}
response.type = 'PUSHER_VERSION'
@ -381,7 +381,7 @@ const PusherMiddleware = (function(){
response => {
store.dispatch(uiActions.processFinished('PUSHER_RADIO_PROCESS'));
if (response.status == 0){
store.dispatch(uiActions.createNotification(response.message, 'bad'));
store.dispatch(uiActions.createNotification({content: response.message, type: 'bad'}));
}
},
error => {
@ -395,8 +395,8 @@ const PusherMiddleware = (function(){
break
case 'PUSHER_STOP_RADIO':
store.dispatch(uiActions.createNotification('Stopping radio'))
ReactGA.event({ category: 'Pusher', action: 'Stop radio' })
store.dispatch(uiActions.createNotification({content: 'Stopping radio'}));
ReactGA.event({ category: 'Pusher', action: 'Stop radio' });
var data = {
seed_artists: [],
@ -420,6 +420,16 @@ const PusherMiddleware = (function(){
store.dispatch(uiActions.createBrowserNotification(action))
break
case 'PUSHER_NOTIFICATION':
var data = Object.assign(
{},
action, {
type: action.notification_type
}
);
store.dispatch(uiActions.createNotification(data));
break
case 'PUSHER_RESTART':
// Hard reload. This doesn't strictly clear the cache, but our compiler's
// cache buster should handle that
@ -430,7 +440,7 @@ const PusherMiddleware = (function(){
ReactGA.event({ category: 'Pusher', action: 'Version', label: action.version.current })
if (action.version.upgrade_available){
store.dispatch(uiActions.createNotification('Version '+action.version.latest+' is available. See settings to upgrade.' ) )
store.dispatch(uiActions.createNotification({content: 'Version '+action.version.latest+' is available. See settings to upgrade.'}));
}
next(action )
break

View File

@ -1456,7 +1456,7 @@ export function createPlaylist(name, description, is_public, is_collaborative){
uris: [response.uri]
})
dispatch(uiActions.createNotification('Created playlist'))
dispatch(uiActions.createNotification({content: 'Created playlist'}));
},
error => {
dispatch(coreActions.handleException(
@ -1491,7 +1491,7 @@ export function savePlaylist(uri, name, description, is_public, is_collaborative
description: description
}
})
dispatch(uiActions.createNotification('Saved'))
dispatch(uiActions.createNotification({content: 'Saved'}));
},
error => {
dispatch(coreActions.handleException(

View File

@ -175,20 +175,22 @@ export function createBrowserNotification(data){
}
}
export function createNotification(content, type = 'default', key = null, title = null, description = null, sticky = false){
if (!key){
key = helpers.generateGuid()
}
// content, type = 'default', key = null, title = null, description = null, sticky = false
export function createNotification(data){
return {
type: 'CREATE_NOTIFICATION',
notification: {
key: key,
type: type,
title: title,
description: description,
content: content,
sticky: sticky
}
notification: Object.assign(
{
key: helpers.generateGuid(),
type: 'default',
title: null,
content: null,
description: null,
sticky: false
},
data
)
}
}

View File

@ -11,7 +11,6 @@ const UIMiddleware = (function(){
* The actual middleware inteceptor
**/
return store => next => action => {
switch(action.type){
case 'MOPIDY_STATE':
@ -117,14 +116,14 @@ const UIMiddleware = (function(){
if (!suppressed_broadcasts.includes(broadcast.key)){
if (broadcast.message){
store.dispatch(uiActions.createNotification(
broadcast.message,
'broadcast',
(broadcast.key ? broadcast.key : null),
(broadcast.title ? broadcast.title : null),
null,
true
))
var data = {
key: (broadcast.key ? broadcast.key : null),
title: (broadcast.title ? broadcast.title : null),
content: broadcast.message,
type: 'broadcast',
sticky: true
}
store.dispatch(uiActions.createNotification(data));
}
}
}

View File

@ -112,7 +112,7 @@ class Debug extends React.Component{
<div className="field">
<div className="name"></div>
<div className="input">
<a className="button secondary" onClick={e => this.props.uiActions.createNotification('Test notification')}>Create notification</a>
<a className="button secondary" onClick={e => this.props.uiActions.createNotification({content: 'Test notification'})}>Create notification</a>
<a className="button secondary" onClick={e => this.props.uiActions.startProcess('test_process', 'Test process', {total: 100, remaining: 17})}>Start process</a>
<a className="button secondary" onClick={e => this.props.uiActions.processFinished('test_process')}>Stop process</a>
</div>
@ -169,7 +169,8 @@ class Debug extends React.Component{
<option value='{"method":"get_connections"}'>Get connections</option>
<option value='{"method":"get_radio"}'>Get radio</option>
<option value='{"method":"get_queue_metadata"}'>Get queue metadata</option>
<option value='{"method":"broadcast","data":{"type":"browser_notification","title":"Testing","body":"This is my message"}}'>Broadcast to all clients</option>
<option value='{"method":"broadcast","data":{"type":"browser_notification","title":"Testing","body":"This is my message"}}'>Broadcast to all clients (browser)</option>
<option value='{"method":"broadcast","data":{"type":"notification","notification_type":"info","title":"Testing","content":"This is my message"}}'>Broadcast to all clients (notification)</option>
<option value='{"method":"deliver_message","data":{"to":"CONNECTION_ID_HERE","message":{"type":"browser_notification","title":"Testing","body":"This is my message"}}}'>Broadcast to one client</option>
<option value='{"method":"set_username","data":{"connection_id":"CONNECTION_ID_HERE","username":"NewUsername"}}'>Change username</option>
<option value='{"method":"refresh_spotify_token"}'>Refresh Spotify token</option>

View File

@ -54,12 +54,15 @@
background: $green;
}
&.broadcast,
&.info {
&.broadcast {
background: $yellow;
color: $black;
}
&.info {
background: $grey;
}
&.warning {
background: $orange;
}

View File

@ -105,8 +105,14 @@ $bp_shallow: 650px;
}
@keyframes fadein {
0% { opacity: 0; }
100% { opacity: 1; }
0% {
opacity: 0;
transform: translateY(5px);
}
100% {
opacity: 1;
transform: translateY(0px);
}
}
@mixin fadein($duration: 0.2s){