Editing playlist modal

This commit is contained in:
James Barnsley
2016-11-14 17:32:36 +13:00
parent cc99c8bf37
commit df30327fe3
18 changed files with 423 additions and 231 deletions

View File

@ -13,6 +13,7 @@ export default class Header extends React.Component{
<header>
<Icon name={ this.props.icon } />
<h1>{ this.props.title }</h1>
{ this.props.actions ? <div className="actions">{ this.props.actions }</div> : null }
</header>
);
}

View File

@ -1,160 +0,0 @@
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{
constructor(props){
super(props)
this.state = {
playlist_name: '',
playlist_scheme: 'spotify',
playlist_private: true
}
}
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>
)
}
createPlaylist(e){
e.preventDefault();
this.props.uiActions.createPlaylist( this.state.playlist_scheme, this.state.playlist_name, this.state.playlist_public );
return false;
}
renderCreatePlaylist(){
return (
<div>
<h1>Create playlist</h1>
<form onSubmit={(e) => this.createPlaylist(e)}>
<div className="field">
<div className="name">Name</div>
<div className="input">
<input
type="text"
onChange={ e => this.setState({ playlist_name: e.target.value })}
value={ this.state.playlist_name } />
</div>
</div>
<div className="field radio white">
<div className="name">Provider</div>
<div className="input">
<label>
<input
type="radio"
name="playlist_scheme"
value="spotify"
checked={ this.state.playlist_scheme == 'spotify' }
onChange={ e => this.setState({ playlist_scheme: e.target.value })} />
<span className="label">Spotify</span>
</label>
<label>
<input
type="radio"
name="playlist_scheme"
value="m3u"
checked={ this.state.playlist_scheme == 'm3u' }
onChange={ e => this.setState({ playlist_scheme: e.target.value })} />
<span className="label">Local</span>
</label>
</div>
</div>
<div className="field checkbox white">
<div className="name">Options</div>
<div className="input">
<label>
<input
type="checkbox"
name="playlist_private"
value={ this.state.playlist_private }
onChange={ e => this.setState({ playlist_private: !this.state.playlist_private })} />
<span className="label">Private</span>
</label>
</div>
</div>
<button type="submit" className="primary centered">Save</button>
</form>
</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">
{ this.props.modal.name == 'add_to_playlist' ? this.renderEditablePlaylists() : null }
{ this.props.modal.name == 'create_playlist' ? this.renderCreatePlaylist() : null }
</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

@ -0,0 +1,38 @@
import React, { PropTypes } from 'react'
import Icon from '../Icon'
import * as helpers from '../../helpers'
class AddToPlaylistModal extends React.Component{
constructor(props){
super(props)
}
playlistSelected( playlist_uri ){
this.props.uiActions.addTracksToPlaylist( playlist_uri, this.props.data.track_indexes )
this.props.uiActions.closeModal()
}
render(){
if( !this.props.playlists ) return <div className="empty">No editable playlists</div>
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="playlists">
{
playlists.map( playlist => {
return (
<div className="playlist" key={playlist.uri} onClick={ () => this.playlistSelected(playlist.uri) }>
{ playlist.name }
</div>
)
})
}
</div>
)
}
}

View File

@ -0,0 +1,82 @@
import React, { PropTypes } from 'react'
import Icon from '../Icon'
import * as helpers from '../../helpers'
export default class CreatePlaylistModal extends React.Component{
constructor(props){
super(props)
this.state = {
submit_enabled: false,
name: '',
scheme: 'spotify',
is_public: true
}
}
createPlaylist(e){
e.preventDefault();
this.props.uiActions.createPlaylist( this.state.scheme, this.state.name, this.state.is_public )
this.props.uiActions.closeModal()
return false;
}
setPlaylistName(name){
var submit_enabled = false
if( name && name != '' ) submit_enabled = true
this.setState({
name: name,
submit_enabled: submit_enabled
})
}
render(){
return (
<div>
<h4>Create playlist</h4>
<form onSubmit={(e) => this.createPlaylist(e)}>
<div className="field">
<input
type="text"
placeholder="Playlist name"
onChange={ e => this.setPlaylistName( e.target.value )}
value={ this.state.name } />
</div>
<div className="field radio white">
<label>
<input
type="radio"
name="scheme"
value="spotify"
checked={ this.state.scheme == 'spotify' }
onChange={ e => this.setState({ scheme: e.target.value })} />
<span className="label">Spotify</span>
</label>
<label>
<input
type="radio"
name="scheme"
value="m3u"
checked={ this.state.scheme == 'm3u' }
onChange={ e => this.setState({ scheme: e.target.value })} />
<span className="label">Local (m3u)</span>
</label>
</div>
<div className="field checkbox white">
<label>
<input
type="checkbox"
name="is_public"
checked={ this.state.is_public }
onChange={ e => this.setState({ is_public: !this.state.is_public })} />
<span className="label">Public</span>
</label>
</div>
<button type="submit" className="primary centered" disabled={!this.state.submit_enabled}>Save</button>
</form>
</div>
)
}
}

View File

@ -0,0 +1,61 @@
import React from 'react'
import Icon from '../Icon'
import * as helpers from '../../helpers'
export default class EditPlaylistModal extends React.Component{
constructor(props){
super(props)
this.state = {
submit_enabled: true,
name: this.props.data.name,
is_public: this.props.data.is_public
}
}
setPlaylistName(name){
var submit_enabled = false
if( name && name != '' ) submit_enabled = true
this.setState({
name: name,
submit_enabled: submit_enabled
})
}
savePlaylist(e){
e.preventDefault();
this.props.uiActions.savePlaylist( this.props.data.uri, this.state.name, this.state.is_public )
this.props.uiActions.closeModal()
return false;
}
render(){
return (
<div>
<h4>Edit playlist</h4>
<form onSubmit={(e) => this.savePlaylist(e)}>
<div className="field">
<input
type="text"
placeholder="Playlist name"
onChange={ e => this.setPlaylistName( e.target.value )}
value={ this.state.name } />
</div>
<div className="field checkbox white">
<label>
<input
type="checkbox"
name="playlist_private"
checked={ this.state.is_public }
onChange={ e => this.setState({ is_public: !this.state.is_public })} />
<span className="label">Public</span>
</label>
</div>
<button type="submit" className="primary centered" disabled={!this.state.submit_enabled}>Save</button>
</form>
</div>
)
}
}

View File

@ -0,0 +1,60 @@
import React, { PropTypes } from 'react'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import * as helpers from '../../helpers'
import Icon from '../Icon'
import AddToPlaylistModal from './AddToPlaylistModal'
import CreatePlaylistModal from './CreatePlaylistModal'
import EditPlaylistModal from './EditPlaylistModal'
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{
constructor(props){
super(props)
}
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">
{ this.props.modal.name == 'add_to_playlist' ? <AddToPlaylistModal /> : null }
{ this.props.modal.name == 'create_playlist' ? <CreatePlaylistModal /> : null }
{ this.props.modal.name == 'edit_playlist' ? <EditPlaylistModal uiActions={this.props.uiActions} data={this.props.modal.data} /> : null }
</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

@ -280,7 +280,11 @@ const MopidyMiddleware = (function(){
}
var playlist = Object.assign({}, response)
playlist.tracks = [...playlist.tracks, ...tracks]
if( playlist.tracks ){
playlist.tracks = [...playlist.tracks, ...tracks]
}else{
playlist.tracks = tracks
}
instruct( socket, store, 'playlists.save', { playlist: playlist } )
.then( response => {
@ -320,6 +324,21 @@ const MopidyMiddleware = (function(){
});
break
case 'MOPIDY_SAVE_PLAYLIST':
instruct( socket, store, 'playlists.lookup', { uri: action.uri })
.then( response => {
var playlist = Object.assign({}, response, { name: action.name })
instruct( socket, store, 'playlists.save', { playlist: playlist } )
.then( response => {
// TODO: changing the name changes the URI, so essentially invalidates our url...
// need to figure out how to handle this
store.dispatch({ type: 'PLAYLIST_UPDATED', playlist: playlist });
})
});
break
case 'MOPIDY_DELETE_PLAYLIST':
instruct( socket, store, 'playlists.delete', { uri: action.uri })
.then( response => {

View File

@ -503,16 +503,32 @@ export function getSearchResults( query, type = 'album,artist,playlist,track', l
* Playlists
**/
export function createPlaylist( name, is_private ){
export function createPlaylist( name, is_public ){
return (dispatch, getState) => {
sendRequest( dispatch, getState, 'users/'+ getState().spotify.me.id +'/playlists/', 'POST', { name: name, public: !is_private } )
sendRequest( dispatch, getState, 'users/'+ getState().spotify.me.id +'/playlists/', 'POST', { name: name, public: is_public } )
.then( response => {
dispatch( getAllLibraryPlaylists() );
})
}
}
export function savePlaylist( uri, name, is_public ){
return (dispatch, getState) => {
sendRequest( dispatch, getState, 'users/'+ getState().spotify.me.id +'/playlists/'+ helpers.getFromUri('playlistid',uri), 'PUT', { name: name, public: is_public } )
.then( response => {
dispatch({
type: 'PLAYLIST_UPDATED',
playlist: {
name: name,
public: is_public
}
});
})
}
}
export function getPlaylist( uri ){
return (dispatch, getState) => {

View File

@ -51,6 +51,16 @@ const SpotifyMiddleware = (function(){
store.dispatch( actions.addTracksToPlaylist( action.playlist_uri, action.tracks_uris ))
break
case 'SPOTIFY_SAVE_PLAYLIST':
if( !store.getState().spotify.authorized ){
alert('Must be logged in to Spotify to do this')
return
}
store.dispatch( actions.savePlaylist( action.uri, action.name, action.is_public ))
break
// when our mopidy server current track changes
case 'MOPIDY_CURRENTTLTRACK':

View File

@ -66,14 +66,35 @@ export function dragEnd(){
return { type: 'DRAG_END' }
}
export function createPlaylist( scheme, name, is_private = false ){
export function savePlaylist( uri, name, is_public = false ){
switch( helpers.uriSource( uri ) ){
case 'spotify':
return {
type: 'SPOTIFY_SAVE_PLAYLIST',
uri: uri,
name: name,
is_public: is_public
}
case 'm3u':
return {
type: 'MOPIDY_SAVE_PLAYLIST',
uri: uri,
name: name
}
}
return false
}
export function createPlaylist( scheme, name, is_public = false ){
switch( scheme ){
case 'spotify':
return {
type: 'SPOTIFY_CREATE_PLAYLIST',
name: name,
is_private: is_private
is_public: is_public
}
case 'm3u':

View File

@ -180,6 +180,10 @@ export default function reducer(ui = {}, action){
* Playlists
**/
case 'PLAYLIST_UPDATED':
var playlist = Object.assign({}, ui.playlist, action.playlist)
return Object.assign({}, ui, { playlist: playlist })
case 'MOPIDY_PLAYLIST_LOADED':
if( !action.data ) return Object.assign({}, ui, { playlist: false })
return Object.assign({}, ui, { playlist: action.data })

View File

@ -8,7 +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 Modal from '../components/Modal/Modal'
import * as uiActions from '../services/ui/actions'
import * as pusherActions from '../services/pusher/actions'

View File

@ -74,23 +74,30 @@ class Playlist extends React.Component{
this.props.uiActions.removeTracksFromPlaylist( this.props.playlist.uri, track_indexes )
}
renderFollowOrDeleteButton(){
renderEditButtons(){
switch( helpers.uriSource( this.props.params.uri ) ){
case 'm3u':
return <ConfirmationButton className="large tertiary" content="Delete" confirmingContent="Are you sure?" onConfirm={ e => this.delete() } />
break
return (
<span>
<button className="large tertiary" onClick={ e => this.props.uiActions.openModal('edit_playlist', { uri: this.props.playlist.uri, name: this.props.playlist.name }) }>Edit</button>
<ConfirmationButton className="large tertiary" content="Delete" confirmingContent="Are you sure?" onConfirm={ e => this.delete() } />
</span>
)
case 'spotify':
if( !this.props.spotify_authorized ) return null
if( this.props.playlist.owner && this.props.playlist.owner.id == this.props.spotify_userid ){
return <ConfirmationButton className="large tertiary" content="Delete" confirmingContent="Are you sure?" onConfirm={ e => this.unfollow() } />
return (
<span>
<button className="large tertiary" onClick={ e => this.props.uiActions.openModal('edit_playlist', { uri: this.props.playlist.uri, name: this.props.playlist.name, is_public: this.props.playlist.public }) }>Edit</button>
<ConfirmationButton className="large tertiary" content="Delete" confirmingContent="Are you sure?" onConfirm={ e => this.unfollow() } />
</span>
)
}else if( this.props.playlist.following ){
return <button className="large tertiary" onClick={ e => this.unfollow() }>Unfollow</button>
}else{
return <button className="large tertiary" onClick={ e => this.follow() }>Follow</button>
}
break
return <button className="large tertiary" onClick={ e => this.follow() }>Follow</button>
}
}
@ -107,7 +114,7 @@ class Playlist extends React.Component{
<div className="actions">
<button className="large primary" onClick={ e => this.play() }>Play</button>
{ this.renderFollowOrDeleteButton() }
{ this.renderEditButtons() }
</div>
<ul className="details">

View File

@ -27,15 +27,17 @@ class LibraryPlaylists extends React.Component{
{ name: 'uri', width: '40'}
]
var actions = (
<button onClick={ () => this.props.uiActions.openModal('create_playlist', {} ) }>
<FontAwesome name="plus" />&nbsp;
New
</button>
)
return (
<div className="view library-playlists-view">
<Header icon="playlist" title="My playlists" />
<button onClick={ () => this.props.uiActions.openModal('create_playlist',false) }>
<FontAwesome name="plus" />&nbsp;
New playlist
</button>
<Header icon="playlist" title="My playlists" actions={actions} />
<section className="grid-wrapper">
<PlaylistGrid playlists={this.props.playlists} />

View File

@ -4,16 +4,30 @@ main {
padding: 14px 20px;
background: #EEEEEE;
h1 {
font-size: 22px;
font-weight: 100;
display: inline-block;
}
.icon {
display: inline-block;
padding-right: 14px;
height: 16px;
}
h1 {
font-size: 22px;
font-weight: 100;
display: inline-block;
.actions {
position: absolute;
top: 0;
right: 10px;
button {
background: transparent;
font-size: 14px;
font-weight: 600;
padding: 16px 10px;
text-transform: uppercase;
}
}
}
}

View File

@ -30,18 +30,35 @@
.content {
padding: 100px;
margin: 0 auto;
width: 70%;
width: 50%;
color: #FFFFFF;
h1 {
font-size: 30px;
font-weight: 300;
padding-bottom: 30px;
h4 {
padding-bottom: 40px;
}
form {
.field {
padding-bottom: 20px;
input[type="text"]{
background: transparent;
font-size: 24px;
border-bottom: 2px solid #FFFFFF;
padding-left: 0;
padding-right: 0;
width: 100%;
color: #FFFFFF;
&:focus,
&:active {
border-color: $turquoise;
}
}
.label {
font-size: 16px;
}
}
button[type="submit"]{

View File

@ -39,49 +39,49 @@ main {
section.list-wrapper {
padding: 20px 0;
}
h1 {
font-weight: 500;
font-size: 40px;
}
h2 {
font-weight: 500;
font-size: 30px;
}
h3 {
font-weight: 100;
font-style: italic;
font-size: 20px;
}
h4 {
font-size: 14px;
margin-bottom: 14px;
text-transform: uppercase;
&.underline {
padding-bottom: 4px;
border-bottom: 1px solid #000000;
}
&:not(:first-child){
padding-top: 40px;
}
}
.no-top-padding { padding-top: 0 !important; }
.no-bottom-padding { padding-bottom: 0 !important; }
.no-right-padding { padding-right: 0 !important; }
.no-left-padding { padding-left: 0 !important; }
.top-padding { padding-top: 20px; }
.bottom-padding { padding-bottom: 20px; }
.right-padding { padding-right: 40px; }
.left-padding { padding-left: 40px; }
}
h1 {
font-weight: 500;
font-size: 40px;
}
h2 {
font-weight: 500;
font-size: 30px;
}
h3 {
font-weight: 100;
font-style: italic;
font-size: 20px;
}
h4 {
font-size: 14px;
margin-bottom: 14px;
text-transform: uppercase;
&.underline {
padding-bottom: 4px;
border-bottom: 1px solid #000000;
}
&:not(:first-child){
padding-top: 40px;
}
}
.no-top-padding { padding-top: 0 !important; }
.no-bottom-padding { padding-bottom: 0 !important; }
.no-right-padding { padding-right: 0 !important; }
.no-left-padding { padding-left: 0 !important; }
.top-padding { padding-top: 20px; }
.bottom-padding { padding-bottom: 20px; }
.right-padding { padding-right: 40px; }
.left-padding { padding-left: 40px; }
.col {
min-height: 1px;
float: left;

View File

@ -99,7 +99,7 @@ input[type="submit"] {
&[disabled],
&[disabled="disabled"] {
background: $light_grey !important;
background: rgba(200,200,200,0.4) !important;
color: $mid_grey !important;
cursor: not-allowed !important;
}