import React from 'react'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import { withRouter } from 'react-router'; import { uriSource, uriType, getFromUri, buildLink, isLoading, throttle, } from '../util/helpers'; import { arrayOf, sortItems, } from '../util/arrays'; import Link from './Link'; import Icon from './Icon'; import Loader from './Loader'; import URILink from './URILink'; 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 = throttle(this.handleScroll.bind(this), 50); } componentDidMount = () => { window.addEventListener('scroll', this.handleScroll, false); } componentWillUnmount = () => { window.removeEventListener('scroll', this.handleScroll, false); } componentDidUpdate = (prevProps) => { const { menu: prevMenu, tracks: prevTracks, } = prevProps; const { menu, tracks, lastfm_authorized, spotify_authorized, spotifyActions, lastfmActions, } = this.props; // if we've been given a menu object (ie activated) when we didn't have one prior if (!prevMenu && menu) { this.setState({ submenu: null }); const context = this.getContext(this.props); // if we're able to be in the library, run a check if (spotify_authorized && context.source === 'spotify') { switch (menu.context) { case 'artist': case 'album': case 'playlist': case 'editable-playlist': case 'track': case 'playlist-track': case 'editable-playlist-track': case 'queue-track': spotifyActions.following(menu.items[0].uri); break; default: break; } } // if we're able to be in the LastFM library, run a check if (lastfm_authorized && context.is_track && context.items_count == 1) { if (menu.items[0].uri && prevTracks[menu.items[0].uri] !== undefined && tracks[menu.items[0].uri].userloved === undefined) { lastfmActions.getTrack(menu.items[0].uri); } } } } handleScroll = () => { const { menu, uiActions: { hideContextMenu } } = this.props; if (menu) hideContextMenu(); } getContext(props = this.props) { const { menu, } = props; const context = { name: null, nice_name: 'Unknown', is_track: false, }; if (menu && menu.context) { context.name = menu.context; context.nice_name = menu.context; // handle ugly labels switch (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 (menu.items && menu.items.length > 0) { const item = menu.items[0]; context.item = item; context.items_count = menu.items.length; context.source = uriSource(item.uri); context.type = uriType(item.uri); context.in_library = this.inLibrary(item); context.is_loved = this.isLoved(item); } } return context; } inLibrary = ({ uri } = {}) => { const { spotify_library_artists, spotify_library_albums, spotify_library_playlists, spotify_library_tracks, } = this.props; if (!uri) return false; switch (uriType(uri)) { case 'artist': return (spotify_library_artists && spotify_library_artists.indexOf(uri) > -1); case 'album': return (spotify_library_albums && spotify_library_albums.indexOf(uri) > -1); case 'playlist': return (spotify_library_playlists && spotify_library_playlists.indexOf(uri) > -1); case 'track': return (spotify_library_tracks && spotify_library_tracks.indexOf(uri) > -1); default: 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 = ({ uri } = {}) => { const { tracks, } = this.props; if (!uri) return false; if (tracks[uri] === undefined) return false; const track = tracks[uri]; return (track.userloved !== undefined && track.userloved == '1'); } canBeInLibrary = () => { const { spotify_authorized, menu: { items } } = this.props; if (!spotify_authorized) return false; return (uriSource(items[0].uri) === 'spotify'); } toggleInLibrary = (e, in_library) => { const { uiActions: { hideContextMenu, }, spotifyActions: { following, }, menu: { items, } = {}, } = this.props; hideContextMenu(); if (in_library) { following(items[0].uri, 'DELETE'); } else { following(items[0].uri, 'PUT'); } } playQueueItem = () => { const { uiActions: { hideContextMenu, }, mopidyActions: { changeTrack, }, menu: { items, } = {}, } = this.props; hideContextMenu(); changeTrack(items[0].tlid); } removeFromQueue = () => { const { uiActions: { hideContextMenu, }, mopidyActions: { removeTracks, }, menu: { items, } = {}, } = this.props; hideContextMenu(); removeTracks(arrayOf('tlid', items)); } playURIs = () => { const { uiActions: { hideContextMenu, }, mopidyActions: { playURIs, }, menu: { uris, tracklist_uri, } = {}, } = this.props; hideContextMenu(); playURIs(uris, tracklist_uri); } playPlaylist = () => { const { uiActions: { hideContextMenu, }, mopidyActions: { playPlaylist, }, menu: { uris, } = {}, } = this.props; hideContextMenu(); playPlaylist(uris[0]); } enqueuePlaylist = (e, play_next = false) => { const { uiActions: { hideContextMenu, }, mopidyActions: { enqueuePlaylist, }, menu: { uris, } = {}, } = this.props; hideContextMenu(); enqueuePlaylist(uris[0], play_next); } shufflePlayPlaylist(e) { const { uiActions: { hideContextMenu, }, mopidyActions: { playPlaylist, }, menu: { uris, } = {}, } = this.props; hideContextMenu(); playPlaylist(uris[0], true); } playArtistTopTracks = () => { const { uiActions: { hideContextMenu, }, spotifyActions: { playArtistTopTracks, }, menu: { uris, } = {}, } = this.props; hideContextMenu(); playArtistTopTracks(uris[0]); } addToQueue = (e, play_next = false) => { const { uiActions: { hideContextMenu, }, mopidyActions: { enqueueURIs, }, menu: { uris, tracklist_uri, } = {}, } = this.props; hideContextMenu(); enqueueURIs(uris, tracklist_uri, play_next); } addTracksToPlaylist = (e, playlist_uri) => { const { uiActions: { hideContextMenu, }, mopidyActions: { addTracksToPlaylist, }, menu: { uris, } = {}, } = this.props; hideContextMenu(); addTracksToPlaylist(playlist_uri, uris); } toggleLoved = (e, is_loved) => { const { uiActions: { hideContextMenu, }, lastfmActions: { unloveTrack, loveTrack, }, menu: { uris, } = {}, } = this.props; hideContextMenu(); if (is_loved) { unloveTrack(uris[0]); } else { loveTrack(uris[0]); } } unloveTrack = () => { const { uiActions: { hideContextMenu, }, lastfmActions: { unloveTrack, }, menu: { items, } = {}, } = this.props; hideContextMenu(); unloveTrack(items[0]); } removeFromPlaylist = () => { const { uiActions: { hideContextMenu, }, coreActions: { removeTracksFromPlaylist, }, menu: { tracklist_uri, indexes, } = {}, } = this.props; hideContextMenu(); removeTracksFromPlaylist(tracklist_uri, indexes); } deletePlaylist = () => { const { uiActions: { hideContextMenu, }, coreActions: { deletePlaylist, }, menu: { uris, } = {}, } = this.props; hideContextMenu(); deletePlaylist(uris[0]); } startRadio(e) { const { uiActions: { hideContextMenu, }, pusherActions: { startRadio, }, menu: { uris, } = {}, } = this.props; hideContextMenu(); startRadio(uris); } goToRecommendations(e) { const { uiActions: { hideContextMenu, }, menu: { items, } = {}, history: { push, }, } = this.props; hideContextMenu(); push(`/discover/recommendations/${arrayOf('uri', items).join(',')}`); } goToArtist = () => { const { uiActions: { hideContextMenu, }, menu: { items, } = {}, history: { push, }, } = this.props; if (!items || items.length <= 0 || !items[0].artists_uris || items[0].artists_uris.length <= 0) { return null; } hideContextMenu(); // note: we can only go to one artist (even if this item has multiple artists, just go to the first one) push(buildLink(items[0].artists_uris[0])); } goToUser = () => { const { uiActions: { hideContextMenu, }, menu: { items, } = {}, history: { push, }, } = this.props; if (!items || items.length <= 0 || !items[0].user_uri) return null; hideContextMenu(); push(buildLink(items[0].user_uri)); } goToTrack = () => { const { uiActions: { hideContextMenu, }, menu: { uris, } = {}, history: { push, }, } = this.props; if (!uris) return null; hideContextMenu(); push(buildLink(uris[0])); } copyURIs = () => { const { uiActions: { hideContextMenu, createNotification, }, menu: { uris, } = {}, } = this.props; const temp = $(''); $('body').append(temp); temp.val(uris.join(',')).select(); document.execCommand('copy'); temp.remove(); createNotification({ content: `Copied ${uris.length} URIs` }); hideContextMenu(); } renderTitle = () => { const { uiActions: { hideContextMenu, setSelectedTracks, }, queue_metadata, menu: { title, }, } = this.props; const context = this.getContext(); if (context.items_count > 1) { return (
{`${context.items_count} ${context.nice_name}${context.items_count > 1 ? 's' : ''} selected`} { setSelectedTracks([]); }}>
); } if (context.items_count == 1 && context.name == 'queue-track' && context.item !== undefined) { if (queue_metadata[`tlid_${context.item.tlid}`] !== undefined) { const metadata = queue_metadata[`tlid_${context.item.tlid}`]; if (metadata.added_from && metadata.added_by) { const type = (metadata.added_from ? uriType(metadata.added_from) : null); switch (type) { case 'discover': var link = discover; break; case 'browse': var link = browse; break; case 'search': var link = search; break; default: var link = {type}; } return (
{`${metadata.added_by} added from `} {link}
); } } } if (context.name == 'custom') { if (!title) return null; return (
{title}
); } } setSubmenu = (name) => { const { submenu } = this.state; const { spotify_library_playlists_loaded_all, mopidy_library_playlists_loaded_all, spotifyActions, mopidyActions, } = this.props; if (submenu !== name && name == 'add-to-playlist') { if (!spotify_library_playlists_loaded_all) spotifyActions.getLibraryPlaylists(); if (!mopidy_library_playlists_loaded_all) mopidyActions.getLibraryPlaylists(); } this.setState({ submenu: name }); } // THIS IS WHERE I GOT TO renderSubmenu() { let list = null; let isLoading = false; switch (this.state.submenu) { case 'add-to-playlist': var playlists = []; for (const uri in this.props.playlists) { if (this.props.playlists[uri].can_edit) playlists.push(this.props.playlists[uri]); } playlists = sortItems(playlists, 'name'); if (this.props.processes.SPOTIFY_GET_LIBRARY_PLAYLISTS_PROCESSOR && this.props.processes.SPOTIFY_GET_LIBRARY_PLAYLISTS_PROCESSOR.status == 'running') { isLoading = true; } list = No writable playlists; if (playlists.length > 0) { list = playlists.map((playlist) => ( this.addTracksToPlaylist(e, playlist.uri)}> { playlist.name } )); } } return (
this.setState({ submenu: null })}> {' '} Back
{list} {isLoading && (
)}
); } renderItems() { const context = this.getContext(); const play_uris = (
this.playURIs(e)}> Play
); const play_playlist = (
this.playPlaylist(e)}> Play
); const shuffle_play_playlist = (
this.shufflePlayPlaylist(e)}> Shuffle play
); const play_queue_item = (
this.playQueueItem(e)}> Play
); const play_uris_next = (
this.addToQueue(e, true)}> Play next
); const play_artist_top_tracks = (
this.playArtistTopTracks(e)}> Play top tracks
); const add_to_queue = (
this.addToQueue(e)}> Add to queue
); const add_playlist_to_queue = (
this.enqueuePlaylist(e)}> Add to queue
); const play_playlist_next = (
this.enqueuePlaylist(e, true)}> Play next
); const add_to_playlist = (
this.setSubmenu('add-to-playlist')}> Add to playlist
); var toggle_in_library = (
this.toggleInLibrary(e, context.in_library)}> {context.in_library ? 'Remove from library' : 'Add to library'}
); if (!this.props.spotify_authorized) { var toggle_in_library = null; } else if (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 = (
Add to library
); } else { var toggle_in_library = (
this.toggleInLibrary(e, context.in_library)}> {context.in_library ? 'Remove from library' : 'Add to library'}
); } if (!this.props.lastfm_authorized) { var toggle_loved = null; } else if (isLoading(this.props.load_queue, ['lastfm_track.getInfo'])) { var toggle_loved = (
Love track
); } else { var toggle_loved = (
this.toggleLoved(e, context.is_loved)}> {context.is_loved ? 'Unlove' : 'Love'} {' '} track
); } const go_to_artist = (
this.goToArtist(e)}> Go to artist
); const go_to_user = (
this.goToUser(e)}> Go to user
); const go_to_track = (
this.goToTrack(e)}> Track info
); const go_to_recommendations = (
this.goToRecommendations(e)}> Discover similar
); const start_radio = (
this.startRadio(e)}> Start radio
); const remove_from_queue = (
this.removeFromQueue(e)}> Remove
); const remove_from_playlist = (
this.removeFromPlaylist(e)}> Remove
); const edit_playlist = (
Edit
); const delete_playlist = (
this.deletePlaylist(e)}> Delete
); const copy_uris = (
this.copyURIs(e)}> Copy URI {context.items_count > 1 ? 's' : ''}
); 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} {play_playlist_next} {shuffle_play_playlist} {add_playlist_to_queue} {this.canBeInLibrary() ?
: null} {this.canBeInLibrary() ? toggle_in_library : null}
{context.source == 'spotify' ? go_to_user : null} {copy_uris} {context.items_count == 1 && context.item.can_edit ? (
{edit_playlist} {delete_playlist}
) : null}
); 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() { const { menu, uiActions: { hideContextMenu, }, } = this.props; const { submenu } = this.state; if (!menu) return null; const style = { left: menu.position_x, top: menu.position_y, }; const height = 200; // TODO: use jquery to detect height let className = `context-menu ${menu.context}`; if (submenu) className += ' context-menu--submenu-expanded'; if (menu.closing) className += ' context-menu--closing'; if (menu.position_x > (window.innerWidth - 174)) { style.left = 'auto'; style.right = 10; } if (menu.position_y > (window.innerHeight - height)) { style.top = 'auto'; style.bottom = 10; } return (
{this.renderTitle()} {this.props.menu.context == 'custom' ? this.props.menu.options : this.renderItems()}
{this.renderSubmenu()}
); } } const mapStateToProps = (state, ownProps) => ({ 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, queue_metadata: state.core.queue_metadata, 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) => ({ 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)(withRouter(ContextMenu));