From df30327fe3040e2e964352a6c13493a98c067536 Mon Sep 17 00:00:00 2001 From: James Barnsley Date: Mon, 14 Nov 2016 17:32:36 +1300 Subject: [PATCH] Editing playlist modal --- src/js/components/Header.js | 1 + src/js/components/Modal.js | 160 ------------------ src/js/components/Modal/AddToPlaylistModal.js | 38 +++++ .../components/Modal/CreatePlaylistModal.js | 82 +++++++++ src/js/components/Modal/EditPlaylistModal.js | 61 +++++++ src/js/components/Modal/Modal.js | 60 +++++++ src/js/services/mopidy/middleware.js | 21 ++- src/js/services/spotify/actions.js | 20 ++- src/js/services/spotify/middleware.js | 10 ++ src/js/services/ui/actions.js | 25 ++- src/js/services/ui/reducer.js | 4 + src/js/views/App.js | 2 +- src/js/views/Playlist.js | 23 ++- src/js/views/library/LibraryPlaylists.js | 14 +- src/scss/components/_header.scss | 22 ++- src/scss/components/_modal.scss | 27 ++- src/scss/global/_core.scss | 82 ++++----- src/scss/global/_forms.scss | 2 +- 18 files changed, 423 insertions(+), 231 deletions(-) delete mode 100755 src/js/components/Modal.js create mode 100755 src/js/components/Modal/AddToPlaylistModal.js create mode 100755 src/js/components/Modal/CreatePlaylistModal.js create mode 100755 src/js/components/Modal/EditPlaylistModal.js create mode 100755 src/js/components/Modal/Modal.js diff --git a/src/js/components/Header.js b/src/js/components/Header.js index 73457352..de43ed61 100755 --- a/src/js/components/Header.js +++ b/src/js/components/Header.js @@ -13,6 +13,7 @@ export default class Header extends React.Component{

{ this.props.title }

+ { this.props.actions ?
{ this.props.actions }
: null }
); } diff --git a/src/js/components/Modal.js b/src/js/components/Modal.js deleted file mode 100755 index 0e7f0899..00000000 --- a/src/js/components/Modal.js +++ /dev/null @@ -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
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 } -
- ) - }) - } -
- ) - } - - createPlaylist(e){ - e.preventDefault(); - this.props.uiActions.createPlaylist( this.state.playlist_scheme, this.state.playlist_name, this.state.playlist_public ); - return false; - } - - renderCreatePlaylist(){ - return ( -
-

Create playlist

-
this.createPlaylist(e)}> -
-
Name
-
- this.setState({ playlist_name: e.target.value })} - value={ this.state.playlist_name } /> -
-
-
-
Provider
-
- - -
-
-
-
Options
-
- -
-
- -
-
- ) - } - - render(){ - if( !this.props.modal ) return null; - - return ( -
-
this.props.uiActions.closeModal() }> - -
-
- - { this.props.modal.name == 'add_to_playlist' ? this.renderEditablePlaylists() : null } - { this.props.modal.name == 'create_playlist' ? this.renderCreatePlaylist() : null } - -
-
- ); - } -} - -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/components/Modal/AddToPlaylistModal.js b/src/js/components/Modal/AddToPlaylistModal.js new file mode 100755 index 00000000..d9845ac4 --- /dev/null +++ b/src/js/components/Modal/AddToPlaylistModal.js @@ -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
No editable playlists
+ 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.playlistSelected(playlist.uri) }> + { playlist.name } +
+ ) + }) + } +
+ ) + } +} \ No newline at end of file diff --git a/src/js/components/Modal/CreatePlaylistModal.js b/src/js/components/Modal/CreatePlaylistModal.js new file mode 100755 index 00000000..7033218c --- /dev/null +++ b/src/js/components/Modal/CreatePlaylistModal.js @@ -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 ( +
+

Create playlist

+
this.createPlaylist(e)}> +
+ this.setPlaylistName( e.target.value )} + value={ this.state.name } /> +
+
+ + +
+
+ +
+ +
+
+ ) + } +} \ No newline at end of file diff --git a/src/js/components/Modal/EditPlaylistModal.js b/src/js/components/Modal/EditPlaylistModal.js new file mode 100755 index 00000000..766d8375 --- /dev/null +++ b/src/js/components/Modal/EditPlaylistModal.js @@ -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 ( +
+

Edit playlist

+
this.savePlaylist(e)}> +
+ this.setPlaylistName( e.target.value )} + value={ this.state.name } /> +
+
+ +
+ +
+
+ ) + } +} diff --git a/src/js/components/Modal/Modal.js b/src/js/components/Modal/Modal.js new file mode 100755 index 00000000..08cdff8e --- /dev/null +++ b/src/js/components/Modal/Modal.js @@ -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 ( +
+
this.props.uiActions.closeModal() }> + +
+
+ + { this.props.modal.name == 'add_to_playlist' ? : null } + { this.props.modal.name == 'create_playlist' ? : null } + { this.props.modal.name == 'edit_playlist' ? : null } + +
+
+ ); + } +} + +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/mopidy/middleware.js b/src/js/services/mopidy/middleware.js index 6f8fe1c0..1b5752b2 100755 --- a/src/js/services/mopidy/middleware.js +++ b/src/js/services/mopidy/middleware.js @@ -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 => { diff --git a/src/js/services/spotify/actions.js b/src/js/services/spotify/actions.js index 615d7549..c38a7e9d 100755 --- a/src/js/services/spotify/actions.js +++ b/src/js/services/spotify/actions.js @@ -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) => { diff --git a/src/js/services/spotify/middleware.js b/src/js/services/spotify/middleware.js index 6430c51b..6960dc9c 100755 --- a/src/js/services/spotify/middleware.js +++ b/src/js/services/spotify/middleware.js @@ -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': diff --git a/src/js/services/ui/actions.js b/src/js/services/ui/actions.js index 8f21b4e0..c6a7d418 100755 --- a/src/js/services/ui/actions.js +++ b/src/js/services/ui/actions.js @@ -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': diff --git a/src/js/services/ui/reducer.js b/src/js/services/ui/reducer.js index f028d51a..a3b4b2a6 100755 --- a/src/js/services/ui/reducer.js +++ b/src/js/services/ui/reducer.js @@ -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 }) diff --git a/src/js/views/App.js b/src/js/views/App.js index 540210a1..b96a0a40 100755 --- a/src/js/views/App.js +++ b/src/js/views/App.js @@ -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' diff --git a/src/js/views/Playlist.js b/src/js/views/Playlist.js index 9bf4c697..79bfbe10 100755 --- a/src/js/views/Playlist.js +++ b/src/js/views/Playlist.js @@ -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 this.delete() } /> - break + return ( + + + this.delete() } /> + + ) case 'spotify': if( !this.props.spotify_authorized ) return null if( this.props.playlist.owner && this.props.playlist.owner.id == this.props.spotify_userid ){ - return this.unfollow() } /> + return ( + + + this.unfollow() } /> + + ) }else if( this.props.playlist.following ){ return - }else{ - return } - break + return } } @@ -107,7 +114,7 @@ class Playlist extends React.Component{
- { this.renderFollowOrDeleteButton() } + { this.renderEditButtons() }
    diff --git a/src/js/views/library/LibraryPlaylists.js b/src/js/views/library/LibraryPlaylists.js index 097ff6bd..eddb5c84 100755 --- a/src/js/views/library/LibraryPlaylists.js +++ b/src/js/views/library/LibraryPlaylists.js @@ -27,15 +27,17 @@ class LibraryPlaylists extends React.Component{ { name: 'uri', width: '40'} ] + var actions = ( + + ) + return (
    -
    - - +
    diff --git a/src/scss/components/_header.scss b/src/scss/components/_header.scss index dcbc9561..18819470 100755 --- a/src/scss/components/_header.scss +++ b/src/scss/components/_header.scss @@ -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; + } } } } \ No newline at end of file diff --git a/src/scss/components/_modal.scss b/src/scss/components/_modal.scss index b3f12105..b1442bed 100755 --- a/src/scss/components/_modal.scss +++ b/src/scss/components/_modal.scss @@ -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"]{ diff --git a/src/scss/global/_core.scss b/src/scss/global/_core.scss index 1e77af95..b13168a6 100755 --- a/src/scss/global/_core.scss +++ b/src/scss/global/_core.scss @@ -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; diff --git a/src/scss/global/_forms.scss b/src/scss/global/_forms.scss index df7ab65b..d2f9ce22 100755 --- a/src/scss/global/_forms.scss +++ b/src/scss/global/_forms.scss @@ -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; }