diff --git a/src/js/components/ContextMenu.js b/src/js/components/ContextMenu.js
index 62899e51..f60b6e9c 100755
--- a/src/js/components/ContextMenu.js
+++ b/src/js/components/ContextMenu.js
@@ -1,815 +1,851 @@
-
-import React, { PropTypes } from 'react'
-import { hashHistory } from 'react-router'
-import Link from './Link'
-import { connect } from 'react-redux'
-import { bindActionCreators } from 'redux'
-
-import TrackList from './TrackList'
-import Icon from './Icon'
-import Thumbnail from './Thumbnail'
-
-import * as helpers from '../helpers'
-import * as coreActions from '../services/core/actions'
-import * as uiActions from '../services/ui/actions'
-import * as pusherActions from '../services/pusher/actions'
-import * as mopidyActions from '../services/mopidy/actions'
-import * as lastfmActions from '../services/lastfm/actions'
-import * as spotifyActions from '../services/spotify/actions'
-
-class ContextMenu extends React.Component{
-
- constructor(props){
- super(props)
- this.state = {
- submenu: null
- }
- this.handleScroll = this.handleScroll.bind(this);
- this.handleMouseDown = this.handleMouseDown.bind(this);
- this.handleTouchStart = this.handleTouchStart.bind(this);
- }
-
- componentDidMount(){
- window.addEventListener("scroll", this.handleScroll, false);
- window.addEventListener("mousedown", this.handleMouseDown, false);
- window.addEventListener("touchstart", this.handleTouchStart, false);
- }
-
- componentWillUnmount(){
- window.removeEventListener("scroll", this.handleScroll, false);
- window.removeEventListener("mousedown", this.handleMouseDown, false);
- window.removeEventListener("touchstart", this.handleTouchStart, false);
- }
-
- componentWillReceiveProps(nextProps){
-
- // if we've been given a menu object (ie activated) when we didn't have one prior
- if (nextProps.menu && !this.props.menu){
- this.setState({ submenu: null });
-
- var context = this.getContext(nextProps);
-
- // if we're able to be in the library, run a check
- if (nextProps.spotify_authorized && context.source == 'spotify'){
- switch (nextProps.menu.context){
- case 'artist':
- case 'album':
- case 'playlist':
- this.props.spotifyActions.following(nextProps.menu.items[0].uri)
- break
- }
- }
-
- // if we're able to be in the LastFM library, run a check
- if (nextProps.lastfm_authorized && context.is_track && context.items_count == 1){
- if (nextProps.menu.items[0].uri && this.props.tracks[nextProps.menu.items[0].uri] !== undefined && this.props.tracks[nextProps.menu.items[0].uri].userloved === undefined){
- this.props.lastfmActions.getTrack(nextProps.menu.items[0].uri);
- }
- }
- }
- }
-
- handleScroll(e){
- if (this.props.menu){
- this.props.uiActions.hideContextMenu();
- }
- }
-
- handleMouseDown(e){
- // if we click (touch or mouse) outside of the context menu or context menu trigger, kill it
- if ($(e.target).closest('.context-menu').length <= 0 && $(e.target).closest('.context-menu-trigger').length <= 0){
- this.props.uiActions.hideContextMenu();
- }
- }
-
- handleTouchStart(e){
-
- // if we click (touch or mouse) outside of the context menu or context menu trigger, kill it
- if ($(e.target).closest('.context-menu').length <= 0 && $(e.target).closest('.context-menu-trigger').length <= 0){
- this.props.uiActions.hideContextMenu();
- }
- }
-
- getContext(props = this.props){
- var context = {
- name: null,
- nice_name: 'Unknown',
- is_track: false
- }
-
- if (props.menu && props.menu.context){
- context.name = props.menu.context;
- context.nice_name = props.menu.context;
-
- // handle ugly labels
- switch (props.menu.context){
- case 'playlist':
- case 'editable-playlist':
- context.nice_name = 'playlist';
- break;
-
- case 'track':
- case 'queue-track':
- case 'playlist-track':
- case 'editable-playlist-track':
- context.nice_name = 'track';
- context.is_track = true;
- break;
- }
-
- // Consider the object(s) themselves
- // We can only really accommodate the first item. The only instances where
- // there is multiple is tracklists, when they're all of the same source (except search?)
- if (props.menu.items && props.menu.items.length > 0){
- var item = props.menu.items[0]
- context.item = item;
- context.items_count = props.menu.items.length;
- context.source = helpers.uriSource(item.uri);
- context.type = helpers.uriType(item.uri);
- context.in_library = this.inLibrary(item);
- context.is_loved = this.isLoved(item);
- }
- }
-
- return context
- }
-
- inLibrary(item = null){
- if (!item){
- return false;
- }
-
- switch (helpers.uriType(item.uri)){
- case 'artist':
- return (this.props.spotify_library_artists && this.props.spotify_library_artists.indexOf(item.uri) > -1);
- break;
- case 'album':
- return (this.props.spotify_library_albums && this.props.spotify_library_albums.indexOf(item.uri) > -1);
- break;
- case 'playlist':
- return (this.props.spotify_library_playlists && this.props.spotify_library_playlists.indexOf(item.uri) > -1);
- break;
- }
- return false;
- }
-
- /**
- * TODO: Currently the select track keys are the only details available. We need
- * the actual track object reference (including name and artists) to getTrack from LastFM
- **/
- isLoved(item = null){
- if (!item){
- return false
- }
-
- if (this.props.tracks[item.uri] === undefined){
- return false;
- }
- var track = this.props.tracks[item.uri];
-
- return (track.userloved !== undefined && track.userloved == "1");
- }
-
- canBeInLibrary(){
- if (!this.props.spotify_authorized){
- return false
- }
- return (helpers.uriSource(this.props.menu.items[0].uri) == 'spotify')
- }
-
- toggleInLibrary(e, in_library){
- this.props.uiActions.hideContextMenu()
- if (in_library){
- this.props.spotifyActions.following(this.props.menu.items[0].uri, 'DELETE')
- } else {
- this.props.spotifyActions.following(this.props.menu.items[0].uri, 'PUT')
- }
- }
-
- playQueueItem(e){
- this.props.uiActions.hideContextMenu()
- var tracks = this.props.menu.items;
- this.props.mopidyActions.changeTrack(tracks[0].tlid);
- }
-
- removeFromQueue(e){
- this.props.uiActions.hideContextMenu()
- var tracks = this.props.menu.items;
- var tracks_tlids = [];
- for(var i = 0; i < tracks.length; i++){
- tracks_tlids.push(tracks[i].tlid );
- }
- this.props.mopidyActions.removeTracks(tracks_tlids);
- }
-
- playURIs(e){
- this.props.uiActions.hideContextMenu();
- this.props.mopidyActions.playURIs(this.props.menu.uris, this.props.menu.tracklist_uri);
- }
-
- playPlaylist(e){
- this.props.uiActions.hideContextMenu();
- this.props.mopidyActions.playPlaylist(this.props.menu.uris[0]);
- }
-
- shufflePlayPlaylist(e){
- this.props.uiActions.hideContextMenu();
- this.props.mopidyActions.playPlaylist(this.props.menu.uris[0], true);
- }
-
- playArtistTopTracks(e){
- this.props.uiActions.hideContextMenu();
- this.props.spotifyActions.playArtistTopTracks(this.props.menu.uris[0]);
- }
-
- addToQueue(e, next = false){
- this.props.uiActions.hideContextMenu();
- this.props.mopidyActions.enqueueURIs(this.props.menu.uris, this.props.menu.tracklist_uri, next);
- }
-
- addTracksToPlaylist(e, playlist_uri){
- this.props.uiActions.hideContextMenu();
- this.props.coreActions.addTracksToPlaylist(playlist_uri, this.props.menu.uris);
- }
-
- toggleLoved(e, is_loved){
- this.props.uiActions.hideContextMenu();
- if (is_loved){
- this.props.lastfmActions.unloveTrack(this.props.menu.items[0].uri);
- } else {
- this.props.lastfmActions.loveTrack(this.props.menu.items[0].uri);
- }
- }
-
- unloveTrack(e){
- this.props.uiActions.hideContextMenu();
- this.props.lastfmActions.unloveTrack(this.props.menu.items[0]);
- }
-
- removeFromPlaylist(e){
- this.props.uiActions.hideContextMenu();
- this.props.coreActions.removeTracksFromPlaylist(this.props.menu.tracklist_uri, this.props.menu.indexes);
- }
-
- deletePlaylist(e){
- this.props.uiActions.hideContextMenu();
- this.props.coreActions.deletePlaylist(this.props.menu.uris[0]);
- }
-
- startRadio(e){
- this.props.uiActions.hideContextMenu();
- this.props.pusherActions.startRadio(this.props.menu.uris);
- }
-
- goToRecommendations(e){
- this.props.uiActions.hideContextMenu();
- var uris_string = helpers.arrayOf('uri',this.props.menu.items).join(',');
- hashHistory.push(global.baseURL +'discover/recommendations/'+ uris_string );
- }
-
- goToArtist(e){
- if (!this.props.menu.items || this.props.menu.items.length <= 0 || !this.props.menu.items[0].artists_uris || this.props.menu.items[0].artists_uris.length <= 0){
- return null;
- } else {
- this.props.uiActions.hideContextMenu();
-
- // note: we can only go to one artist (even if this item has multiple artists, just go to the first one)
- hashHistory.push(global.baseURL +'artist/'+ this.props.menu.items[0].artists_uris[0]);
- }
- }
-
- goToUser(e){
- if (!this.props.menu.items || this.props.menu.items.length <= 0 || !this.props.menu.items[0].user_uri){
- return null;
- } else {
- this.props.uiActions.hideContextMenu();
- hashHistory.push(global.baseURL +'user/'+ this.props.menu.items[0].user_uri);
- }
- }
-
- goToTrack(e){
- if (!this.props.menu.items || this.props.menu.items.length <= 0){
- return null;
- } else {
- this.props.uiActions.hideContextMenu();
- hashHistory.push(global.baseURL +'track/'+ encodeURIComponent(this.props.menu.items[0].uri));
- }
- }
-
- copyURIs(e){
- var temp = $(" ");
- $("body").append(temp);
- temp.val(this.props.menu.uris.join(',')).select();
- document.execCommand("copy");
- temp.remove();
-
- this.props.uiActions.createNotification({content: "Copied "+this.props.menu.uris.length+" URIs"});
- this.props.uiActions.hideContextMenu()
- }
-
- closeAndDeselectTracks(e){
- this.props.uiActions.hideContextMenu();
- // TODO
- }
-
- renderTitle(){
-
- var context = this.getContext()
-
- if (context.items_count > 1){
- return (
-
-
- {context.items_count} {context.nice_name}{context.items_count > 1 ? 's' : null} selected {this.props.uiActions.setSelectedTracks([]); this.props.uiActions.hideContextMenu()}}>
-
-
- )
- }
-
- if (context.name == 'custom'){
- if (!this.props.menu.title){
- return null;
- }
-
- return (
-
-
- {this.props.menu.title}
-
-
- )
- }
-
- // Do we need titles or does it just confuse things and rip off Spotify too hard?
- return null;
-/*
- switch (context.type){
-
- case 'artist':
- case 'album':
- case 'playlist':
- var style = null;
-
- return (
-
-
-
{context.item.name}
-
- {context.source}
-
-
- );
-
- default:
- return (
-
-
- {context.items_count} {context.nice_name}{context.items_count > 1 ? 's' : null}
-
-
- {context.source}
-
-
- );
-
- }*/
- }
-
- setSubmenu(name){
-
- if (this.state.submenu !== name && name == 'add-to-playlist'){
- if (!this.props.spotify_library_playlists_loaded_all){
- this.props.spotifyActions.getLibraryPlaylists()
- }
- if (!this.props.mopidy_library_playlists_loaded_all){
- this.props.mopidyActions.getLibraryPlaylists()
- }
- }
-
- this.setState({submenu: name});
- }
-
- renderSubmenu(){
- var list = null;
- var loader = null;
-
- switch (this.state.submenu){
-
- case 'add-to-playlist':
-
- var playlists = []
- for (var uri in this.props.playlists){
- if (this.props.playlists[uri].can_edit) playlists.push(this.props.playlists[uri])
- }
-
- playlists = helpers.sortItems(playlists, 'name')
-
- if (this.props.processes.SPOTIFY_GET_LIBRARY_PLAYLISTS_PROCESSOR && this.props.processes.SPOTIFY_GET_LIBRARY_PLAYLISTS_PROCESSOR.status == 'running'){
- loader = (
-
- )
- }
-
- list = No writable playlists
- if (playlists.length > 0){
- list = playlists.map(playlist => {
- return (
-
- this.addTracksToPlaylist(e,playlist.uri) }>
- { playlist.name }
-
-
- )
- })
- }
-
- }
-
- return (
-
- );
- }
-
- renderItems(){
- var context = this.getContext()
-
- var play_uris = (
-
- )
-
- var play_playlist = (
-
- )
-
- var shuffle_play_playlist = (
-
- )
-
- var play_queue_item = (
-
- )
-
- var play_uris_next = (
-
- )
-
- var play_artist_top_tracks = (
-
- )
-
- var add_to_queue = (
-
- )
-
- var add_to_playlist = (
-
- )
-
- var toggle_in_library = (
-
- )
-
- if (!this.props.lastfm_authorized){
- var toggle_loved = null;
- } else if (helpers.isLoading(this.props.load_queue,['lastfm_track.getInfo'])){
- var toggle_loved = (
-
- )
- } else {
- var toggle_loved = (
-
- )
- }
-
- var go_to_artist = (
-
- )
-
- var go_to_user = (
-
- )
-
- var go_to_track = (
-
- )
-
- var go_to_recommendations = (
-
- )
-
- var start_radio = (
-
- )
-
- var remove_from_queue = (
-
- )
-
- var remove_from_playlist = (
-
- )
-
- var delete_playlist = (
-
- )
-
- var copy_uris = (
-
- )
-
- switch (context.name){
-
- case 'album':
- return (
-
- {play_uris}
- {play_uris_next}
- {add_to_queue}
- {this.canBeInLibrary() ?
: null}
- {this.canBeInLibrary() ? toggle_in_library : null}
-
- {go_to_artist}
- {copy_uris}
-
- )
- break
-
- case 'artist':
- return (
-
- {context.source == 'spotify' ? play_artist_top_tracks : null}
- {context.source == 'spotify' ? start_radio : null}
- {this.canBeInLibrary() ?
: null}
- {this.canBeInLibrary() ? toggle_in_library : null}
-
- {context.source == 'spotify' ? go_to_recommendations : null}
- {copy_uris}
-
- )
- break
-
- case 'playlist':
- return (
-
- {play_playlist}
- {shuffle_play_playlist}
- {this.canBeInLibrary() ?
: null}
- {this.canBeInLibrary() ? toggle_in_library : null}
-
- {context.source == 'spotify' ? go_to_user : null}
- {copy_uris}
-
- )
- break
-
- case 'editable-playlist':
- return (
-
- {play_playlist}
- {shuffle_play_playlist}
- {this.canBeInLibrary() ?
: null}
- {this.canBeInLibrary() ? toggle_in_library : null}
-
- {context.source == 'spotify' ? go_to_user : null}
- {copy_uris}
-
- {delete_playlist}
-
- )
- break
-
- case 'queue-track':
- return (
-
- {context.items_count == 1 ? play_queue_item : null}
-
- {add_to_playlist}
- {toggle_loved}
-
- {context.source == 'spotify' && context.items_count <= 5 ? go_to_recommendations : null}
- {context.items_count == 1 ? go_to_track : null}
- {copy_uris}
-
- {remove_from_queue}
-
- )
- break
-
- case 'editable-playlist-track':
- return (
-
- {play_uris}
- {play_uris_next}
- {add_to_queue}
- {context.source == 'spotify' && context.items_count == 1 ? start_radio : null}
-
- {add_to_playlist}
- {toggle_loved}
-
- {context.source == 'spotify' && context.items_count <= 5 ? go_to_recommendations : null}
- {context.items_count == 1 ? go_to_track : null}
- {copy_uris}
-
- {remove_from_playlist}
-
- )
- break
-
- default:
- return (
-
- {play_uris}
- {play_uris_next}
- {add_to_queue}
- {context.source == 'spotify' && context.items_count == 1 ? start_radio : null}
-
- {add_to_playlist}
- {toggle_loved}
-
- {context.source == 'spotify' && context.items_count <= 5 ? go_to_recommendations : null}
- {context.items_count == 1 ? go_to_track : null}
-
- {copy_uris}
-
- )
- break
- }
- }
-
- render(){
- if (!this.props.menu){
- return null;
- }
-
- var style = {
- left: this.props.menu.position_x,
- top: this.props.menu.position_y,
- }
- var height = 200 // TODO: use jquery to detect height
- var className = "context-menu "+this.props.menu.context
- if (this.state.submenu){
- className += ' context-menu--submenu-expanded';
- }
-
- if (this.props.menu.closing){
- className += ' context-menu--closing';
- }
-
- if (this.props.menu.position_x > (window.innerWidth - 174)){
- style.left = 'auto';
- style.right = 10;
- }
-
- if (this.props.menu.position_y > (window.innerHeight - height)){
- style.top = 'auto';
- style.bottom = 10;
- }
-
- return (
-
- );
- }
-}
-
-const mapStateToProps = (state, ownProps) => {
- return {
- menu: state.ui.context_menu,
- load_queue: state.ui.load_queue,
- processes: state.ui.processes,
- current_track: state.core.current_track,
- current_tracklist: state.core.current_tracklist,
- spotify_library_playlists: state.spotify.library_playlists,
- spotify_library_playlists_loaded_all: state.spotify.library_playlists_loaded_all,
- mopidy_library_playlists: state.mopidy.library_playlists,
- mopidy_library_playlists_loaded_all: state.mopidy.library_playlists_loaded_all,
- spotify_library_artists: state.spotify.library_artists,
- mopidy_library_artists: state.mopidy.library_artists,
- spotify_library_albums: state.spotify.library_albums,
- mopidy_library_albums: state.mopidy.library_albums,
- playlists: state.core.playlists,
- tracks: state.core.tracks,
- spotify_authorized: state.spotify.authorization,
- lastfm_authorized: state.lastfm.authorization
- }
-}
-
-const mapDispatchToProps = (dispatch) => {
- return {
- coreActions: bindActionCreators(coreActions, dispatch),
- uiActions: bindActionCreators(uiActions, dispatch),
- pusherActions: bindActionCreators(pusherActions, dispatch),
- spotifyActions: bindActionCreators(spotifyActions, dispatch),
- lastfmActions: bindActionCreators(lastfmActions, dispatch),
- mopidyActions: bindActionCreators(mopidyActions, dispatch)
- }
-}
-
-export default connect(mapStateToProps, mapDispatchToProps)(ContextMenu)
\ No newline at end of file
+
+import React, { PropTypes } from 'react'
+import { hashHistory } from 'react-router'
+import Link from './Link'
+import { connect } from 'react-redux'
+import { bindActionCreators } from 'redux'
+
+import TrackList from './TrackList'
+import Icon from './Icon'
+import Thumbnail from './Thumbnail'
+
+import * as helpers from '../helpers'
+import * as coreActions from '../services/core/actions'
+import * as uiActions from '../services/ui/actions'
+import * as pusherActions from '../services/pusher/actions'
+import * as mopidyActions from '../services/mopidy/actions'
+import * as lastfmActions from '../services/lastfm/actions'
+import * as spotifyActions from '../services/spotify/actions'
+
+class ContextMenu extends React.Component{
+
+ constructor(props){
+ super(props)
+ this.state = {
+ submenu: null
+ }
+ this.handleScroll = this.handleScroll.bind(this);
+ this.handleMouseDown = this.handleMouseDown.bind(this);
+ this.handleTouchStart = this.handleTouchStart.bind(this);
+ }
+
+ componentDidMount(){
+ window.addEventListener("scroll", this.handleScroll, false);
+ window.addEventListener("mousedown", this.handleMouseDown, false);
+ window.addEventListener("touchstart", this.handleTouchStart, false);
+ }
+
+ componentWillUnmount(){
+ window.removeEventListener("scroll", this.handleScroll, false);
+ window.removeEventListener("mousedown", this.handleMouseDown, false);
+ window.removeEventListener("touchstart", this.handleTouchStart, false);
+ }
+
+ componentWillReceiveProps(nextProps){
+
+ // if we've been given a menu object (ie activated) when we didn't have one prior
+ if (nextProps.menu && !this.props.menu){
+ this.setState({ submenu: null });
+
+ var context = this.getContext(nextProps);
+
+ // if we're able to be in the library, run a check
+ if (nextProps.spotify_authorized && context.source == 'spotify'){
+ switch (nextProps.menu.context){
+ case 'artist':
+ case 'album':
+ case 'playlist':
+ case 'editable-playlist':
+ case 'track':
+ case 'playlist-track':
+ case 'editable-playlist-track':
+ case 'queue-track':
+ this.props.spotifyActions.following(nextProps.menu.items[0].uri)
+ break
+ }
+ }
+
+ // if we're able to be in the LastFM library, run a check
+ if (nextProps.lastfm_authorized && context.is_track && context.items_count == 1){
+ if (nextProps.menu.items[0].uri && this.props.tracks[nextProps.menu.items[0].uri] !== undefined && this.props.tracks[nextProps.menu.items[0].uri].userloved === undefined){
+ this.props.lastfmActions.getTrack(nextProps.menu.items[0].uri);
+ }
+ }
+ }
+ }
+
+ handleScroll(e){
+ if (this.props.menu){
+ this.props.uiActions.hideContextMenu();
+ }
+ }
+
+ handleMouseDown(e){
+ // if we click (touch or mouse) outside of the context menu or context menu trigger, kill it
+ if ($(e.target).closest('.context-menu').length <= 0 && $(e.target).closest('.context-menu-trigger').length <= 0){
+ this.props.uiActions.hideContextMenu();
+ }
+ }
+
+ handleTouchStart(e){
+
+ // if we click (touch or mouse) outside of the context menu or context menu trigger, kill it
+ if ($(e.target).closest('.context-menu').length <= 0 && $(e.target).closest('.context-menu-trigger').length <= 0){
+ this.props.uiActions.hideContextMenu();
+ }
+ }
+
+ getContext(props = this.props){
+ var context = {
+ name: null,
+ nice_name: 'Unknown',
+ is_track: false
+ }
+
+ if (props.menu && props.menu.context){
+ context.name = props.menu.context;
+ context.nice_name = props.menu.context;
+
+ // handle ugly labels
+ switch (props.menu.context){
+ case 'playlist':
+ case 'editable-playlist':
+ context.nice_name = 'playlist';
+ break;
+
+ case 'track':
+ case 'queue-track':
+ case 'playlist-track':
+ case 'editable-playlist-track':
+ context.nice_name = 'track';
+ context.is_track = true;
+ break;
+ }
+
+ // Consider the object(s) themselves
+ // We can only really accommodate the first item. The only instances where
+ // there is multiple is tracklists, when they're all of the same source (except search?)
+ if (props.menu.items && props.menu.items.length > 0){
+ var item = props.menu.items[0]
+ context.item = item;
+ context.items_count = props.menu.items.length;
+ context.source = helpers.uriSource(item.uri);
+ context.type = helpers.uriType(item.uri);
+ context.in_library = this.inLibrary(item);
+ context.is_loved = this.isLoved(item);
+ }
+ }
+
+ return context
+ }
+
+ inLibrary(item = null){
+ if (!item){
+ return false;
+ }
+
+ switch (helpers.uriType(item.uri)){
+ case 'artist':
+ return (this.props.spotify_library_artists && this.props.spotify_library_artists.indexOf(item.uri) > -1);
+ break;
+ case 'album':
+ return (this.props.spotify_library_albums && this.props.spotify_library_albums.indexOf(item.uri) > -1);
+ break;
+ case 'playlist':
+ return (this.props.spotify_library_playlists && this.props.spotify_library_playlists.indexOf(item.uri) > -1);
+ break;
+ case 'track':
+ return (this.props.spotify_library_tracks && this.props.spotify_library_tracks.indexOf(item.uri) > -1);
+ break;
+ }
+ return false;
+ }
+
+ /**
+ * TODO: Currently the select track keys are the only details available. We need
+ * the actual track object reference (including name and artists) to getTrack from LastFM
+ **/
+ isLoved(item = null){
+ if (!item){
+ return false
+ }
+
+ if (this.props.tracks[item.uri] === undefined){
+ return false;
+ }
+ var track = this.props.tracks[item.uri];
+
+ return (track.userloved !== undefined && track.userloved == "1");
+ }
+
+ canBeInLibrary(){
+ if (!this.props.spotify_authorized){
+ return false
+ }
+ return (helpers.uriSource(this.props.menu.items[0].uri) == 'spotify')
+ }
+
+ toggleInLibrary(e, in_library){
+ this.props.uiActions.hideContextMenu()
+ if (in_library){
+ this.props.spotifyActions.following(this.props.menu.items[0].uri, 'DELETE')
+ } else {
+ this.props.spotifyActions.following(this.props.menu.items[0].uri, 'PUT')
+ }
+ }
+
+ playQueueItem(e){
+ this.props.uiActions.hideContextMenu()
+ var tracks = this.props.menu.items;
+ this.props.mopidyActions.changeTrack(tracks[0].tlid);
+ }
+
+ removeFromQueue(e){
+ this.props.uiActions.hideContextMenu()
+ var tracks = this.props.menu.items;
+ var tracks_tlids = [];
+ for(var i = 0; i < tracks.length; i++){
+ tracks_tlids.push(tracks[i].tlid );
+ }
+ this.props.mopidyActions.removeTracks(tracks_tlids);
+ }
+
+ playURIs(e){
+ this.props.uiActions.hideContextMenu();
+ this.props.mopidyActions.playURIs(this.props.menu.uris, this.props.menu.tracklist_uri);
+ }
+
+ playPlaylist(e){
+ this.props.uiActions.hideContextMenu();
+ this.props.mopidyActions.playPlaylist(this.props.menu.uris[0]);
+ }
+
+ shufflePlayPlaylist(e){
+ this.props.uiActions.hideContextMenu();
+ this.props.mopidyActions.playPlaylist(this.props.menu.uris[0], true);
+ }
+
+ playArtistTopTracks(e){
+ this.props.uiActions.hideContextMenu();
+ this.props.spotifyActions.playArtistTopTracks(this.props.menu.uris[0]);
+ }
+
+ addToQueue(e, next = false){
+ this.props.uiActions.hideContextMenu();
+ this.props.mopidyActions.enqueueURIs(this.props.menu.uris, this.props.menu.tracklist_uri, next);
+ }
+
+ addTracksToPlaylist(e, playlist_uri){
+ this.props.uiActions.hideContextMenu();
+ this.props.coreActions.addTracksToPlaylist(playlist_uri, this.props.menu.uris);
+ }
+
+ toggleLoved(e, is_loved){
+ this.props.uiActions.hideContextMenu();
+ if (is_loved){
+ this.props.lastfmActions.unloveTrack(this.props.menu.items[0].uri);
+ } else {
+ this.props.lastfmActions.loveTrack(this.props.menu.items[0].uri);
+ }
+ }
+
+ unloveTrack(e){
+ this.props.uiActions.hideContextMenu();
+ this.props.lastfmActions.unloveTrack(this.props.menu.items[0]);
+ }
+
+ removeFromPlaylist(e){
+ this.props.uiActions.hideContextMenu();
+ this.props.coreActions.removeTracksFromPlaylist(this.props.menu.tracklist_uri, this.props.menu.indexes);
+ }
+
+ deletePlaylist(e){
+ this.props.uiActions.hideContextMenu();
+ this.props.coreActions.deletePlaylist(this.props.menu.uris[0]);
+ }
+
+ startRadio(e){
+ this.props.uiActions.hideContextMenu();
+ this.props.pusherActions.startRadio(this.props.menu.uris);
+ }
+
+ goToRecommendations(e){
+ this.props.uiActions.hideContextMenu();
+ var uris_string = helpers.arrayOf('uri',this.props.menu.items).join(',');
+ hashHistory.push(global.baseURL +'discover/recommendations/'+ uris_string );
+ }
+
+ goToArtist(e){
+ if (!this.props.menu.items || this.props.menu.items.length <= 0 || !this.props.menu.items[0].artists_uris || this.props.menu.items[0].artists_uris.length <= 0){
+ return null;
+ } else {
+ this.props.uiActions.hideContextMenu();
+
+ // note: we can only go to one artist (even if this item has multiple artists, just go to the first one)
+ hashHistory.push(global.baseURL +'artist/'+ this.props.menu.items[0].artists_uris[0]);
+ }
+ }
+
+ goToUser(e){
+ if (!this.props.menu.items || this.props.menu.items.length <= 0 || !this.props.menu.items[0].user_uri){
+ return null;
+ } else {
+ this.props.uiActions.hideContextMenu();
+ hashHistory.push(global.baseURL +'user/'+ this.props.menu.items[0].user_uri);
+ }
+ }
+
+ goToTrack(e){
+ if (!this.props.menu.items || this.props.menu.items.length <= 0){
+ return null;
+ } else {
+ this.props.uiActions.hideContextMenu();
+ hashHistory.push(global.baseURL +'track/'+ encodeURIComponent(this.props.menu.items[0].uri));
+ }
+ }
+
+ copyURIs(e){
+ var temp = $(" ");
+ $("body").append(temp);
+ temp.val(this.props.menu.uris.join(',')).select();
+ document.execCommand("copy");
+ temp.remove();
+
+ this.props.uiActions.createNotification({content: "Copied "+this.props.menu.uris.length+" URIs"});
+ this.props.uiActions.hideContextMenu()
+ }
+
+ closeAndDeselectTracks(e){
+ this.props.uiActions.hideContextMenu();
+ // TODO
+ }
+
+ renderTitle(){
+
+ var context = this.getContext()
+
+ if (context.items_count > 1){
+ return (
+
+
+ {context.items_count} {context.nice_name}{context.items_count > 1 ? 's' : null} selected {this.props.uiActions.setSelectedTracks([]); this.props.uiActions.hideContextMenu()}}>
+
+
+ )
+ }
+
+ if (context.name == 'custom'){
+ if (!this.props.menu.title){
+ return null;
+ }
+
+ return (
+
+
+ {this.props.menu.title}
+
+
+ )
+ }
+
+ // Do we need titles or does it just confuse things and rip off Spotify too hard?
+ return null;
+/*
+ switch (context.type){
+
+ case 'artist':
+ case 'album':
+ case 'playlist':
+ var style = null;
+
+ return (
+
+
+
{context.item.name}
+
+ {context.source}
+
+
+ );
+
+ default:
+ return (
+
+
+ {context.items_count} {context.nice_name}{context.items_count > 1 ? 's' : null}
+
+
+ {context.source}
+
+
+ );
+
+ }*/
+ }
+
+ setSubmenu(name){
+
+ if (this.state.submenu !== name && name == 'add-to-playlist'){
+ if (!this.props.spotify_library_playlists_loaded_all){
+ this.props.spotifyActions.getLibraryPlaylists()
+ }
+ if (!this.props.mopidy_library_playlists_loaded_all){
+ this.props.mopidyActions.getLibraryPlaylists()
+ }
+ }
+
+ this.setState({submenu: name});
+ }
+
+ renderSubmenu(){
+ var list = null;
+ var loader = null;
+
+ switch (this.state.submenu){
+
+ case 'add-to-playlist':
+
+ var playlists = []
+ for (var uri in this.props.playlists){
+ if (this.props.playlists[uri].can_edit) playlists.push(this.props.playlists[uri])
+ }
+
+ playlists = helpers.sortItems(playlists, 'name')
+
+ if (this.props.processes.SPOTIFY_GET_LIBRARY_PLAYLISTS_PROCESSOR && this.props.processes.SPOTIFY_GET_LIBRARY_PLAYLISTS_PROCESSOR.status == 'running'){
+ loader = (
+
+ )
+ }
+
+ list = No writable playlists
+ if (playlists.length > 0){
+ list = playlists.map(playlist => {
+ return (
+
+ this.addTracksToPlaylist(e,playlist.uri) }>
+ { playlist.name }
+
+
+ )
+ })
+ }
+
+ }
+
+ return (
+
+ );
+ }
+
+ renderItems(){
+ var context = this.getContext()
+
+ var play_uris = (
+
+ )
+
+ var play_playlist = (
+
+ )
+
+ var shuffle_play_playlist = (
+
+ )
+
+ var play_queue_item = (
+
+ )
+
+ var play_uris_next = (
+
+ )
+
+ var play_artist_top_tracks = (
+
+ )
+
+ var add_to_queue = (
+
+ )
+
+ var add_to_playlist = (
+
+ )
+
+ var toggle_in_library = (
+
+ )
+
+ if (!this.props.spotify_authorized){
+ var toggle_in_library = null;
+ } else if (helpers.isLoading(this.props.load_queue,['spotify_me/tracks/contains','spotify_me/playlists/contains','spotify_me/albums/contains','spotify_me/artists/contains'])){
+ var toggle_in_library = (
+
+ )
+ } else {
+ var toggle_in_library = (
+
+ )
+ }
+
+ if (!this.props.lastfm_authorized){
+ var toggle_loved = null;
+ } else if (helpers.isLoading(this.props.load_queue,['lastfm_track.getInfo'])){
+ var toggle_loved = (
+
+ )
+ } else {
+ var toggle_loved = (
+
+ )
+ }
+
+ var go_to_artist = (
+
+ )
+
+ var go_to_user = (
+
+ )
+
+ var go_to_track = (
+
+ )
+
+ var go_to_recommendations = (
+
+ )
+
+ var start_radio = (
+
+ )
+
+ var remove_from_queue = (
+
+ )
+
+ var remove_from_playlist = (
+
+ )
+
+ var delete_playlist = (
+
+ )
+
+ var copy_uris = (
+
+ )
+
+ switch (context.name){
+
+ case 'album':
+ return (
+
+ {play_uris}
+ {play_uris_next}
+ {add_to_queue}
+ {this.canBeInLibrary() ?
: null}
+ {this.canBeInLibrary() ? toggle_in_library : null}
+
+ {go_to_artist}
+ {copy_uris}
+
+ )
+ break
+
+ case 'artist':
+ return (
+
+ {context.source == 'spotify' ? play_artist_top_tracks : null}
+ {context.source == 'spotify' ? start_radio : null}
+ {this.canBeInLibrary() ?
: null}
+ {this.canBeInLibrary() ? toggle_in_library : null}
+
+ {context.source == 'spotify' ? go_to_recommendations : null}
+ {copy_uris}
+
+ )
+ break
+
+ case 'playlist':
+ return (
+
+ {play_playlist}
+ {shuffle_play_playlist}
+ {this.canBeInLibrary() ?
: null}
+ {this.canBeInLibrary() ? toggle_in_library : null}
+
+ {context.source == 'spotify' ? go_to_user : null}
+ {copy_uris}
+
+ )
+ break
+
+ case 'editable-playlist':
+ return (
+
+ {play_playlist}
+ {shuffle_play_playlist}
+ {this.canBeInLibrary() ?
: null}
+ {this.canBeInLibrary() ? toggle_in_library : null}
+
+ {context.source == 'spotify' ? go_to_user : null}
+ {copy_uris}
+
+ {delete_playlist}
+
+ )
+ break
+
+ case 'queue-track':
+ return (
+
+ {context.items_count == 1 ? play_queue_item : null}
+
+ {add_to_playlist}
+ {this.canBeInLibrary() ? toggle_in_library : null}
+ {toggle_loved}
+
+ {context.source == 'spotify' && context.items_count <= 5 ? go_to_recommendations : null}
+ {context.items_count == 1 ? go_to_track : null}
+ {copy_uris}
+
+ {remove_from_queue}
+
+ )
+ break
+
+ case 'editable-playlist-track':
+ return (
+
+ {play_uris}
+ {play_uris_next}
+ {add_to_queue}
+ {context.source == 'spotify' && context.items_count == 1 ? start_radio : null}
+
+ {add_to_playlist}
+ {this.canBeInLibrary() ? toggle_in_library : null}
+ {toggle_loved}
+
+ {context.source == 'spotify' && context.items_count <= 5 ? go_to_recommendations : null}
+ {context.items_count == 1 ? go_to_track : null}
+ {copy_uris}
+
+ {remove_from_playlist}
+
+ )
+ break
+
+ default:
+ return (
+
+ {play_uris}
+ {play_uris_next}
+ {add_to_queue}
+ {context.source == 'spotify' && context.items_count == 1 ? start_radio : null}
+
+ {add_to_playlist}
+ {this.canBeInLibrary() ? toggle_in_library : null}
+ {toggle_loved}
+
+ {context.source == 'spotify' && context.items_count <= 5 ? go_to_recommendations : null}
+ {context.items_count == 1 ? go_to_track : null}
+
+ {copy_uris}
+
+ )
+ break
+ }
+ }
+
+ render(){
+ if (!this.props.menu){
+ return null;
+ }
+
+ var style = {
+ left: this.props.menu.position_x,
+ top: this.props.menu.position_y,
+ }
+ var height = 200 // TODO: use jquery to detect height
+ var className = "context-menu "+this.props.menu.context
+ if (this.state.submenu){
+ className += ' context-menu--submenu-expanded';
+ }
+
+ if (this.props.menu.closing){
+ className += ' context-menu--closing';
+ }
+
+ if (this.props.menu.position_x > (window.innerWidth - 174)){
+ style.left = 'auto';
+ style.right = 10;
+ }
+
+ if (this.props.menu.position_y > (window.innerHeight - height)){
+ style.top = 'auto';
+ style.bottom = 10;
+ }
+
+ return (
+
+ );
+ }
+}
+
+const mapStateToProps = (state, ownProps) => {
+ return {
+ menu: state.ui.context_menu,
+ load_queue: state.ui.load_queue,
+ processes: state.ui.processes,
+ current_track: state.core.current_track,
+ current_tracklist: state.core.current_tracklist,
+ spotify_library_playlists: state.spotify.library_playlists,
+ spotify_library_playlists_loaded_all: state.spotify.library_playlists_loaded_all,
+ mopidy_library_playlists: state.mopidy.library_playlists,
+ mopidy_library_playlists_loaded_all: state.mopidy.library_playlists_loaded_all,
+ spotify_library_artists: state.spotify.library_artists,
+ mopidy_library_artists: state.mopidy.library_artists,
+ spotify_library_albums: state.spotify.library_albums,
+ mopidy_library_albums: state.mopidy.library_albums,
+ spotify_library_tracks: state.spotify.library_tracks,
+ playlists: state.core.playlists,
+ tracks: state.core.tracks,
+ spotify_authorized: state.spotify.authorization,
+ lastfm_authorized: state.lastfm.authorization
+ }
+}
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ coreActions: bindActionCreators(coreActions, dispatch),
+ uiActions: bindActionCreators(uiActions, dispatch),
+ pusherActions: bindActionCreators(pusherActions, dispatch),
+ spotifyActions: bindActionCreators(spotifyActions, dispatch),
+ lastfmActions: bindActionCreators(lastfmActions, dispatch),
+ mopidyActions: bindActionCreators(mopidyActions, dispatch)
+ }
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(ContextMenu)
diff --git a/src/js/services/spotify/reducer.js b/src/js/services/spotify/reducer.js
index e3f20a42..5c0c7e57 100755
--- a/src/js/services/spotify/reducer.js
+++ b/src/js/services/spotify/reducer.js
@@ -307,6 +307,16 @@ export default function reducer(spotify = {}, action){
}
return Object.assign({}, spotify, { library_playlists: items });
+ case 'SPOTIFY_LIBRARY_TRACK_CHECK':
+ var items = Object.assign([], spotify.library_tracks)
+ var index = items.indexOf(action.key)
+ if (index > -1 && !action.in_library) {
+ items.splice(index, 1)
+ } else if (index < 0 && action.in_library){
+ items.push(action.key)
+ }
+ return Object.assign({}, spotify, { library_tracks: items });
+
/**
* Searching
diff --git a/src/js/views/library/LibraryTracks.js b/src/js/views/library/LibraryTracks.js
index 9030ae26..e62a3659 100755
--- a/src/js/views/library/LibraryTracks.js
+++ b/src/js/views/library/LibraryTracks.js
@@ -46,7 +46,9 @@ class LibraryTracks extends React.Component{
}
render(){
- if (helpers.isLoading(this.props.load_queue,['spotify_me/tracks'])){
+
+ // Note trailing "?" makes sure our context menu in_library checks doesn't interfere
+ if (helpers.isLoading(this.props.load_queue,['spotify_me/tracks?'])){
return (