Context submenu; Modal beginnings

This commit is contained in:
James Barnsley
2016-11-11 23:21:51 +13:00
parent 31a1856baf
commit c471ae62e7
17 changed files with 294 additions and 19 deletions

View File

@ -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

View File

Before

Width:  |  Height:  |  Size: 703 B

After

Width:  |  Height:  |  Size: 703 B

View File

@ -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
}
}

View File

@ -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
View 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)

View File

@ -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 ){

View File

@ -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;

View File

@ -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

View File

@ -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' }
}

View File

@ -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);

View File

@ -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
}

View File

@ -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>
);
}

View File

@ -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 (

View File

@ -7,6 +7,7 @@
@import 'global/forms';
@import 'components/context-menu';
@import 'components/modal';
@import 'components/dragger';
@import 'components/images';
@import 'components/icon';

View File

@ -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%);
}
}
}
}

View File

@ -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
View 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);
}
}
}
}
}