diff --git a/src/assets/icons/close-white.svg b/src/assets/icons/close-white.svg
deleted file mode 100755
index 4f73b97f..00000000
--- a/src/assets/icons/close-white.svg
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
diff --git a/src/assets/icons/close-black.svg b/src/assets/icons/close.svg
similarity index 100%
rename from src/assets/icons/close-black.svg
rename to src/assets/icons/close.svg
diff --git a/src/js/components/ContextMenu.js b/src/js/components/ContextMenu.js
index 426cd31b..014a98f5 100755
--- a/src/js/components/ContextMenu.js
+++ b/src/js/components/ContextMenu.js
@@ -14,6 +14,33 @@ class ContextMenu extends React.Component{
super(props);
}
+ renderPlaylistSubmenu(){
+ var playlists = []
+ for( var i = 0; i < this.props.playlists.length; i++ ){
+ if( this.props.playlists[i].can_edit ) playlists.push( this.props.playlists[i] )
+ }
+
+ return (
+
+ )
+ }
+
renderItems(){
var items = [];
@@ -22,6 +49,7 @@ class ContextMenu extends React.Component{
case 'queue':
items = [
{ handleClick: 'playQueueItem', label: 'Play' },
+ { handleClick: 'addToPlaylist', label: 'Add to playlist', playlists: true },
{ handleClick: 'removeFromQueue', label: 'Remove' }
];
break;
@@ -31,6 +59,7 @@ class ContextMenu extends React.Component{
{ handleClick: 'playItems', label: 'Play' },
{ handleClick: 'playItemsNext', label: 'Play next' },
{ handleClick: 'addToQueue', label: 'Add to queue' },
+ { handleClick: 'addToPlaylist', label: 'Add to playlist', playlists: true },
{ handleClick: 'removeFromPlaylist', label: 'Remove' }
];
break;
@@ -39,7 +68,8 @@ class ContextMenu extends React.Component{
items = [
{ handleClick: 'playItems', label: 'Play' },
{ handleClick: 'playItemsNext', label: 'Play next' },
- { handleClick: 'addToQueue', label: 'Add to queue' }
+ { handleClick: 'addToQueue', label: 'Add to queue' },
+ { handleClick: 'addToPlaylist', label: 'Add to playlist', playlists: true }
];
break;
}
@@ -48,7 +78,16 @@ class ContextMenu extends React.Component{
@@ -114,6 +153,11 @@ class ContextMenu extends React.Component{
this.props.uiActions.hideContextMenu();
}
+ addToPlaylist(){
+ this.props.uiActions.openModal( 'AddToPlaylistModal', { track_indexes: this.props.context_menu.data.selected_tracks_indexes })
+ this.props.uiActions.hideContextMenu();
+ }
+
removeFromPlaylist(){
this.props.uiActions.removeTracksFromPlaylist( this.props.context_menu.data.selected_tracks_indexes )
this.props.uiActions.hideContextMenu();
@@ -139,7 +183,8 @@ const mapStateToProps = (state, ownProps) => {
return {
context_menu: state.ui.context_menu,
current_track: state.ui.current_track,
- current_tracklist: state.ui.current_tracklist
+ current_tracklist: state.ui.current_tracklist,
+ playlists: state.ui.playlists
}
}
diff --git a/src/js/components/List.js b/src/js/components/List.js
index 2b23bea9..c666b6a5 100755
--- a/src/js/components/List.js
+++ b/src/js/components/List.js
@@ -2,6 +2,8 @@
import React, { PropTypes } from 'react'
import { Link } from 'react-router'
+import FontAwesome from 'react-fontawesome'
+
export default class List extends React.Component{
constructor(props) {
@@ -22,6 +24,12 @@ export default class List extends React.Component{
)
}
+ renderValue( value ){
+ if( typeof(value) === 'undefined' ) return -
+ if( value === true ) return
+ return {value}
+ }
+
render(){
if( !this.props.rows ) return null
@@ -36,7 +44,7 @@ export default class List extends React.Component{
this.props.columns.map( (col, col_index) => {
return (
- { row[col.name] ? row[col.name] : - }
+ { this.renderValue(row[col.name]) }
)
})
diff --git a/src/js/components/Modal.js b/src/js/components/Modal.js
new file mode 100755
index 00000000..f2e20836
--- /dev/null
+++ b/src/js/components/Modal.js
@@ -0,0 +1,90 @@
+
+import React, { PropTypes } from 'react'
+import { connect } from 'react-redux'
+import { bindActionCreators } from 'redux'
+
+import Icon from './Icon'
+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'
+
+class Modal extends React.Component{
+
+ playlistSelected( playlist_uri ){
+ this.props.uiActions.addTracksToPlaylist( playlist_uri, this.props.modal.data.track_indexes )
+ this.props.uiActions.closeModal()
+ }
+
+ renderEditablePlaylists(){
+ if( !this.props.playlists ) return No editable playlists
+ var playlists = []
+ for( var i = 0; i < this.props.playlists.length; i++ ){
+ switch( helpers.uriSource( this.props.playlists[i].uri ) ){
+
+ case 'spotify':
+ if( this.props.playlists[i].can_edit ) playlists.push( this.props.playlists[i] )
+ break
+
+ case 'm3u':
+ playlists.push( this.props.playlists[i] )
+ break
+
+ }
+ }
+
+ return (
+
+ {
+ playlists.map( playlist => {
+ return (
+
this.playlistSelected(playlist.uri) }>
+ { playlist.name }
+
+ )
+ })
+ }
+
+ )
+ }
+
+ render(){
+ if( !this.props.modal ) return null;
+
+ return (
+
+
this.props.uiActions.closeModal() }>
+
+
+
+
+
{ this.props.modal.name }
+
+ { this.renderEditablePlaylists() }
+
+
+
+ );
+ }
+}
+
+const mapStateToProps = (state, ownProps) => {
+ return {
+ modal: state.ui.modal,
+ playlists: state.ui.playlists,
+ context_menu: state.ui.context_menu,
+ mopidy_connected: state.mopidy.connected,
+ spotify_authorized: state.spotify.authorized
+ }
+}
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ uiActions: bindActionCreators(uiActions, dispatch),
+ spotifyActions: bindActionCreators(spotifyActions, dispatch),
+ mopidyActions: bindActionCreators(mopidyActions, dispatch)
+ }
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(Modal)
\ No newline at end of file
diff --git a/src/js/services/localstorage/middleware.js b/src/js/services/localstorage/middleware.js
index 71932e75..db626418 100755
--- a/src/js/services/localstorage/middleware.js
+++ b/src/js/services/localstorage/middleware.js
@@ -12,7 +12,7 @@ const localstorageMiddleware = (function(){
// append our state to a global variable. This gives us access to debug the store at any point
window._store = store
- console.log(action)
+ //console.log(action)
switch( action.type ){
diff --git a/src/js/services/mopidy/middleware.js b/src/js/services/mopidy/middleware.js
index 40500b9c..352a67c2 100755
--- a/src/js/services/mopidy/middleware.js
+++ b/src/js/services/mopidy/middleware.js
@@ -191,6 +191,9 @@ const MopidyMiddleware = (function(){
case 'MOPIDY_PLAYLISTS':
instruct( socket, store, 'playlists.asList' )
.then( response => {
+ for( var i = 0; i < response.length; i++ ){
+ if( response[i].uri.startsWith('m3u:')) response[i] = Object.assign({}, response[i], { can_edit: true })
+ }
store.dispatch({ type: 'MOPIDY_PLAYLISTS_LOADED', data: response });
})
break;
diff --git a/src/js/services/spotify/actions.js b/src/js/services/spotify/actions.js
index 5c3a7943..e5532bf6 100755
--- a/src/js/services/spotify/actions.js
+++ b/src/js/services/spotify/actions.js
@@ -543,6 +543,17 @@ function loadNextPlaylistsBatch( dispatch, getState, playlists, lastResponse ){
loadNextPlaylistsBatch( dispatch, getState, playlists, response )
});
}else{
+
+ // check our editability of each playlist
+ // used to define what we can add tracks to
+ if( getState().spotify.authorized ){
+ for( var i = 0; i < playlists.length; i++ ){
+ if( playlists[i].owner.id == getState().spotify.me.id ){
+ playlists[i] = Object.assign({}, playlists[i], { can_edit: true })
+ }
+ }
+ }
+
dispatch({
type: 'SPOTIFY_LIBRARY_PLAYLISTS_LOADED',
data: playlists
diff --git a/src/js/services/ui/actions.js b/src/js/services/ui/actions.js
index 58d0767c..c5f78aaa 100755
--- a/src/js/services/ui/actions.js
+++ b/src/js/services/ui/actions.js
@@ -72,4 +72,25 @@ export function removeTracksFromPlaylist( track_indexes ){
}
}
+export function addTracksToPlaylist( playlist_uri, tracks ){
+ return {
+ type: 'ADD_TRACKS_TO_PLAYLIST',
+ playlist_uri: playlist_uri,
+ tracks: tracks
+ }
+}
+
+export function openModal( name, data ){
+ return {
+ type: 'OPEN_MODAL',
+ modal: {
+ name: name,
+ data: data
+ }
+ }
+}
+
+export function closeModal(){
+ return { type: 'CLOSE_MODAL' }
+}
diff --git a/src/js/services/ui/middleware.js b/src/js/services/ui/middleware.js
index 66f6a7ab..fdd07c3a 100755
--- a/src/js/services/ui/middleware.js
+++ b/src/js/services/ui/middleware.js
@@ -35,6 +35,10 @@ const UIMiddleware = (function(){
break
}
+ case 'ADD_TRACKS_TO_PLAYLIST':
+ console.log(action)
+ break
+
// This action is irrelevant to us, pass it on to the next middleware
default:
return next(action);
diff --git a/src/js/services/ui/reducer.js b/src/js/services/ui/reducer.js
index f7f94395..c4c54b9d 100755
--- a/src/js/services/ui/reducer.js
+++ b/src/js/services/ui/reducer.js
@@ -189,6 +189,9 @@ export default function reducer(ui = {}, action){
/**
* Library Playlists
+ *
+ * TODO: Map sources and merge any replicated URIS
+ *
**/
case 'MOPIDY_PLAYLISTS_LOADED':
@@ -312,6 +315,18 @@ export default function reducer(ui = {}, action){
})
return Object.assign({}, ui, { search_results: results })
+
+ /**
+ * Modals
+ **/
+
+ case 'OPEN_MODAL':
+ return Object.assign({}, ui, { modal: action.modal })
+
+ case 'CLOSE_MODAL':
+ return Object.assign({}, ui, { modal: false })
+
+
default:
return ui
}
diff --git a/src/js/views/App.js b/src/js/views/App.js
index b721f786..540210a1 100755
--- a/src/js/views/App.js
+++ b/src/js/views/App.js
@@ -8,6 +8,7 @@ import { connect } from 'react-redux'
import Sidebar from '../components/Sidebar'
import ContextMenu from '../components/ContextMenu'
import Dragger from '../components/Dragger'
+import Modal from '../components/Modal'
import * as uiActions from '../services/ui/actions'
import * as pusherActions from '../services/pusher/actions'
@@ -112,6 +113,7 @@ class App extends React.Component{
+
);
}
diff --git a/src/js/views/library/LibraryPlaylists.js b/src/js/views/library/LibraryPlaylists.js
index 8b4a8c93..f746e650 100755
--- a/src/js/views/library/LibraryPlaylists.js
+++ b/src/js/views/library/LibraryPlaylists.js
@@ -19,8 +19,9 @@ class LibraryPlaylists extends React.Component{
render(){
if( !this.props.playlists ) return null
var columns = [
- { name: 'name', width: '50'},
- { name: 'uri', width: '25'}
+ { name: 'name', width: '40'},
+ { name: 'can_edit', width: '15'},
+ { name: 'uri', width: '45'}
]
return (
diff --git a/src/scss/app.scss b/src/scss/app.scss
index 5c8f387b..b13a1157 100755
--- a/src/scss/app.scss
+++ b/src/scss/app.scss
@@ -7,6 +7,7 @@
@import 'global/forms';
@import 'components/context-menu';
+@import 'components/modal';
@import 'components/dragger';
@import 'components/images';
@import 'components/icon';
diff --git a/src/scss/components/_context-menu.scss b/src/scss/components/_context-menu.scss
index 8a378c4f..a54f2260 100755
--- a/src/scss/components/_context-menu.scss
+++ b/src/scss/components/_context-menu.scss
@@ -3,14 +3,15 @@
position: fixed;
left: 0;
top: 0;
- z-index: 9999;
+ z-index: 98;
background: $dark_grey;
color: #FFFFFF;
- a {
+ .menu-item {
display: block;
padding: 8px 12px;
width: 120px;
+ box-sizing: border-box;
&:not(:first-child){
border-top: 1px solid lighten($dark_grey, 8%);
@@ -19,6 +20,24 @@
&:hover {
cursor: pointer;
background: lighten($dark_grey, 5%);
+
+ .submenu {
+ display: block;
+ }
+ }
+
+ .submenu {
+ display: none;
+ position: absolute;
+ left: 120px;
+ top: 0;
+ max-height: 400px;
+ overflow-y: scroll;
+ background: lighten($dark_grey, 5%);
+
+ .menu-item:hover {
+ background: lighten($dark_grey, 10%);
+ }
}
}
}
\ No newline at end of file
diff --git a/src/scss/components/_dragger.scss b/src/scss/components/_dragger.scss
index d3b075ee..50fb64b1 100755
--- a/src/scss/components/_dragger.scss
+++ b/src/scss/components/_dragger.scss
@@ -9,7 +9,7 @@
position: fixed;
left: 0;
top: 0;
- z-index: 9999;
+ z-index: 97;
background: $blue;
color: #FFFFFF;
padding: 12px 20px;
@@ -22,7 +22,7 @@
left: 0;
width: 100%;
height: 100%;
- z-index: 9998;
+ z-index: 98;
background: rgba(25, 25, 25, 0.9);
.dropzone {
diff --git a/src/scss/components/_modal.scss b/src/scss/components/_modal.scss
new file mode 100755
index 00000000..6f82ee93
--- /dev/null
+++ b/src/scss/components/_modal.scss
@@ -0,0 +1,63 @@
+
+.modal {
+ position: fixed;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ right: 0;
+ z-index: 99;
+ background: rgba(10, 10, 10, 0.95);
+ overflow-y: scroll;
+
+ .close-modal {
+ @include animate();
+ position: absolute;
+ top: 10px;
+ right: 10px;
+ opacity: 0.5;
+ cursor: pointer;
+ padding: 20px;
+
+ img {
+ width: 30px;
+ }
+
+ &:hover {
+ opacity: 1;
+ }
+ }
+
+ .content {
+ padding: 100px;
+ margin: 0 auto;
+ width: 70%;
+ color: #FFFFFF;
+
+ .playlists {
+ @include clearfix();
+
+ .playlist {
+ float: left;
+ box-sizing: border-box;
+ display: block;
+ width: 49%;
+ padding: 16px 20px;
+ border-bottom: 1px solid $mid_grey;
+
+ &:nth-child(1),
+ &:nth-child(2){
+ border-top: 1px solid $mid_grey;
+ }
+
+ &:nth-child(2n+1){
+ float: right;
+ }
+
+ &:hover {
+ cursor: pointer;
+ background: rgba(255,255,255,0.1);
+ }
+ }
+ }
+ }
+}
\ No newline at end of file