import React, { createRef } from 'react'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; import { withRouter } from 'react-router'; import { compact } from 'lodash'; import { uriSource, uriType, getFromUri, buildLink, isLoading, throttle, } from '../util/helpers'; import { arrayOf, sortItems, indexToArray, } 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'; import { I18n, i18n } from '../locale'; import { collate } from '../util/format'; class ContextMenu extends React.Component { constructor(props) { super(props); this.state = { submenu: null, }; this.handleScroll = throttle(this.handleScroll.bind(this), 50); 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); } componentDidUpdate = (prevProps) => { const { menu: prevMenu, items: prevItems, } = prevProps; const { menu, items, lastfm_authorized, spotify_available, 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_available && 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 && prevItems[menu.items[0].uri] !== undefined && items[menu.items[0].uri].userloved === undefined) { lastfmActions.getTrack(menu.items[0].uri); } } } } 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 = item.in_library; context.is_loved = this.isLoved(item); context.is_pinned = this.isPinned(item); } } return context; } handleScroll = () => { const { menu, uiActions: { hideContextMenu } } = this.props; if (menu) hideContextMenu(); } handleMouseDown = (e) => { const { menu, uiActions: { hideContextMenu } } = this.props; if (menu && $(e.target).closest('.context-menu').length <= 0 && $(e.target).closest('.context-menu-trigger').length <= 0) { hideContextMenu(); } } handleTouchStart = (e) => { const { menu, uiActions: { hideContextMenu } } = this.props; if (menu && $(e.target).closest('.context-menu').length <= 0 && $(e.target).closest('.context-menu-trigger').length <= 0) { hideContextMenu(); } } 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.items_uris.indexOf(uri) > -1); case 'album': return (spotify_library_albums.items_uris.indexOf(uri) > -1); case 'playlist': return (spotify_library_playlists.items_uris.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 { items, } = this.props; if (!uri) return false; if (items[uri] === undefined) return false; const track = items[uri]; return (track.userloved !== undefined && track.userloved === '1'); } isPinned = ({ uri }) => { const { pinned, } = this.props; return pinned.findIndex((pinnedItem) => pinnedItem.uri === uri) > -1; } canBeInLibrary = () => { const { spotify_available, menu: { items } } = this.props; if (!spotify_available) 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'); } } togglePinned = (isItemPinned) => { const { uiActions: { hideContextMenu, }, pusherActions: { addPinned, removePinned, }, menu: { items, } = {}, } = this.props; const item = items[0]; hideContextMenu(); if (isItemPinned) { removePinned(item.uri); } else { addPinned(item); } } 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 = () => { 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, }, coreActions: { 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 = () => { const { uiActions: { hideContextMenu, }, pusherActions: { startRadio, }, menu: { uris, } = {}, } = this.props; hideContextMenu(); startRadio(uris); } goToRecommendations = () => { 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 || items[0].artists.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[0].uri)); } 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(); } refresh = () => { const { uiActions: { hideContextMenu, }, coreActions: { loadItem, }, menu: { uris, } = {}, } = this.props; const uri = uris[0]; loadItem(uri, { forceRefetch: true, full: true }); hideContextMenu(); } renderTitle = () => { const { uiActions: { hideContextMenu, setSelectedTracks, }, queue_metadata, menu: { title, }, } = this.props; const context = this.getContext(); if (context.items_count > 1) { return (