diff --git a/src/js/bootstrap.js b/src/js/bootstrap.js
index e4a1bb01..924e6aa4 100755
--- a/src/js/bootstrap.js
+++ b/src/js/bootstrap.js
@@ -3,6 +3,7 @@ console.info('Bootstrapping...');
import { createStore, applyMiddleware, combineReducers } from 'redux'
+import ui from './services/ui/reducer'
import mopidy from './services/mopidy/reducer'
import spotify from './services/spotify/reducer'
@@ -12,6 +13,7 @@ import spotifyMiddleware from './services/spotify/middleware'
import localstorageMiddleware from './services/localstorage/middleware'
let reducers = combineReducers({
+ ui,
mopidy,
spotify
});
@@ -28,6 +30,11 @@ var initialState = {
country: 'NZ',
locale: 'en_NZ',
me: false
+ },
+ ui: {
+ context_menu: {
+ show: false
+ }
}
};
diff --git a/src/js/components/ContextMenu.js b/src/js/components/ContextMenu.js
new file mode 100755
index 00000000..b5c9b5cd
--- /dev/null
+++ b/src/js/components/ContextMenu.js
@@ -0,0 +1,142 @@
+
+import React, { PropTypes } from 'react'
+import { connect } from 'react-redux'
+import { bindActionCreators } from 'redux'
+
+import TrackList from './TrackList'
+import * as uiActions from '../services/ui/actions'
+import * as mopidyActions from '../services/mopidy/actions'
+import * as spotifyActions from '../services/spotify/actions'
+
+class ContextMenu extends React.Component{
+
+ constructor(props) {
+ super(props);
+ }
+
+ handleClick(e){
+ }
+
+ renderItems(){
+ var items = [];
+
+ switch( this.props.state.context ){
+
+ case 'queue':
+ items = [
+ { handleClick: 'playQueueItem', label: 'Play' },
+ { handleClick: 'removeFromQueue', label: 'Remove' }
+ ];
+ break;
+
+ case 'editable-playlist':
+ items = [
+ { handleClick: 'playItems', label: 'Play' },
+ { handleClick: 'removeFromPlaylist', label: 'Remove' }
+ ];
+ break;
+
+ default:
+ items = [
+ { handleClick: 'playItems', label: 'Play' },
+ { handleClick: 'playItemsNext', label: 'Play next' },
+ { handleClick: 'addToQueue', label: 'Add to queue' }
+ ];
+ break;
+ }
+
+ return (
+
+ );
+ }
+
+ playQueueItem(){
+ var selectedTracks = this.props.state.data.selected_tracks;
+ this.props.mopidyActions.changeTrack( selectedTracks[0].tlid );
+ this.props.uiActions.hideContextMenu();
+ }
+
+ removeFromQueue(){
+ var selectedTracks = this.props.state.data.selected_tracks;
+ var selectedTracksTlids = [];
+ for( var i = 0; i < selectedTracks.length; i++ ){
+ selectedTracksTlids.push( selectedTracks[i].tlid );
+ }
+ this.props.mopidyActions.removeTracks( selectedTracksTlids );
+ this.props.uiActions.hideContextMenu();
+ }
+
+ playItems(){
+ var selectedTracks = this.props.state.data.selected_tracks;
+ var selectedTracksUris = [];
+ for( var i = 0; i < selectedTracks.length; i++ ){
+ selectedTracksUris.push( selectedTracks[i].uri );
+ }
+ this.props.mopidyActions.playTracks(selectedTracksUris);
+ this.props.uiActions.hideContextMenu();
+ }
+
+ playItemsNext(){
+ var selectedTracks = this.props.state.data.selected_tracks;
+ var selectedTracksUris = [];
+ for( var i = 0; i < selectedTracks.length; i++ ){
+ selectedTracksUris.push( selectedTracks[i].uri );
+ }
+
+ // TODO: figure out what index the currentTlTrack is (.indexOf()?)
+ var currentTrackIndex = 1;
+
+ this.props.mopidyActions.enqueueTracks(selectedTracksUris, currentTrackIndex);
+ this.props.uiActions.hideContextMenu();
+ }
+
+ addToQueue(){
+ var selectedTracks = this.props.state.data.selected_tracks;
+ var selectedTracksUris = [];
+ for( var i = 0; i < selectedTracks.length; i++ ){
+ selectedTracksUris.push( selectedTracks[i].uri );
+ }
+ this.props.mopidyActions.enqueueTracks(selectedTracksUris);
+ this.props.uiActions.hideContextMenu();
+ }
+
+ removeFromPlaylist(){
+ console.log('removeFromPlaylist')
+ this.props.uiActions.hideContextMenu();
+ }
+
+ render(){
+ if( !this.props.state.show ) return null;
+
+ var style = {
+ left: this.props.state.position_x,
+ top: this.props.state.position_y,
+ }
+
+ return (
+
+ { this.renderItems() }
+
+ );
+ }
+}
+
+const mapStateToProps = (state, ownProps) => {
+ return state;
+}
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ uiActions: bindActionCreators(uiActions, dispatch),
+ spotifyActions: bindActionCreators(spotifyActions, dispatch),
+ mopidyActions: bindActionCreators(mopidyActions, dispatch)
+ }
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(ContextMenu)
\ No newline at end of file
diff --git a/src/js/components/Sidebar.js b/src/js/components/Sidebar.js
index 45f3038c..41d26877 100755
--- a/src/js/components/Sidebar.js
+++ b/src/js/components/Sidebar.js
@@ -50,6 +50,10 @@ class Sidebar extends React.Component{
Featured playlists
+
+
+ New releases
+
diff --git a/src/js/components/Track.js b/src/js/components/Track.js
index 5166058a..c2dc76a1 100755
--- a/src/js/components/Track.js
+++ b/src/js/components/Track.js
@@ -10,17 +10,27 @@ export default class Track extends React.Component{
super(props);
}
- handleClick( e ){
+ handleClick(e){
var target = $(e.target);
if( !target.is('a') && target.closest('a').length <= 0 ){
this.props.handleClick(e);
}
}
- handleDoubleClick( e ){
+ handleDoubleClick(e){
return this.props.handleDoubleClick(e);
}
+ handleContextMenu(e){
+ e.preventDefault();
+
+ // trigger a regular click event
+ if( !this.props.track.selected ) this.handleClick(e);
+
+ // notify our tracklist
+ this.props.handleContextMenu(e);
+ }
+
formatDuration(){
if( typeof(this.props.track.duration_ms) !== 'undefined' ){
var ms = this.props.track.duration_ms;
@@ -52,7 +62,8 @@ export default class Track extends React.Component{
this.handleDoubleClick(e) }
- onClick={ (e) => this.handleClick(e) }>
+ onClick={ (e) => this.handleClick(e) }
+ onContextMenu={ (e) => this.handleContextMenu(e) }>
{ this.props.track.selected ?
: null }
{this.props.track.name}
diff --git a/src/js/components/TrackList.js b/src/js/components/TrackList.js
index 675a4e5f..4c90ae45 100755
--- a/src/js/components/TrackList.js
+++ b/src/js/components/TrackList.js
@@ -3,6 +3,7 @@ import React, { PropTypes } from 'react'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import * as mopidyActions from '../services/mopidy/actions'
+import * as uiActions from '../services/ui/actions'
import Track from './Track'
@@ -22,6 +23,8 @@ class TrackList extends React.Component{
}
handleClick( e, index ){
+ if( this.props.ui.context_menu.show ) this.props.uiActions.hideContextMenu();
+
var tracks = this.state.tracks;
if( e.ctrlKey ){
@@ -54,10 +57,19 @@ class TrackList extends React.Component{
}
handleDoubleClick( e, index ){
+ if( this.props.ui.context_menu.show ) this.props.uiActions.hideContextMenu();
+
var tracks = this.state.tracks;
this.playTrack( tracks[index] )
}
+ handleContextMenu( e, index ){
+ var data = {
+ selected_tracks: this.selectedTracks()
+ }
+ this.props.uiActions.showContextMenu( e, this.props.context, data )
+ }
+
selectedTracks(){
function isSelected( track ){
return ( typeof(track.selected) !== 'undefined' && track.selected );
@@ -103,46 +115,44 @@ class TrackList extends React.Component{
let self = this;
if( this.state.tracks ){
return (
-
-
- -
- Name
- Artists
- Album
- Duration
-
- {
- this.state.tracks.map(
- (track, index) => {
+
+ -
+ Name
+ Artists
+ Album
+ Duration
+
+ {
+ this.state.tracks.map(
+ (track, index) => {
- // flatten nested track objects (as in the case of TlTracks)
- if( typeof(track.track) !== 'undefined' ){
+ // flatten nested track objects (as in the case of TlTracks)
+ if( typeof(track.track) !== 'undefined' ){
- // see if we're the current tlTrack
- if( self.props.mopidy.trackInFocus && self.props.mopidy.trackInFocus.tlid == track.tlid ){
- track.playing = true;
- }else{
- track.playing = false;
- }
+ // see if we're the current tlTrack
+ // TODO: figure out why this isn't fired when the tracklist changes
+ if( self.props.mopidy.trackInFocus && self.props.mopidy.trackInFocus.tlid == track.tlid ){
+ track.playing = true;
+ }else{
+ track.playing = false;
+ }
- for( var property in track.track ){
- if( track.track.hasOwnProperty(property) ){
- track[property] = track.track[property]
- }
+ for( var property in track.track ){
+ if( track.track.hasOwnProperty(property) ){
+ track[property] = track.track[property]
}
}
- return
-
-
-
+ return
);
}
@@ -48,6 +52,7 @@ const mapStateToProps = (state, ownProps) => {
const mapDispatchToProps = (dispatch) => {
return {
+ uiActions: bindActionCreators(uiActions, dispatch),
mopidyActions: bindActionCreators(mopidyActions, dispatch),
spotifyActions: bindActionCreators(spotifyActions, dispatch)
}
diff --git a/src/js/views/Playlist.js b/src/js/views/Playlist.js
index f9b22349..5c14fab8 100755
--- a/src/js/views/Playlist.js
+++ b/src/js/views/Playlist.js
@@ -29,10 +29,14 @@ class Playlist extends React.Component{
render(){
if( this.props.spotify.playlist ){
+
+ var context = null;
+ if( this.props.spotify.playlist.owner.id == this.props.spotify.me.id ) context = 'editable-playlist'
+
return (
-
+
);
}
diff --git a/src/js/views/Queue.js b/src/js/views/Queue.js
index 3040cfaf..ceea9cce 100755
--- a/src/js/views/Queue.js
+++ b/src/js/views/Queue.js
@@ -36,7 +36,7 @@ class Queue extends React.Component{
if( this.props.mopidy && this.props.mopidy.tracks ){
return (
this.removeTracks( tracks ) }
playTracks={ null }
diff --git a/src/js/views/discover/DiscoverNewReleases.js b/src/js/views/discover/DiscoverNewReleases.js
new file mode 100755
index 00000000..8812bc94
--- /dev/null
+++ b/src/js/views/discover/DiscoverNewReleases.js
@@ -0,0 +1,49 @@
+
+import React, { PropTypes } from 'react'
+import { connect } from 'react-redux'
+import { bindActionCreators } from 'redux'
+
+import Header from '../../components/Header'
+import AlbumGrid from '../../components/AlbumGrid'
+
+import * as spotifyActions from '../../services/spotify/actions'
+
+class DiscoverNewReleases extends React.Component{
+
+ constructor(props) {
+ super(props);
+ }
+
+ // on render
+ componentDidMount(){
+ this.props.spotifyActions.getNewReleases();
+ }
+
+ render(){
+ return (
+
+
+ { this.props.spotify.new_releases ?
: null }
+
+ );
+ }
+}
+
+
+/**
+ * Export our component
+ *
+ * We also integrate our global store, using connect()
+ **/
+
+const mapStateToProps = (state, ownProps) => {
+ return state;
+}
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ spotifyActions: bindActionCreators(spotifyActions, dispatch)
+ }
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(DiscoverNewReleases)
\ No newline at end of file
diff --git a/src/scss/app.scss b/src/scss/app.scss
index 60e96ee9..096c731c 100755
--- a/src/scss/app.scss
+++ b/src/scss/app.scss
@@ -7,6 +7,7 @@
@import 'global/reset';
@import 'global/forms';
+@import 'components/context-menu';
@import 'components/images';
@import 'components/icon';
@import 'components/sidebar';
diff --git a/src/scss/components/_context-menu.scss b/src/scss/components/_context-menu.scss
new file mode 100755
index 00000000..8a378c4f
--- /dev/null
+++ b/src/scss/components/_context-menu.scss
@@ -0,0 +1,24 @@
+
+.context-menu {
+ position: fixed;
+ left: 0;
+ top: 0;
+ z-index: 9999;
+ background: $dark_grey;
+ color: #FFFFFF;
+
+ a {
+ display: block;
+ padding: 8px 12px;
+ width: 120px;
+
+ &:not(:first-child){
+ border-top: 1px solid lighten($dark_grey, 8%);
+ }
+
+ &:hover {
+ cursor: pointer;
+ background: lighten($dark_grey, 5%);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/scss/components/_grid.scss b/src/scss/components/_grid.scss
index 100cc054..9ba1c335 100755
--- a/src/scss/components/_grid.scss
+++ b/src/scss/components/_grid.scss
@@ -11,6 +11,7 @@
border-bottom: 0;
.thumbnail {
+ @include animate();
max-width: 100%;
}
@@ -31,7 +32,10 @@
&:hover{
cursor: pointer;
- opacity: 0.75;
+
+ .thumbnail {
+ opacity: 0.75;
+ }
}
}
diff --git a/src/scss/components/_sidebar.scss b/src/scss/components/_sidebar.scss
index 0d27fbf2..2105c2b1 100755
--- a/src/scss/components/_sidebar.scss
+++ b/src/scss/components/_sidebar.scss
@@ -12,10 +12,10 @@ aside{
.thumbnail {
position: absolute;
- top: 0;
- right: 0;
- bottom: 0;
- left: 0;
+ top: -30px;
+ right: -30px;
+ bottom: -30px;
+ left: -30px;
height: auto;
width: auto;
max-width: none;
@@ -23,6 +23,7 @@ aside{
opacity: 0.2;
.image {
+ @include blur();
padding-bottom: 0;
height: 100%;
}
diff --git a/src/scss/global/_variables.scss b/src/scss/global/_variables.scss
index f9a75519..796332e9 100755
--- a/src/scss/global/_variables.scss
+++ b/src/scss/global/_variables.scss
@@ -25,4 +25,9 @@ $red: #cf2d2d;
-moz-transition: all $speed ease-in-out;
-o-transition: all $speed ease-in-out;
transition: all $speed ease-in-out;
+}
+
+@mixin blur( $size: 10px ) {
+ -webkit-filter: blur( $size );
+ filter: blur( $size );
}
\ No newline at end of file