diff --git a/src/js/components/AlbumGrid.js b/src/js/components/AlbumGrid.js index a981989d..e4c27c3b 100755 --- a/src/js/components/AlbumGrid.js +++ b/src/js/components/AlbumGrid.js @@ -1,13 +1,36 @@ import React, { PropTypes } from 'react' -import GridItem from './GridItem' +import { connect } from 'react-redux' +import { createStore, bindActionCreators } from 'redux' +import { hashHistory } from 'react-router' +import FontAwesome from 'react-fontawesome' -export default class AlbumGrid extends React.Component{ +import Thumbnail from './Thumbnail' +import ArtistSentence from './ArtistSentence' + +import * as uiActions from '../services/ui/actions' + +class AlbumGrid extends React.Component{ constructor(props) { super(props); } + handleClick(e,link){ + if( e.target.tagName.toLowerCase() !== 'a' ){ + hashHistory.push(link) + } + } + + handleContextMenu(e,item){ + e.preventDefault() + var data = { + uris: [item.uri], + item: item + } + this.props.uiActions.showContextMenu( e, data, 'album', 'click' ) + } + render(){ if( this.props.albums ){ var className = "grid album-grid" @@ -17,7 +40,18 @@ export default class AlbumGrid extends React.Component{ { this.props.albums.map( (album, index) => { - return + return ( +
this.handleClick(e,global.baseURL+'album/'+album.uri) } + onContextMenu={e => this.handleContextMenu(e,album)}> + +
{ album.name }
+
+ { album.artists ? : - } +
+
+ ) } ) } @@ -28,3 +62,15 @@ export default class AlbumGrid extends React.Component{ } } +const mapStateToProps = (state, ownProps) => { + return {} +} + +const mapDispatchToProps = (dispatch) => { + return { + uiActions: bindActionCreators(uiActions, dispatch) + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(AlbumGrid) + diff --git a/src/js/components/ArtistGrid.js b/src/js/components/ArtistGrid.js index 7dc83808..9088c1f2 100755 --- a/src/js/components/ArtistGrid.js +++ b/src/js/components/ArtistGrid.js @@ -1,13 +1,28 @@ import React, { PropTypes } from 'react' -import GridItem from './GridItem' +import { connect } from 'react-redux' +import { createStore, bindActionCreators } from 'redux' +import { Link } from 'react-router' -export default class ArtistGrid extends React.Component{ +import Thumbnail from './Thumbnail' + +import * as uiActions from '../services/ui/actions' + +class ArtistGrid extends React.Component{ constructor(props) { super(props); } + handleContextMenu(e,item){ + e.preventDefault() + var data = { + uris: [item.uri], + item: item + } + this.props.uiActions.showContextMenu( e, data, 'artist', 'click' ) + } + render(){ if( this.props.artists ){ var className = "grid artist-grid" @@ -17,7 +32,19 @@ export default class ArtistGrid extends React.Component{ { this.props.artists.map( (artist, index) => { - return + return ( + this.handleContextMenu(e,artist)}> + +
{ artist.name }
+
+ {artist.followers ? artist.followers.total.toLocaleString()+' followers' : 0 followers} +
+ + ) } ) } @@ -28,3 +55,15 @@ export default class ArtistGrid extends React.Component{ } } +const mapStateToProps = (state, ownProps) => { + return {} +} + +const mapDispatchToProps = (dispatch) => { + return { + uiActions: bindActionCreators(uiActions, dispatch) + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(ArtistGrid) + diff --git a/src/js/components/ContextMenu.js b/src/js/components/ContextMenu.js index 1aa59037..2b940d56 100755 --- a/src/js/components/ContextMenu.js +++ b/src/js/components/ContextMenu.js @@ -1,5 +1,6 @@ import React, { PropTypes } from 'react' +import { Link } from 'react-router' import { connect } from 'react-redux' import { bindActionCreators } from 'redux' import FontAwesome from 'react-fontawesome' @@ -7,6 +8,7 @@ import FontAwesome from 'react-fontawesome' import TrackList from './TrackList' import * as helpers from '../helpers' import * as uiActions from '../services/ui/actions' +import * as pusherActions from '../services/pusher/actions' import * as mopidyActions from '../services/mopidy/actions' import * as spotifyActions from '../services/spotify/actions' @@ -41,63 +43,64 @@ class ContextMenu extends React.Component{ } playQueueItem(){ - var selectedTracks = this.props.context_menu.data.selected_tracks; - this.props.mopidyActions.changeTrack( selectedTracks[0].tlid ); + var tracks = this.props.context_menu.data.items; + this.props.mopidyActions.changeTrack( tracks[0].tlid ); this.props.uiActions.hideContextMenu(); } removeFromQueue(){ - var selected_tracks = this.props.context_menu.data.selected_tracks; - var selected_tracks_tlids = []; - for( var i = 0; i < selected_tracks.length; i++ ){ - selected_tracks_tlids.push( selected_tracks[i].tlid ); + var tracks = this.props.context_menu.data.items; + var tracks_tlids = []; + for( var i = 0; i < tracks.length; i++ ){ + tracks_tlids.push( tracks[i].tlid ); } - this.props.mopidyActions.removeTracks( selected_tracks_tlids ); + this.props.mopidyActions.removeTracks( tracks_tlids ); this.props.uiActions.hideContextMenu(); } - playItems(){ - var selected_tracks_uris = helpers.asURIs(this.props.context_menu.data.selected_tracks) - this.props.mopidyActions.playURIs(selected_tracks_uris); + playURIs(){ + this.props.mopidyActions.playURIs(this.props.context_menu.data.uris); this.props.uiActions.hideContextMenu(); } - playItemsNext(){ - var selected_tracks_uris = helpers.asURIs(this.props.context_menu.data.selected_tracks) - this.props.mopidyActions.enqueueTracksNext(selected_tracks_uris); + playURIsNext(){ + this.props.mopidyActions.enqueueURIsNext(this.props.context_menu.data.uris); this.props.uiActions.hideContextMenu(); } addToPlaylist(){ - var selected_tracks_uris = helpers.asURIs(this.props.context_menu.data.selected_tracks) - this.props.uiActions.openModal( 'add_to_playlist', { tracks_uris: selected_tracks_uris } ) + this.props.uiActions.openModal( 'add_to_playlist', { tracks_uris: this.props.context_menu.data.uris } ) this.props.uiActions.hideContextMenu(); } addToQueue(){ - var selected_tracks_uris = helpers.asURIs(this.props.context_menu.data.selected_tracks) - this.props.mopidyActions.enqueueTracks(selected_tracks_uris); - this.props.uiActions.hideContextMenu(); + this.props.mopidyActions.enqueueURIs(this.props.context_menu.data.uris) + this.props.uiActions.hideContextMenu() } addTracksToPlaylist( playlist_uri ){ - var selected_tracks_uris = helpers.asURIs(this.props.context_menu.data.selected_tracks) - this.props.uiActions.addTracksToPlaylist( playlist_uri, selected_tracks_uris ) + this.props.uiActions.addTracksToPlaylist( playlist_uri, this.props.context_menu.data.uris ) this.props.uiActions.hideContextMenu(); } removeFromPlaylist(){ - this.props.uiActions.removeTracksFromPlaylist( this.props.playlist.uri, this.props.context_menu.data.selected_tracks_indexes ) + this.props.uiActions.removeTracksFromPlaylist( this.props.playlist.uri, this.props.context_menu.data.indexes ) this.props.uiActions.hideContextMenu(); } - copyURIs(){ - var uris = ''; - for( var i = 0; i < this.props.context_menu.data.selected_tracks.length; i++ ){ - if( i > 0 ) uris += ',' - uris += this.props.context_menu.data.selected_tracks[i].uri - } - console.log('Yet to be implemented', uris) + startRadio(){ + this.props.pusherActions.startRadio(this.props.context_menu.data.uris) + this.props.uiActions.hideContextMenu(); + } + + copyURIs(e){ + var temp = $(""); + $("body").append(temp); + temp.val(this.props.context_menu.data.uris.join(',')).select(); + document.execCommand("copy"); + temp.remove(); + + this.props.uiActions.createNotification( "Copied "+this.props.context_menu.data.uris.length+" URIs" ) this.props.uiActions.hideContextMenu() } @@ -130,7 +133,7 @@ class ContextMenu extends React.Component{ ) } - getItems(trigger){ + getItems(){ switch (this.props.context_menu.context) { case 'queue': @@ -142,12 +145,39 @@ class ContextMenu extends React.Component{ ] break - case 'editable-playlist': + case 'album': var items = [ - { handleClick: 'playItems', label: 'Play', icon: 'play' }, - { handleClick: 'playItemsNext', label: 'Play next', icon: 'play' }, + { handleClick: 'playURIs', label: 'Play', icon: 'play' }, + { handleClick: 'playURIsNext', label: 'Play next', icon: 'play' }, + { handleClick: 'addToQueue', label: 'Add to queue', icon: 'plus' }, + { handleClick: 'startRadio', label: 'Start radio', icon: 'spotify' }, + { handleClick: 'copyURIs', label: 'Copy URI', icon: 'copy' } + ] + break + + case 'artist': + var items = [ + { handleClick: 'startRadio', label: 'Start radio', icon: 'spotify' }, + { handleClick: 'toggleFollow', label: 'Follow/unfollow', icon: 'toggle-on' }, + { handleClick: 'copyURIs', label: 'Copy URI', icon: 'copy' } + ] + break + + case 'playlist': + var items = [ + { handleClick: 'playURIs', label: 'Play', icon: 'play' }, + { handleClick: 'toggleFollow', label: 'Follow/unfollow', icon: 'toggle-on' }, + { handleClick: 'copyURIs', label: 'Copy URI', icon: 'copy' } + ] + break + + case 'editable-playlist-track': + var items = [ + { handleClick: 'playURIs', label: 'Play', icon: 'play' }, + { handleClick: 'playURIsNext', label: 'Play next', icon: 'play' }, { handleClick: 'addToQueue', label: 'Add to queue', icon: 'plus' }, { handleClick: 'addToPlaylist', label: 'Add to playlist', icon: 'plus', playlists: true }, + { handleClick: 'startRadio', label: 'Start radio', icon: 'spotify' }, { handleClick: 'copyURIs', label: 'Copy URIs', icon: 'copy' }, { handleClick: 'removeFromPlaylist', label: 'Remove', icon: 'trash' } ] @@ -155,10 +185,11 @@ class ContextMenu extends React.Component{ default: var items = [ - { handleClick: 'playItems', label: 'Play', icon: 'play' }, - { handleClick: 'playItemsNext', label: 'Play next', icon: 'play' }, + { handleClick: 'playURIs', label: 'Play', icon: 'play' }, + { handleClick: 'playURIsNext', label: 'Play next', icon: 'play' }, { handleClick: 'addToQueue', label: 'Add to queue', icon: 'plus' }, { handleClick: 'addToPlaylist', label: 'Add to playlist', icon: 'plus', playlists: true }, + { handleClick: 'startRadio', label: 'Start radio', icon: 'spotify' }, { handleClick: 'copyURIs', label: 'Copy URIs', icon: 'copy' } ] break @@ -167,17 +198,24 @@ class ContextMenu extends React.Component{ return items } - renderItems(trigger){ - var items = this.getItems(trigger) - - var closeItem = ( - - this.closeAndDeselectTracks(e) }> - - Cancel - - + renderTitle(){ + var item = this.props.context_menu.data.item + var style = null + if (item && item.images){ + style = { + backgroundImage: 'url('+helpers.sizedImages(item.images).medium+')' + } + } + return ( + + {style ?
: null} +
{item.name}
+ ) + } + + renderItems(){ + var items = this.getItems() return (
@@ -186,7 +224,7 @@ class ContextMenu extends React.Component{ if( item.playlists ){ return ( - this[item.handleClick](e) }> + this[item.handleClick](e)}> { item.label } @@ -197,7 +235,7 @@ class ContextMenu extends React.Component{ }else{ return ( - this[item.handleClick](e) }> + this[item.handleClick](e)}> { item.label } @@ -206,7 +244,6 @@ class ContextMenu extends React.Component{ } }) } - { trigger == 'touch' ? closeItem : null }
) } @@ -241,7 +278,8 @@ class ContextMenu extends React.Component{ return (
- { this.renderItems(trigger) } + {this.props.context_menu.data.item ? this.renderTitle() : null} + {this.renderItems(trigger)}
); } @@ -261,6 +299,7 @@ const mapStateToProps = (state, ownProps) => { const mapDispatchToProps = (dispatch) => { return { uiActions: bindActionCreators(uiActions, dispatch), + pusherActions: bindActionCreators(pusherActions, dispatch), spotifyActions: bindActionCreators(spotifyActions, dispatch), mopidyActions: bindActionCreators(mopidyActions, dispatch) } diff --git a/src/js/components/DebugInfo.js b/src/js/components/DebugInfo.js new file mode 100755 index 00000000..4d808866 --- /dev/null +++ b/src/js/components/DebugInfo.js @@ -0,0 +1,50 @@ + +import React, { PropTypes } from 'react' +import { connect } from 'react-redux' +import { Link } from 'react-router' +import { createStore, bindActionCreators } from 'redux' + +import * as uiActions from '../services/ui/actions' + +class DebugInfo extends React.Component{ + + constructor(props) { + super(props); + } + + render(){ + return ( +
+
+ {this.props.ui.albums ? Object.keys(this.props.ui.albums).length : '0'} albums +
+
+ {this.props.ui.artists ? Object.keys(this.props.ui.artists).length : '0'} artists +
+
+ {this.props.ui.playlists ? Object.keys(this.props.ui.playlists).length : '0'} playlists +
+
+ {this.props.ui.users ? Object.keys(this.props.ui.users).length : '0'} users +
+
+ {'ontouchstart' in document.documentElement ? 'can touch' : 'no touch'} +
+
+ ); + } +} + +const mapStateToProps = (state, ownProps) => { + return { + ui: state.ui + } +} + +const mapDispatchToProps = (dispatch) => { + return { + uiActions: bindActionCreators(uiActions, dispatch) + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(DebugInfo) \ No newline at end of file diff --git a/src/js/components/GridSlider.js b/src/js/components/GridSlider.js index 79d85320..0098b95c 100755 --- a/src/js/components/GridSlider.js +++ b/src/js/components/GridSlider.js @@ -1,9 +1,16 @@ import React, { PropTypes } from 'react' +import { connect } from 'react-redux' +import { createStore, bindActionCreators } from 'redux' +import { Link } from 'react-router' import FontAwesome from 'react-fontawesome' -import GridItem from './GridItem' -export default class GridSlider extends React.Component{ +import ArtistSentence from './ArtistSentence' +import Thumbnail from './Thumbnail' + +import * as uiActions from '../services/ui/actions' + +class GridSlider extends React.Component{ constructor(props) { super(props) @@ -15,6 +22,21 @@ export default class GridSlider extends React.Component{ } } + handleClick(e,link){ + if( e.target.tagName.toLowerCase() !== 'a' ){ + hashHistory.push(link) + } + } + + handleContextMenu(e,item){ + e.preventDefault() + var data = { + uris: [item.uri], + item: item + } + this.props.uiActions.showContextMenu( e, data, 'album', 'click' ) + } + next(){ if (this.state.page >= this._pagelimit) return false this.setState({ page: this.state.page + 1 }) @@ -47,8 +69,19 @@ export default class GridSlider extends React.Component{ { this.props.tracks.map( (track, index) => { - var item = Object.assign({}, track.album, { artists: track.artists }) - return + var album = Object.assign({}, track.album, { artists: track.artists }) + return ( +
this.handleClick(e,global.baseURL+'album/'+album.uri) } + onContextMenu={e => this.handleContextMenu(e,album)}> + +
{ album.name }
+
+ { album.artists ? : - } +
+
+ ) } ) } @@ -61,3 +94,15 @@ export default class GridSlider extends React.Component{ } } +const mapStateToProps = (state, ownProps) => { + return {} +} + +const mapDispatchToProps = (dispatch) => { + return { + uiActions: bindActionCreators(uiActions, dispatch) + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(GridSlider) + diff --git a/src/js/components/LazyLoadListener.js b/src/js/components/LazyLoadListener.js index 3ce1f4c7..a9d29778 100755 --- a/src/js/components/LazyLoadListener.js +++ b/src/js/components/LazyLoadListener.js @@ -19,8 +19,8 @@ export default class LazyLoadListener extends React.Component{ } handleScroll(e){ - if( (window.innerHeight + window.scrollY) >= document.body.offsetHeight ){ - if( !this.state.loading ){ + if( (window.innerHeight + window.scrollY) >= (document.body.offsetHeight - 80) ){ + if( !this.state.loading && this.props.enabled ){ this.setState({ loading: true }) this.props.loadMore(); } @@ -30,6 +30,10 @@ export default class LazyLoadListener extends React.Component{ } render(){ - return
+ return ( +
+
+
+ ) } } \ No newline at end of file diff --git a/src/js/components/List.js b/src/js/components/List.js index a24303bc..43f5c8cb 100755 --- a/src/js/components/List.js +++ b/src/js/components/List.js @@ -1,14 +1,17 @@ import React, { PropTypes } from 'react' -import { Link, hashHistory } from 'react-router' +import { connect } from 'react-redux' +import { createStore, bindActionCreators } from 'redux' +import { hashHistory, Link } from 'react-router' import FontAwesome from 'react-fontawesome' import ArtistSentence from './ArtistSentence' import Dater from './Dater' import * as helpers from '../helpers' +import * as uiActions from '../services/ui/actions' -export default class List extends React.Component{ +class List extends React.Component{ constructor(props) { super(props); @@ -22,6 +25,15 @@ export default class List extends React.Component{ } } + handleContextMenu(e,item){ + e.preventDefault() + var data = { + uris: [item.uri], + item: item + } + this.props.uiActions.showContextMenu(e, data, helpers.uriType(item.uri), 'click') + } + renderHeader(){ if (!this.props.columns || this.props.noheader) return null @@ -71,7 +83,11 @@ export default class List extends React.Component{ if( row.type ) className += ' '+row.type return ( -
this.handleClick(e, row.uri)} className={className} key={row_index}> +
this.handleClick(e, row.uri)} + onContextMenu={e => this.handleContextMenu(e,row)} + className={className} + key={row_index}> { this.props.columns.map( (col, col_index) => { return ( @@ -89,4 +105,16 @@ export default class List extends React.Component{
); } -} \ No newline at end of file +} + +const mapStateToProps = (state, ownProps) => { + return {} +} + +const mapDispatchToProps = (dispatch) => { + return { + uiActions: bindActionCreators(uiActions, dispatch) + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(List) diff --git a/src/js/components/PlaylistGrid.js b/src/js/components/PlaylistGrid.js index f8d703c3..6ce3924a 100755 --- a/src/js/components/PlaylistGrid.js +++ b/src/js/components/PlaylistGrid.js @@ -1,13 +1,29 @@ import React, { PropTypes } from 'react' -import GridItem from './GridItem' +import { connect } from 'react-redux' +import { createStore, bindActionCreators } from 'redux' +import { Link } from 'react-router' +import FontAwesome from 'react-fontawesome' -export default class PlaylistGrid extends React.Component{ +import Thumbnail from './Thumbnail' + +import * as uiActions from '../services/ui/actions' + +class PlaylistGrid extends React.Component{ constructor(props) { super(props); } + handleContextMenu(e,item){ + e.preventDefault() + var data = { + uris: [item.uri], + item: item + } + this.props.uiActions.showContextMenu( e, data, 'playlist', 'click' ) + } + render(){ if( !this.props.playlists ) return null @@ -17,8 +33,21 @@ export default class PlaylistGrid extends React.Component{
{ this.props.playlists.map( - (playlist, index) => { - return + (playlist, index) => { + return ( + this.handleContextMenu(e,playlist)}> + +
{ playlist.name }
+
+ { playlist.tracks_total ? playlist.tracks_total+' tracks' : 0 tracks } + { playlist.can_edit ? : null } +
+ + ) } ) } @@ -27,3 +56,15 @@ export default class PlaylistGrid extends React.Component{ } } +const mapStateToProps = (state, ownProps) => { + return {} +} + +const mapDispatchToProps = (dispatch) => { + return { + uiActions: bindActionCreators(uiActions, dispatch) + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(PlaylistGrid) + diff --git a/src/js/components/PlaylistListItem.js b/src/js/components/PlaylistListItem.js deleted file mode 100755 index f9639176..00000000 --- a/src/js/components/PlaylistListItem.js +++ /dev/null @@ -1,41 +0,0 @@ - -import React, { PropTypes } from 'react' -import FontAwesome from 'react-fontawesome' -import { Link } from 'react-router' - -import ListItem from './ListItem' -import ItemSource from './ItemSource' - -export default class PlaylistListItem extends React.Component{ - - constructor(props) { - super(props); - } - - renderOwner(){ - if( !this.props.item.owner ) return null; - return ( - - { this.props.item.owner.id } - - ); - } - - render(){ - if( !this.props.item ) return null - - return ( - - - {this.props.item.name} - - - { this.renderOwner() } - - - - - - ); - } -} \ No newline at end of file diff --git a/src/js/components/Sidebar.js b/src/js/components/Sidebar.js index 0f8af507..2115136f 100755 --- a/src/js/components/Sidebar.js +++ b/src/js/components/Sidebar.js @@ -39,7 +39,7 @@ class Sidebar extends React.Component{
Discover - + Discover diff --git a/src/js/components/TrackList.js b/src/js/components/TrackList.js index b4e40abe..b43da2e5 100755 --- a/src/js/components/TrackList.js +++ b/src/js/components/TrackList.js @@ -5,6 +5,7 @@ import { bindActionCreators } from 'redux' import Track from './Track' +import * as helpers from '../helpers' import * as mopidyActions from '../services/mopidy/actions' import * as uiActions from '../services/ui/actions' @@ -78,8 +79,9 @@ class TrackList extends React.Component{ var selected_tracks = this.selectedTracks() if( selected_tracks.length > 0 ){ var data = { - selected_tracks: selected_tracks, - selected_tracks_indexes: this.tracksIndexes( selected_tracks ) + items: selected_tracks, + uris: helpers.asURIs(selected_tracks), + indexes: this.tracksIndexes(selected_tracks) } this.props.uiActions.showContextMenu( e, data, this.props.context, 'touch' ) }else{ @@ -133,8 +135,9 @@ class TrackList extends React.Component{ handleContextMenu(e, index){ var selected_tracks = this.selectedTracks() var data = { - selected_tracks: selected_tracks, - selected_tracks_indexes: this.tracksIndexes( selected_tracks ) + items: selected_tracks, + uris: helpers.asURIs(selected_tracks), + indexes: this.tracksIndexes(selected_tracks) } this.props.uiActions.showContextMenu( e, data, this.props.context, 'click' ) } diff --git a/src/js/helpers.js b/src/js/helpers.js index e24b8e42..64bd088f 100755 --- a/src/js/helpers.js +++ b/src/js/helpers.js @@ -161,6 +161,7 @@ export let getFromUri = function( element, uri ){ if( element == 'userid' && exploded[1] == 'user' ) return exploded[2]; if( element == 'playlistid' && exploded[3] == 'playlist' ) return exploded[4]; if( element == 'artistid' && exploded[1] == 'artist' ) return exploded[2]; + if( element == 'artistid' && exploded[3] == 'playlist' ) return exploded[2]; if( element == 'albumid' && exploded[1] == 'album' ) return exploded[2]; if( element == 'trackid' && exploded[1] == 'track' ) return exploded[2]; return null; diff --git a/src/js/services/mopidy/actions.js b/src/js/services/mopidy/actions.js index 1df3fb96..0fe7d7f0 100755 --- a/src/js/services/mopidy/actions.js +++ b/src/js/services/mopidy/actions.js @@ -56,15 +56,15 @@ export function playURIs( uris ){ } } -export function enqueueTracksNext( uris ){ +export function enqueueURIsNext( uris ){ if( typeof(uris) !== 'object' ) uris = [uris] return { - type: 'MOPIDY_ENQUEUE_TRACKS_NEXT', + type: 'MOPIDY_ENQUEUE_URIS_NEXT', uris: uris } } -export function enqueueTracks( uris, at_position = false ){ +export function enqueueURIs( uris, at_position = false ){ if( typeof(uris) !== 'object' ) uris = [uris]; var value = { uris: uris }; if( at_position ) value.at_position = at_position; diff --git a/src/js/services/mopidy/middleware.js b/src/js/services/mopidy/middleware.js index c19bea99..db0654c4 100755 --- a/src/js/services/mopidy/middleware.js +++ b/src/js/services/mopidy/middleware.js @@ -3,6 +3,7 @@ import Mopidy from 'mopidy' import * as helpers from '../../helpers' var mopidyActions = require('./actions.js') +var uiActions = require('../ui/actions.js') var lastfmActions = require('../lastfm/actions.js') var pusherActions = require('../pusher/actions.js') @@ -229,7 +230,7 @@ const MopidyMiddleware = (function(){ store.dispatch({ type: 'MOPIDY_URISCHEMES_FILTERED', data: uri_schemes }); break; - case 'MOPIDY_ENQUEUE_TRACKS_NEXT': + case 'MOPIDY_ENQUEUE_URIS_NEXT': var current_track = store.getState().ui.current_track var current_tracklist = store.getState().ui.current_tracklist @@ -268,7 +269,7 @@ const MopidyMiddleware = (function(){ // add the rest of our uris (if any) action.uris.shift(); if( action.uris.length > 0 ){ - store.dispatch( mopidyActions.enqueueTracks( action.uris, 1 ) ) + store.dispatch( mopidyActions.enqueueURIs( action.uris, 1 ) ) } }) break; diff --git a/src/js/services/pusher/middleware.js b/src/js/services/pusher/middleware.js index 588f2372..eb8082fe 100755 --- a/src/js/services/pusher/middleware.js +++ b/src/js/services/pusher/middleware.js @@ -214,7 +214,7 @@ const PusherMiddleware = (function(){ }) .then( response => { - uiActions.createNotification('Starting radio...') + store.dispatch(uiActions.createNotification('Starting radio...')) } ) @@ -252,11 +252,11 @@ const PusherMiddleware = (function(){ }) .then( response => { - uiActions.createNotification('Stopping radio') + store.dispatch(uiActions.createNotification('Stopping radio')) } ) - store.dispatch( uiActions.createNotification('Stopping radio') ) + store.dispatch(uiActions.createNotification('Stopping radio')) var data = { action: 'stop_radio', seed_artists: [], diff --git a/src/js/services/spotify/actions.js b/src/js/services/spotify/actions.js index d5431a51..c71d15c8 100755 --- a/src/js/services/spotify/actions.js +++ b/src/js/services/spotify/actions.js @@ -532,10 +532,9 @@ export function resolveRadioSeeds( radio ){ // add to our list of async requests requests.push( - sendRequest( dispatch, getState, 'tracks/'+ track_ids ) + sendRequest( dispatch, getState, 'tracks?ids='+ track_ids ) .then( response => { - if (!(response instanceof Array)) response = [response] - Object.assign(resolved_seeds.seed_tracks, response); + Object.assign(resolved_seeds.seed_tracks, response.tracks); }) ) } diff --git a/src/js/services/spotify/middleware.js b/src/js/services/spotify/middleware.js index d9617aba..6aca393c 100755 --- a/src/js/services/spotify/middleware.js +++ b/src/js/services/spotify/middleware.js @@ -56,6 +56,11 @@ const SpotifyMiddleware = (function(){ store.dispatch( uiActions.createNotification( "Must be logged in to Spotify to do that", 'bad' ) ) return } + + if( !store.getState().spotify.me || store.getState().spotify.me.id != helpers.getFromUri('artistid',action.key) ){ + store.dispatch( uiActions.createNotification( "You can't edit a playlist you don't own", 'bad' ) ) + return + } store.dispatch( spotifyActions.reorderPlaylistTracks( action.key, action.range_start, action.range_length, action.insert_before, action.snapshot_id )) break @@ -89,7 +94,7 @@ const SpotifyMiddleware = (function(){ // only resolve if radio is enabled if( action.data.radio.enabled ){ - store.dispatch( spotifyActions.resolveRadioSeeds( action.data.radio ) ) + store.dispatch(spotifyActions.resolveRadioSeeds(action.data.radio)) } break diff --git a/src/js/views/Album.js b/src/js/views/Album.js index 0b14193a..21fea844 100755 --- a/src/js/views/Album.js +++ b/src/js/views/Album.js @@ -15,6 +15,7 @@ import LazyLoadListener from '../components/LazyLoadListener' import SidebarToggleButton from '../components/SidebarToggleButton' import * as helpers from '../helpers' +import * as uiActions from '../services/ui/actions' import * as mopidyActions from '../services/mopidy/actions' import * as spotifyActions from '../services/spotify/actions' @@ -28,6 +29,12 @@ class Album extends React.Component{ this.loadAlbum() } + handleContextMenu(e){ + e.preventDefault() + var data = { uris: [this.props.params.uri] } + this.props.uiActions.showContextMenu( e, data, 'album', 'click' ) + } + componentWillReceiveProps( nextProps ){ // if our URI has changed, fetch new album @@ -66,7 +73,6 @@ class Album extends React.Component{ } loadMore(){ - if( !this.props.album.tracks_more ) return this.props.spotifyActions.getURL( this.props.album.tracks_more, 'SPOTIFY_ALBUM_LOADED_MORE' ); } @@ -92,7 +98,7 @@ class Album extends React.Component{
- + this.handleContextMenu(e) } />
@@ -118,7 +124,7 @@ class Album extends React.Component{
{ this.props.album.tracks ? : null } - this.loadMore() }/> + this.loadMore() }/>
@@ -146,6 +152,7 @@ const mapStateToProps = (state, ownProps) => { const mapDispatchToProps = (dispatch) => { return { + uiActions: bindActionCreators(uiActions, dispatch), mopidyActions: bindActionCreators(mopidyActions, dispatch), spotifyActions: bindActionCreators(spotifyActions, dispatch) } diff --git a/src/js/views/App.js b/src/js/views/App.js index 45e80322..76605906 100755 --- a/src/js/views/App.js +++ b/src/js/views/App.js @@ -12,6 +12,7 @@ import ContextMenu from '../components/ContextMenu' import Dragger from '../components/Dragger' import Modal from '../components/Modal/Modal' import Notifications from '../components/Notifications' +import DebugInfo from '../components/DebugInfo' import * as uiActions from '../services/ui/actions' import * as pusherActions from '../services/pusher/actions' @@ -132,6 +133,7 @@ class App extends React.Component{ + {this.props.debug_info ? : null}
); } @@ -151,7 +153,8 @@ const mapStateToProps = (state, ownProps) => { sidebar_open: state.ui.sidebar_open, dragger: state.ui.dragger, modal: state.ui.modal, - context_menu: state.ui.context_menu + context_menu: state.ui.context_menu, + debug_info: state.ui.debug_info } } diff --git a/src/js/views/Artist.js b/src/js/views/Artist.js index bba09849..6c2ab45f 100755 --- a/src/js/views/Artist.js +++ b/src/js/views/Artist.js @@ -81,7 +81,6 @@ class Artist extends React.Component{ } loadMore(){ - if( !this.props.artist.albums_more ) return this.props.spotifyActions.getURL( this.props.artist.albums_more, 'SPOTIFY_ARTIST_ALBUMS_LOADED', this.props.params.uri ); } @@ -164,7 +163,7 @@ class Artist extends React.Component{

Albums

- this.loadMore() }/> + this.loadMore() }/>
) diff --git a/src/js/views/Debug.js b/src/js/views/Debug.js index 02fe0c46..df52f463 100755 --- a/src/js/views/Debug.js +++ b/src/js/views/Debug.js @@ -87,6 +87,14 @@ class Debug extends React.Component{ onChange={ e => this.props.uiActions.set({ log_actions: !this.props.log_actions })} /> Log actions in developer console +
@@ -161,6 +169,7 @@ const mapStateToProps = (state, ownProps) => { connectionid: state.pusher.connectionid, emulate_touch: state.ui.emulate_touch, log_actions: state.ui.log_actions, + debug_info: state.ui.debug_info, debug_response: state.ui.debug_response } } diff --git a/src/js/views/Playlist.js b/src/js/views/Playlist.js index 00b1ccce..91228e00 100755 --- a/src/js/views/Playlist.js +++ b/src/js/views/Playlist.js @@ -54,7 +54,6 @@ class Playlist extends React.Component{ } loadMore(){ - if( !this.props.playlist.tracks_more ) return this.props.spotifyActions.getURL( this.props.playlist.tracks_more, 'PLAYLIST_LOADED_MORE_TRACKS', this.props.playlist.uri ); } @@ -154,8 +153,8 @@ class Playlist extends React.Component{
- { this.props.playlist.tracks ? this.removeTracks(tracks_indexes) } reorderTracks={ (indexes, index) => this.reorderTracks(indexes, index) } /> : null } - this.loadMore() }/> + { this.props.playlist.tracks ? this.removeTracks(tracks_indexes) } reorderTracks={ (indexes, index) => this.reorderTracks(indexes, index) } /> : null } + this.loadMore() }/>
diff --git a/src/js/views/Queue.js b/src/js/views/Queue.js index db32cb7f..7c7197d5 100755 --- a/src/js/views/Queue.js +++ b/src/js/views/Queue.js @@ -67,7 +67,7 @@ class Queue extends React.Component{ } } if (this.props.radio.seed_genres.length > 0) seed_sentence =this.props.radio.seed_artists.length+' genres' - if (this.props.radio.seed_tracks.length > 0) seed_sentence =this.props.radio.seed_artists.length+' tracks' + if (this.props.radio.seed_tracks.length > 0) seed_sentence =this.props.radio.seed_tracks.length+' tracks' return (
diff --git a/src/js/views/Search.js b/src/js/views/Search.js index 40051712..80c957df 100755 --- a/src/js/views/Search.js +++ b/src/js/views/Search.js @@ -47,9 +47,7 @@ class Search extends React.Component{ } loadMore(type){ - if( this.props[type+'_more'] ){ - this.props.spotifyActions.getURL( this.props[type+'_more'], 'SPOTIFY_SEARCH_RESULTS_LOADED_MORE_'+type.toUpperCase()); - } + this.props.spotifyActions.getURL( this.props[type+'_more'], 'SPOTIFY_SEARCH_RESULTS_LOADED_MORE_'+type.toUpperCase()); } renderResults(){ @@ -91,7 +89,7 @@ class Search extends React.Component{
- this.loadMore('artists') }/> + this.loadMore('artists') }/>
) @@ -102,7 +100,7 @@ class Search extends React.Component{
- this.loadMore('albums') }/> + this.loadMore('albums') }/>
) @@ -113,7 +111,7 @@ class Search extends React.Component{
- this.loadMore('playlists') }/> + this.loadMore('playlists') }/>
) @@ -124,7 +122,7 @@ class Search extends React.Component{
- this.loadMore('tracks') }/> + this.loadMore('tracks') }/>
) diff --git a/src/js/views/User.js b/src/js/views/User.js index 5493546a..190d2500 100755 --- a/src/js/views/User.js +++ b/src/js/views/User.js @@ -33,7 +33,6 @@ class User extends React.Component{ } loadMore(){ - if( !this.props.user.playlists_more ) return this.props.spotifyActions.getURL( this.props.user.playlists_more, 'SPOTIFY_USER_PLAYLISTS_LOADED', this.props.params.uri ) } @@ -83,7 +82,7 @@ class User extends React.Component{
- this.loadMore() }/> + this.loadMore() }/>
diff --git a/src/js/views/discover/DiscoverCategory.js b/src/js/views/discover/DiscoverCategory.js index 382928e6..a7ed4ec4 100755 --- a/src/js/views/discover/DiscoverCategory.js +++ b/src/js/views/discover/DiscoverCategory.js @@ -30,9 +30,7 @@ class DiscoverCategory extends React.Component{ } loadMore(){ - if (this.props.category.playlists_more){ - this.props.spotifyActions.getURL( this.props.category.playlists_more, 'SPOTIFY_CATEGORY_PLAYLISTS_LOADED', 'category:'+this.props.params.id ); - } + this.props.spotifyActions.getURL( this.props.category.playlists_more, 'SPOTIFY_CATEGORY_PLAYLISTS_LOADED', 'category:'+this.props.params.id ); } render(){ @@ -54,7 +52,7 @@ class DiscoverCategory extends React.Component{
- this.loadMore() }/> + this.loadMore() }/> ); } diff --git a/src/js/views/discover/DiscoverNewReleases.js b/src/js/views/discover/DiscoverNewReleases.js index a6a50f7f..fe63e323 100755 --- a/src/js/views/discover/DiscoverNewReleases.js +++ b/src/js/views/discover/DiscoverNewReleases.js @@ -21,7 +21,6 @@ class DiscoverNewReleases extends React.Component{ } loadMore(){ - if (!this.props.new_releases_more) return this.props.spotifyActions.getURL(this.props.new_releases_more, 'SPOTIFY_NEW_RELEASES_LOADED'); } @@ -43,7 +42,7 @@ class DiscoverNewReleases extends React.Component{
- this.loadMore() }/> + this.loadMore() }/> ); } diff --git a/src/js/views/library/LibraryAlbums.js b/src/js/views/library/LibraryAlbums.js index 8ddd73bc..ccbcb21b 100755 --- a/src/js/views/library/LibraryAlbums.js +++ b/src/js/views/library/LibraryAlbums.js @@ -28,8 +28,13 @@ class LibraryAlbums extends React.Component{ if (!this.props.library_albums) this.props.spotifyActions.getLibraryAlbums(); } + handleContextMenu(e,uri){ + e.preventDefault() + var data = { uris: [uri] } + this.props.uiActions.showContextMenu( e, data, 'album', 'click' ) + } + loadMore(){ - if( !this.props.library_albums_more ) return this.props.spotifyActions.getURL( this.props.library_albums_more, 'SPOTIFY_LIBRARY_ALBUMS_LOADED' ); } @@ -77,13 +82,19 @@ class LibraryAlbums extends React.Component{ ] return (
- + this.handleContextMenu(e,uri)} + rows={albums} + columns={columns} + link_prefix={global.baseURL+"album/"} />
) }else if( this.props.view == 'thumbnails' ){ return (
- + this.handleContextMenu(e,uri)} + albums={albums} />
) }else{ @@ -173,7 +184,7 @@ class LibraryAlbums extends React.Component{
{ this.renderView(albums) } - this.loadMore() }/> + this.loadMore() }/>
); } diff --git a/src/js/views/library/LibraryArtists.js b/src/js/views/library/LibraryArtists.js index 748c23dc..3d994b1f 100755 --- a/src/js/views/library/LibraryArtists.js +++ b/src/js/views/library/LibraryArtists.js @@ -21,12 +21,17 @@ class LibraryArtists extends React.Component{ super(props); } + handleContextMenu(e,uri){ + e.preventDefault() + var data = { uris: [uri] } + this.props.uiActions.showContextMenu( e, data, 'artist', 'click' ) + } + componentDidMount(){ if (!this.props.library_artists) this.props.spotifyActions.getLibraryArtists(); } loadMore(){ - if( !this.props.library_artists_more ) return this.props.spotifyActions.getURL( this.props.library_artists_more, 'SPOTIFY_LIBRARY_ARTISTS_LOADED' ); } @@ -62,13 +67,19 @@ class LibraryArtists extends React.Component{ ] return (
- + this.handleContextMenu(e,uri)} + rows={artists} + columns={columns} + link_prefix={global.baseURL+"artist/"} show_source_icon={true} />
) }else{ return (
- + this.handleContextMenu(e,uri)} + artists={artists} />
) } @@ -127,7 +138,7 @@ class LibraryArtists extends React.Component{
{ this.renderView(artists) } - this.loadMore() }/> + this.loadMore() }/>
); } diff --git a/src/js/views/library/LibraryLocalAlbums.js b/src/js/views/library/LibraryLocalAlbums.js index f0b95b30..dac9dc63 100755 --- a/src/js/views/library/LibraryLocalAlbums.js +++ b/src/js/views/library/LibraryLocalAlbums.js @@ -36,7 +36,7 @@ class LibraryLocalAlbums extends React.Component{ } } - loadMore(){ + moreURIsToLoad(){ var uris = [] if (this.props.albums && this.props.local_albums){ for (var i = 0; i < this.props.local_albums.length; i++){ @@ -50,7 +50,12 @@ class LibraryLocalAlbums extends React.Component{ } } - if (uris && uris.length > 0) this.props.mopidyActions.getAlbums(uris) + return uris + } + + loadMore(){ + var uris = this.moreURIsToLoad() + this.props.mopidyActions.getAlbums(uris) } setSort(value){ @@ -147,7 +152,7 @@ class LibraryLocalAlbums extends React.Component{
{this.renderView(albums)} - this.loadMore() }/> + 0)} loadMore={ () => this.loadMore() }/>
) } diff --git a/src/js/views/library/LibraryTracks.js b/src/js/views/library/LibraryTracks.js index e1f0c5aa..544e4672 100755 --- a/src/js/views/library/LibraryTracks.js +++ b/src/js/views/library/LibraryTracks.js @@ -23,7 +23,6 @@ class LibraryTracks extends React.Component{ } loadMore(){ - if( !this.props.tracks_more ) return this.props.spotifyActions.getURL( this.props.tracks_more, 'SPOTIFY_LIBRARY_TRACKS_LOADED_MORE' ); } @@ -33,7 +32,7 @@ class LibraryTracks extends React.Component{
{ this.props.tracks ? : null } - this.loadMore() }/> + this.loadMore() }/>
); diff --git a/src/scss/app.scss b/src/scss/app.scss index 9b71437f..8b169085 100755 --- a/src/scss/app.scss +++ b/src/scss/app.scss @@ -22,6 +22,8 @@ @import 'components/header'; @import 'components/notifications'; @import 'components/dropdown-field'; +@import 'components/loader'; +@import 'components/debug'; @import 'views/artist'; @import 'views/user'; diff --git a/src/scss/components/_context-menu.scss b/src/scss/components/_context-menu.scss index 7c4c6207..c3bed4a3 100755 --- a/src/scss/components/_context-menu.scss +++ b/src/scss/components/_context-menu.scss @@ -21,6 +21,45 @@ bottom: 0 !important; } + .title { + cursor: pointer; + display: block; + padding: 8px 12px; + width: 140px; + font-weight: bold; + box-sizing: border-box; + position: relative; + overflow: hidden; + text-decoration: none; + background: $darkest_grey; + + .background { + @include blur(5px); + background-size: cover; + background-position: 50% 20%; + opacity: 0.5; + position: absolute; + top: -10px; + left: -10px; + bottom: -10px; + right: -10px; + z-index: 1; + } + + .text { + @include one_line_text(); + position: relative; + z-index: 2; + color: #FFFFFF; + } + + &:hover{ + .background { + opacity: 0.7; + } + } + } + .menu-item-wrapper { display: block; position: relative; @@ -65,7 +104,7 @@ opacity: 0.5; } - &:not(:first-child) .menu-item { + .menu-item { border-top: 1px solid lighten($dark_grey, 8%); } diff --git a/src/scss/components/_debug.scss b/src/scss/components/_debug.scss new file mode 100755 index 00000000..8e682756 --- /dev/null +++ b/src/scss/components/_debug.scss @@ -0,0 +1,11 @@ + +.debug-info { + font-size: 10px; + position: fixed; + top: 0; + right: 0; + padding: 10px; + background: rgba(0,0,0,0.5); + color: #FFFFFF; + pointer-events: none; +} \ No newline at end of file diff --git a/src/scss/components/_grid.scss b/src/scss/components/_grid.scss index fe916bf4..6b65b4e3 100755 --- a/src/scss/components/_grid.scss +++ b/src/scss/components/_grid.scss @@ -16,6 +16,10 @@ .grid { + .grid-item-wrapper { + display: inline-block; + } + .grid-item { display: inline-block; vertical-align: top; diff --git a/src/scss/components/_loader.scss b/src/scss/components/_loader.scss new file mode 100755 index 00000000..b50200e9 --- /dev/null +++ b/src/scss/components/_loader.scss @@ -0,0 +1,24 @@ + +.lazy-loader { + padding: 20px 0 40px; + + .loader { + @include animate(); + opacity: 0; + margin: 0 auto; + width: 30px; + height: 30px; + border: { + radius: 100%; + style: solid; + color: $mid_grey; + width: 2px; + } + border-top-color: transparent; + } + + &.loading .loader { + @include spin(); + opacity: 1; + } +} \ No newline at end of file diff --git a/src/scss/global/_variables.scss b/src/scss/global/_variables.scss index d9366a58..662e6e92 100755 --- a/src/scss/global/_variables.scss +++ b/src/scss/global/_variables.scss @@ -49,6 +49,18 @@ $bp_shallow: 650px; animation-timing-function: linear; } +@keyframes spin { + from { transform: rotate(0); } + to { transform: rotate(360deg); } +} + +@mixin spin( $duration: 0.5s ){ + animation-name: spin; + animation-duration: $duration; + animation-timing-function: linear; + animation-iteration-count: infinite; +} + @mixin blur( $size: 10px ) { -webkit-filter: blur( $size ); filter: blur( $size );