This commit is contained in:
James Barnsley
2017-03-13 08:13:55 +13:00
9 changed files with 387 additions and 185 deletions

View File

@ -34,11 +34,25 @@ class ContextMenu extends React.Component{
}
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_expanded: false })
$('body').addClass('context-menu-open')
var context = this.getContext()
// if we're able to be in the library, run a check
if (this.props.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
}
}
// we DID have one prior, and now we don't
} else if (this.props.menu && !nextProps.menu){
$('body').removeClass('context-menu-open')
@ -58,6 +72,81 @@ class ContextMenu extends React.Component{
}
}
getContext(){
var context = {
name: null,
nice_name: 'Unknown'
}
if (this.props.menu && this.props.menu.context){
context.name = this.props.menu.context
context.nice_name = this.props.menu.context
// handle ugly labels
switch (this.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'
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 (this.props.menu.items && this.props.menu.items.length > 0){
var item = this.props.menu.items[0]
context.item = item
context.source = helpers.uriSource(item.uri)
context.type = helpers.uriType(item.uri)
context.in_library = this.inLibrary(item)
}
}
return context
}
inLibrary(item = null){
if (!item){
return false
}
switch (helpers.uriType(item.uri)){
case 'artist':
return (this.props.library_artists && this.props.library_artists.indexOf(item.uri) > -1)
break
case 'album':
return (this.props.library_albums && this.props.library_albums.indexOf(item.uri) > -1)
break
case 'playlist':
return (this.props.library_playlists && this.props.library_playlists.indexOf(item.uri) > -1)
break
}
return false
}
canBeInLibrary(){
if (!this.props.spotify_authorized){
return false
}
return (helpers.uriSource(this.props.menu.items[0].uri) == 'spotify')
}
toggleInLibrary(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(){
this.props.uiActions.hideContextMenu()
var tracks = this.props.menu.items;
@ -84,10 +173,6 @@ class ContextMenu extends React.Component{
this.props.mopidyActions.enqueueURIsNext(this.props.menu.uris, this.props.menu.tracklist_uri)
}
addToPlaylist(){
this.setState({ submenu_expanded: !this.state.submenu_expanded })
}
addToQueue(){
this.props.uiActions.hideContextMenu()
this.props.mopidyActions.enqueueURIs(this.props.menu.uris, this.props.menu.tracklist_uri)
@ -139,6 +224,7 @@ class ContextMenu extends React.Component{
closeAndDeselectTracks(){
this.props.uiActions.hideContextMenu();
// TODO
}
renderPlaylistSubmenu(){
@ -175,100 +261,30 @@ class ContextMenu extends React.Component{
)
}
getItems(){
switch (this.props.menu.context) {
case 'album':
var items = [
{ handleClick: 'playURIs', label: 'Play' },
{ handleClick: 'playURIsNext', label: 'Play next' },
{ handleClick: 'addToQueue', label: 'Add to queue' },
{ handleClick: 'goToArtist', label: 'Go to artist' },
// { handleClick: 'toggleFollow', label: 'Follow/unfollow' }, TODO
{ handleClick: 'copyURIs', label: 'Copy URI' }
]
break
case 'artist':
var items = [
//{ handleClick: 'toggleFollow', label: 'Follow/unfollow' }, TODO
{ handleClick: 'startRadio', label: 'Start radio' },
{ handleClick: 'copyURIs', label: 'Copy URI' }
]
break
case 'playlist':
var items = [
{ handleClick: 'playURIs', label: 'Play' },
{ handleClick: 'goToUser', label: 'Go to user' },
// { handleClick: 'toggleFollow', label: 'Follow/unfollow' }, TODO
{ handleClick: 'copyURIs', label: 'Copy URI' }
]
break
case 'queue':
var items = [
{ handleClick: 'playQueueItem', label: 'Play' },
{ handleClick: 'addToPlaylist', label: 'Add to playlist' },
{ handleClick: 'copyURIs', label: 'Copy URIs' },
{ handleClick: 'removeFromQueue', label: 'Remove' }
]
break
case 'editable-playlist-track':
var items = [
{ handleClick: 'playURIs', label: 'Play' },
{ handleClick: 'playURIsNext', label: 'Play next' },
{ handleClick: 'addToQueue', label: 'Add to queue' },
{ handleClick: 'addToPlaylist', label: 'Add to playlist' },
{ handleClick: 'startRadio', label: 'Start radio' },
{ handleClick: 'copyURIs', label: 'Copy URIs' },
{ handleClick: 'removeFromPlaylist', label: 'Remove' }
]
break
default:
var items = [
{ handleClick: 'playURIs', label: 'Play' },
{ handleClick: 'playURIsNext', label: 'Play next' },
{ handleClick: 'addToQueue', label: 'Add to queue' },
{ handleClick: 'addToPlaylist', label: 'Add to playlist' },
{ handleClick: 'startRadio', label: 'Start radio' },
{ handleClick: 'copyURIs', label: 'Copy URIs' }
]
break
}
return items
}
renderTitle(){
if (!this.props.menu.items || this.props.menu.items.length <= 0){
return null
}
var context = this.getContext()
switch (this.props.menu.context){
switch (context.type){
case 'artist':
case 'album':
case 'playlist':
var item = this.props.menu.items[0]
var style = null
if (item && item.images){
if (context.item && context.item.images){
style = {
backgroundImage: 'url('+helpers.sizedImages(item.images).medium+')'
backgroundImage: 'url('+helpers.sizedImages(context.item.images).medium+')'
}
}
return (
<Link className="title" to={global.baseURL+helpers.uriType(item.uri)+'/'+item.uri}>
<Link className="title" to={global.baseURL+context.type+'/'+context.item.uri}>
{style ? <div className="background" style={style}></div> : null}
<div className="type">
{helpers.uriSource(item.uri)}
{context.source}
&nbsp;
{this.props.menu.context}
{context.nice_name}
</div>
<div className="text">{item.name}</div>
<div className="text">{context.item.name}</div>
</Link>
)
break
@ -277,9 +293,9 @@ class ContextMenu extends React.Component{
return (
<span className="title">
<div className="type">
{helpers.uriSource(this.props.menu.items[0].uri)}
{context.source}
&nbsp;
{this.props.menu.context}s
{context.nice_name}s
</div>
<div className="text">
{this.props.menu.items.length} items
@ -292,35 +308,182 @@ class ContextMenu extends React.Component{
}
renderItems(){
var items = this.getItems()
var context = this.getContext()
return (
<div>
{
items.map((item, index) => {
if (item.handleClick == 'addToPlaylist'){
return (
<span key={item.handleClick} className="menu-item-wrapper has-submenu">
<a className="menu-item" onClick={e => this[item.handleClick](e)}>
<span className="label">{ item.label }</span>
<FontAwesome className="submenu-icon" name='caret-right' />
</a>
{this.renderPlaylistSubmenu()}
</span>
)
}else{
return (
<span className="menu-item-wrapper" key={item.handleClick}>
<a className="menu-item" onClick={e => this[item.handleClick](e)}>
<span className="label">{ item.label }</span>
</a>
</span>
)
}
})
}
</div>
var play_uris = (
<span className="menu-item-wrapper">
<a className="menu-item" onClick={e => this.playURIs(e)}>
<span className="label">Play</span>
</a>
</span>
)
var play_queue_item = (
<span className="menu-item-wrapper">
<a className="menu-item" onClick={e => this.playQueueItem(e)}>
<span className="label">Play</span>
</a>
</span>
)
var play_uris_next = (
<span className="menu-item-wrapper">
<a className="menu-item" onClick={e => this.playURIsNext(e)}>
<span className="label">Play next</span>
</a>
</span>
)
var add_to_queue = (
<span className="menu-item-wrapper">
<a className="menu-item" onClick={e => this.addToQueue(e)}>
<span className="label">Add to queue</span>
</a>
</span>
)
var add_to_playlist = (
<span className="menu-item-wrapper has-submenu">
<a className="menu-item" onClick={e => this.setState({ submenu_expanded: !this.state.submenu_expanded })}>
<span className="label">Add to playlist</span>
<FontAwesome className="submenu-icon" name='caret-right' />
</a>
{this.renderPlaylistSubmenu()}
</span>
)
var toggle_in_library = (
<span className="menu-item-wrapper">
<a className="menu-item" onClick={e => this.toggleInLibrary(context.in_library)}>
<span className="label">
{context.in_library ? 'Remove from library' : 'Add to library'}
</span>
</a>
</span>
)
var go_to_artist = (
<span className="menu-item-wrapper">
<a className="menu-item" onClick={e => this.goToArtist(e)}>
<span className="label">Go to artist</span>
</a>
</span>
)
var go_to_user = (
<span className="menu-item-wrapper">
<a className="menu-item" onClick={e => this.goToUser(e)}>
<span className="label">Go to user</span>
</a>
</span>
)
var start_radio = (
<span className="menu-item-wrapper">
<a className="menu-item" onClick={e => this.startRadio(e)}>
<span className="label">Start {this.context.nice_name} radio</span>
</a>
</span>
)
var remove_from_queue = (
<span className="menu-item-wrapper">
<a className="menu-item" onClick={e => this.removeFromQueue(e)}>
<span className="label">Remove</span>
</a>
</span>
)
var remove_from_playlist = (
<span className="menu-item-wrapper">
<a className="menu-item" onClick={e => this.removeFromPlaylist(e)}>
<span className="label">Remove</span>
</a>
</span>
)
var copy_uris = (
<span className="menu-item-wrapper">
<a className="menu-item" onClick={e => this.copyURIs(e)}>
<span className="label">Copy URIs</span>
</a>
</span>
)
switch (context.name){
case 'album':
return (
<div>
{play_uris}
{play_uris_next}
{add_to_queue}
{this.canBeInLibrary() ? toggle_in_library : null}
{go_to_artist}
{copy_uris}
</div>
)
break
case 'artist':
return (
<div>
{start_radio}
{this.canBeInLibrary() ? toggle_in_library : null}
{copy_uris}
</div>
)
break
case 'playlist':
return (
<div>
{play_uris}
{this.canBeInLibrary() ? toggle_in_library : null}
{context.source == 'spotify' ? go_to_user : null}
{copy_uris}
</div>
)
break
case 'queue':
return (
<div>
{play_queue_item}
{add_to_playlist}
{copy_uris}
{remove_from_queue}
</div>
)
break
case 'editable-playlist-track':
return (
<div>
{play_uris}
{play_uris_next}
{add_to_queue}
{add_to_playlist}
{start_radio}
{copy_uris}
{remove_from_playlist}
</div>
)
break
default:
return (
<div>
{play_uris}
{play_uris_next}
{add_to_queue}
{add_to_playlist}
{start_radio}
{copy_uris}
</div>
)
break
}
}
render(){
@ -330,11 +493,7 @@ class ContextMenu extends React.Component{
left: this.props.menu.position_x,
top: this.props.menu.position_y,
}
var items = this.getItems()
var height = 0
if (items) height = items.length * 34 // this is an approximation of how tall each menu item is
var height = 0 // TODO: use jquery to detect height
var className = "context-menu "+this.props.menu.context
if (this.state.submenu_expanded) className += ' submenu-expanded'
if (this.props.menu.position_x > (window.innerWidth - 154)) className += ' right-align'
@ -361,7 +520,11 @@ const mapStateToProps = (state, ownProps) => {
menu: state.ui.context_menu,
current_track: state.ui.current_track,
current_tracklist: state.ui.current_tracklist,
playlists: state.ui.playlists
library_artists: state.ui.library_artists,
library_albums: state.ui.library_albums,
library_playlists: state.ui.library_playlists,
playlists: state.ui.playlists,
spotify_authorized: state.spotify.authorized
}
}

View File

@ -39,7 +39,8 @@ class List extends React.Component{
<div className="list-item header cf">
{
this.props.columns.map( (col, col_index) => {
return <div className={'col w'+col.width} key={col_index}>{ col.label ? col.label : col.name }</div>
var className = 'col '+col.name.replace('.','_')
return <div className={className} key={col_index}>{ col.label ? col.label : col.name }</div>
})
}
</div>
@ -88,8 +89,9 @@ class List extends React.Component{
key={row_index}>
{
this.props.columns.map( (col, col_index) => {
var className = 'col '+col.name.replace('.','_')
return (
<div className={'col w'+col.width} key={col_index}>
<div className={className} key={col_index}>
{ this.renderValue(row, col.name) }
</div>
)

View File

@ -538,6 +538,7 @@ export default function reducer(ui = {}, action){
return Object.assign({}, ui, {
library_playlists: library_playlists,
library_playlists_started: true
library_playlists_loaded: true
});

View File

@ -218,7 +218,7 @@ class Artist extends React.Component{
<h1>{this.props.artist ? this.props.artist.name : null}</h1>
<div className="actions">
{ can_play_radio ? <button className="primary" onClick={e => this.props.pusherActions.startRadio([this.props.artist.uri])}>Start radio</button> : null}
{ can_follow ? <FollowButton className="white" uri={this.props.params.uri} removeText="Unfollow" addText="Follow" is_following={this.inLibrary()} /> : null}
{ can_follow ? <FollowButton className="white" uri={this.props.params.uri} removeText="Remove from library" addText="Add to library" is_following={this.inLibrary()} /> : null}
<ContextMenuTrigger className="white" onTrigger={e => this.handleContextMenu(e)} />
</div>
{ this.renderSubViewMenu() }

View File

@ -35,7 +35,7 @@ class LibraryAlbums extends React.Component{
e: e,
context: 'album',
uris: [item.uri],
item: item
items: [item]
}
this.props.uiActions.showContextMenu(data)
}
@ -61,33 +61,28 @@ class LibraryAlbums extends React.Component{
if( this.props.view == 'list' ){
var columns = [
{
width: 30,
label: 'Name',
name: 'name'
},
{
width: 30,
label: 'Artists',
name: 'artists'
},
{
width: 15,
label: 'Added',
name: 'added_at'
},
{
width: 15,
label: 'Released',
name: 'release_date'
},
{
width: 10,
label: 'Tracks',
name: 'tracks_total'
}
]
return (
<section className="list-wrapper">
<section className="list-wrapper album-list">
<List
handleContextMenu={(e,item) => this.handleContextMenu(e,item)}
rows={albums}
@ -216,8 +211,8 @@ const mapStateToProps = (state, ownProps) => {
sort: state.ui.library_albums_sort,
sort_reverse: state.ui.library_albums_sort_reverse,
library_albums: state.ui.library_albums,
library_albums_more: state.ui.library_albums_more,
library_albums_started: state.ui.library_albums_started,
library_albums_more: state.ui.library_albums_more
}
}

View File

@ -26,7 +26,7 @@ class LibraryArtists extends React.Component{
e: e,
context: 'artist',
uris: [item.uri],
item: item
items: [item]
}
this.props.uiActions.showContextMenu(data)
}
@ -57,22 +57,19 @@ class LibraryArtists extends React.Component{
var columns = [
{
label: 'Name',
width: 70,
name: 'name'
},
{
label: 'Followers',
width: 15,
name: 'followers.total'
},
{
label: 'Popularity',
width: 15,
name: 'popularity'
}
]
return (
<section className="list-wrapper">
<section className="list-wrapper artist-list">
<List
handleContextMenu={(e,item) => this.handleContextMenu(e,item)}
rows={artists}
@ -163,6 +160,7 @@ const mapStateToProps = (state, ownProps) => {
sort: state.ui.library_artists_sort,
sort_reverse: state.ui.library_artists_sort_reverse,
library_artists: state.ui.library_artists,
library_artists_started: state.ui.library_artists_started,
library_artists_more: state.ui.library_artists_more,
library_artists_started: state.ui.library_artists_started,
view: state.ui.library_artists_view

View File

@ -32,7 +32,7 @@ class LibraryLocalArtists extends React.Component{
e: e,
context: 'artist',
uris: [item.uri],
item: item
items: [item]
}
this.props.uiActions.showContextMenu(data)
}

View File

@ -26,7 +26,7 @@ class LibraryPlaylists extends React.Component{
e: e,
context: 'playlist',
uris: [item.uri],
item: item
items: [item]
}
this.props.uiActions.showContextMenu(data)
}
@ -60,33 +60,28 @@ class LibraryPlaylists extends React.Component{
if( this.props.view == 'list' ){
var columns = [
{
width: 50,
label: 'Name',
name: 'name'
},
{
width: 20,
label: 'Owner',
name: 'owner'
},
{
width: 10,
label: 'Source',
name: 'source'
},
{
width: 10,
label: 'Tracks',
name: 'tracks_total'
},
{
width: 10,
label: 'Editable',
name: 'can_edit'
}
]
return (
<section className="list-wrapper">
<section className="list-wrapper playlist-list">
<List
handleContextMenu={(e,item) => this.handleContextMenu(e,item)}
rows={playlists}

View File

@ -79,57 +79,105 @@
margin-top: -3px;
background: transparent;
}
@include responsive( $bp_medium ){
padding: 7px 10px 7px 20px;
.col {
&.name {
width: 90%;
padding-right: 10%;
}
&.duration {
width: auto;
position: absolute;
right: 10px;
text-align: right;
}
&.artists,
&.album {
width: auto;
color: $mid_grey;
display: inline-block;
}
&.album {
@include bullet();
}
}
&.header {
display: none;
}
.select-state,
.play-state {
top: 11px;
left: 5px;
}
}
}
&.playlist {
@include responsive( $bp_medium ){
padding: 7px 10px 7px 20px;
.col {
&.name {
width: 30%;
width: 90% !important;
padding-right: 10%;
}
&.owner {
width: 20%;
&.duration {
width: auto;
position: absolute;
right: 10px;
text-align: right;
}
&:not(.name){
color: $mid_grey;
display: inline-block;
width: auto !important;
}
&:not(.name):not(:nth-child(2)){
@include bullet();
}
&.followers_total:after {
content: ' followers';
}
&.tracks_total:after {
content: ' tracks';
}
&.popularity:after {
content: '% popularity';
}
&.can_edit,
&.release_date {
display: none;
}
}
&.header {
display: none;
}
.select-state,
.play-state {
top: 11px;
left: 5px;
}
}
}
&.playlist-list .list-item {
.col {
&.name {
width: 50%;
}
&.owner {
width: 20%
}
&.source,
&.can_edit,
&.tracks_total,
&.popularity {
width: 10%;
}
}
}
&.album-list .list-item {
.col {
&.name,
&.artists {
width: 30%;
}
&.added_at,
&.release_date {
width: 15%;
}
&.tracks_total {
width: 10%;
}
}
}
&.artist-list .list-item {
.col {
&.name {
width: 70%;
}
&.followers_total,
&.popularity {
width: 15%;
}
}
}