Context submenu; Modal beginnings
This commit is contained in:
@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="50px" height="50px" viewBox="0 0 50 50" enable-background="new 0 0 50 50" xml:space="preserve">
|
||||
<line fill="none" stroke="#FFFFFF" stroke-width="2.8346" stroke-miterlimit="10" x1="6" y1="6" x2="44" y2="44"/>
|
||||
<line fill="none" stroke="#FFFFFF" stroke-width="2.8346" stroke-miterlimit="10" x1="6" y1="44" x2="44" y2="6"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 703 B |
|
Before Width: | Height: | Size: 703 B After Width: | Height: | Size: 703 B |
@ -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 (
|
||||
<div className="submenu">
|
||||
{
|
||||
playlists.map( playlist => {
|
||||
return (
|
||||
<a
|
||||
className="menu-item"
|
||||
key={playlist.uri}
|
||||
onClick={ () => {
|
||||
this.props.uiActions.addTracksToPlaylist( playlist.uri, this.props.context_menu.data.selected_tracks )
|
||||
this.props.uiActions.hideContextMenu() }
|
||||
}>
|
||||
{ playlist.name }
|
||||
</a>
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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{
|
||||
<div>
|
||||
{
|
||||
items.map((item, index) => {
|
||||
return <a key={item.handleClick} onClick={ (e) => this[item.handleClick](e) }>{ item.label }</a>
|
||||
if( item.playlists ){
|
||||
return (
|
||||
<span key={item.handleClick} className="menu-item has-submenu">
|
||||
{ item.label }
|
||||
{ this.renderPlaylistSubmenu() }
|
||||
</span>
|
||||
)
|
||||
}else{
|
||||
return <a className="menu-item" key={item.handleClick} onClick={ (e) => this[item.handleClick](e) }>{ item.label }</a>
|
||||
}
|
||||
})
|
||||
}
|
||||
</div>
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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 <span>-</span>
|
||||
if( value === true ) return <FontAwesome name="check" />
|
||||
return <span>{value}</span>
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className={'col w'+col.width} key={col_index}>
|
||||
{ row[col.name] ? row[col.name] : <span>-</span> }
|
||||
{ this.renderValue(row[col.name]) }
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
90
src/js/components/Modal.js
Executable file
90
src/js/components/Modal.js
Executable file
@ -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 <div className="empty">No editable playlists</div>
|
||||
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 (
|
||||
<div className="playlists">
|
||||
{
|
||||
playlists.map( playlist => {
|
||||
return (
|
||||
<div className="playlist" key={playlist.uri} onClick={ () => this.playlistSelected(playlist.uri) }>
|
||||
{ playlist.name }
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render(){
|
||||
if( !this.props.modal ) return null;
|
||||
|
||||
return (
|
||||
<div className="modal">
|
||||
<div className="close-modal" onClick={ () => this.props.uiActions.closeModal() }>
|
||||
<Icon name="close" className="white" />
|
||||
</div>
|
||||
<div className="content">
|
||||
|
||||
<h1>{ this.props.modal.name }</h1>
|
||||
|
||||
{ this.renderEditablePlaylists() }
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
@ -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 ){
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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' }
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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{
|
||||
</main>
|
||||
<ContextMenu state={this.props.context_menu} />
|
||||
<Dragger />
|
||||
<Modal />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
@import 'global/forms';
|
||||
|
||||
@import 'components/context-menu';
|
||||
@import 'components/modal';
|
||||
@import 'components/dragger';
|
||||
@import 'components/images';
|
||||
@import 'components/icon';
|
||||
|
||||
@ -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%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
63
src/scss/components/_modal.scss
Executable file
63
src/scss/components/_modal.scss
Executable file
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user