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 ( +
+ { + playlists.map( playlist => { + return ( + { + this.props.uiActions.addTracksToPlaylist( playlist.uri, this.props.context_menu.data.selected_tracks ) + this.props.uiActions.hideContextMenu() } + }> + { playlist.name } + + ) + }) + } +
+ ) + } + 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{
{ items.map((item, index) => { - return this[item.handleClick](e) }>{ item.label } + if( item.playlists ){ + return ( + + { item.label } + { this.renderPlaylistSubmenu() } + + ) + }else{ + return this[item.handleClick](e) }>{ item.label } + } }) }
@@ -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