- {this.props.context_menu.data.item ? this.renderTitle() : null}
- {this.renderItems(trigger)}
+
+ {this.props.menu.item ? this.renderTitle() : null}
+ {this.renderItems()}
);
}
@@ -287,11 +272,9 @@ class ContextMenu extends React.Component{
const mapStateToProps = (state, ownProps) => {
return {
- emulate_touch: state.ui.emulate_touch,
- context_menu: state.ui.context_menu,
+ menu: state.ui.context_menu,
current_track: state.ui.current_track,
current_tracklist: state.ui.current_tracklist,
- playlist: state.ui.playlist,
playlists: state.ui.playlists
}
}
diff --git a/src/js/components/DebugInfo.js b/src/js/components/DebugInfo.js
index 4d808866..97473e3c 100755
--- a/src/js/components/DebugInfo.js
+++ b/src/js/components/DebugInfo.js
@@ -4,6 +4,7 @@ import { connect } from 'react-redux'
import { Link } from 'react-router'
import { createStore, bindActionCreators } from 'redux'
+import * as helpers from '../helpers'
import * as uiActions from '../services/ui/actions'
class DebugInfo extends React.Component{
@@ -13,6 +14,13 @@ class DebugInfo extends React.Component{
}
render(){
+ var touch_state = 'no touch'
+ if (this.props.ui.emulate_touch){
+ touch_state = 'emulated touch'
+ } else if (helpers.isTouchDevice()){
+ touch_state = 'touch device'
+ }
+
return (
@@ -28,7 +36,7 @@ class DebugInfo extends React.Component{
{this.props.ui.users ? Object.keys(this.props.ui.users).length : '0'} users
- {'ontouchstart' in document.documentElement ? 'can touch' : 'no touch'}
+ {touch_state}
);
diff --git a/src/js/components/TouchContextMenu.js b/src/js/components/TouchContextMenu.js
new file mode 100755
index 00000000..5265b09a
--- /dev/null
+++ b/src/js/components/TouchContextMenu.js
@@ -0,0 +1,164 @@
+
+import React, { PropTypes } from 'react'
+import { Link } from 'react-router'
+import { connect } from 'react-redux'
+import { bindActionCreators } from 'redux'
+import FontAwesome from 'react-fontawesome'
+
+import TrackList from './TrackList'
+import * as helpers from '../helpers'
+import * as uiActions from '../services/ui/actions'
+import * as pusherActions from '../services/pusher/actions'
+import * as mopidyActions from '../services/mopidy/actions'
+import * as spotifyActions from '../services/spotify/actions'
+
+class TouchContextMenu extends React.Component{
+
+ constructor(props) {
+ super(props);
+ }
+
+ close(){
+ this.props.uiActions.hideTouchContextMenu();
+ }
+
+ playQueueItem(){
+ var tracks = this.props.menu.items
+ this.props.mopidyActions.changeTrack( tracks[0].tlid )
+ this.close()
+ }
+
+ removeFromQueue(e){
+ var tracks = this.props.menu.items
+ var tracks_tlids = []
+ for( var i = 0; i < tracks.length; i++ ){
+ tracks_tlids.push( tracks[i].tlid );
+ }
+ this.props.mopidyActions.removeTracks( tracks_tlids )
+ this.close()
+ }
+
+ playURIs(e){
+ this.props.mopidyActions.playURIs(this.props.menu.uris)
+ this.close()
+ }
+
+ playURIsNext(e){
+ this.props.mopidyActions.enqueueURIsNext(this.props.menu.uris)
+ this.close()
+ }
+
+ addToPlaylist(e){
+ this.props.uiActions.openModal('add_to_playlist', { tracks_uris: this.props.menu.uris })
+ this.close()
+ }
+
+ addToQueue(e){
+ this.props.mopidyActions.enqueueURIs(this.props.menu.uris)
+ this.close()
+ }
+
+ removeFromPlaylist(e){
+ this.props.uiActions.removeTracksFromPlaylist(this.props.menu.tracklist_uri, this.props.menu.indexes)
+ this.close()
+ }
+
+ startRadio(e){
+ this.props.pusherActions.startRadio(this.props.menu.uris)
+ this.close()
+ }
+
+ getItems(){
+ switch (this.props.menu.context) {
+
+ case 'queue-track':
+ var items = [
+ { handleClick: 'playQueueItem', label: 'Play', icon: 'play' },
+ { handleClick: 'addToPlaylist', label: 'Playlist', icon: 'plus' },
+ { handleClick: 'removeFromQueue', label: 'Remove', icon: 'trash' }
+ ]
+ break
+
+ case 'editable-playlist-track':
+ var items = [
+ { handleClick: 'playURIs', label: 'Play', icon: 'play' },
+ { handleClick: 'playURIsNext', label: 'Next', icon: 'play' },
+ { handleClick: 'addToQueue', label: 'Queue', icon: 'plus' },
+ { handleClick: 'addToPlaylist', label: 'Playlist', icon: 'plus' },
+ { handleClick: 'startRadio', label: 'Start radio', icon: 'spotify' },
+ { handleClick: 'removeFromPlaylist', label: 'Remove', icon: 'trash' }
+ ]
+ break
+
+ default:
+ var items = [
+ { handleClick: 'playURIs', label: 'Play', icon: 'play' },
+ { handleClick: 'playURIsNext', label: 'Next', icon: 'play' },
+ { handleClick: 'addToQueue', label: 'Queue', icon: 'plus' },
+ { handleClick: 'addToPlaylist', label: 'Playlist', icon: 'plus' },
+ { handleClick: 'startRadio', label: 'Start radio', icon: 'spotify' }
+ ]
+ break
+ }
+
+ return items
+ }
+
+ renderItems(){
+ var items = this.getItems()
+
+ return (
+
+ )
+ }
+
+ render(){
+ if( !this.props.menu ) return null;
+
+ return (
+
+ {this.renderItems()}
+
+ );
+ }
+}
+
+const mapStateToProps = (state, ownProps) => {
+ return {
+ menu: state.ui.touch_context_menu,
+ current_track: state.ui.current_track,
+ current_tracklist: state.ui.current_tracklist,
+ playlist: state.ui.playlist,
+ playlists: state.ui.playlists
+ }
+}
+
+const mapDispatchToProps = (dispatch) => {
+ return {
+ uiActions: bindActionCreators(uiActions, dispatch),
+ pusherActions: bindActionCreators(pusherActions, dispatch),
+ spotifyActions: bindActionCreators(spotifyActions, dispatch),
+ mopidyActions: bindActionCreators(mopidyActions, dispatch)
+ }
+}
+
+export default connect(mapStateToProps, mapDispatchToProps)(TouchContextMenu)
\ No newline at end of file
diff --git a/src/js/components/Track.js b/src/js/components/Track.js
index 0d7cfc11..4020739d 100755
--- a/src/js/components/Track.js
+++ b/src/js/components/Track.js
@@ -20,6 +20,13 @@ export default class Track extends React.Component{
}
}
+ handleTouchStart(e){
+ var target = $(e.target);
+ if( !target.is('a') && target.closest('a').length <= 0 ){
+ this.props.handleTouchStart(e);
+ }
+ }
+
handleTouchEnd(e){
var target = $(e.target);
if( !target.is('a') && target.closest('a').length <= 0 ){
diff --git a/src/js/components/TrackList.js b/src/js/components/TrackList.js
index b43da2e5..8200f531 100755
--- a/src/js/components/TrackList.js
+++ b/src/js/components/TrackList.js
@@ -34,7 +34,7 @@ class TrackList extends React.Component{
window.removeEventListener("keyup", this.handleKeyUp, false);
}
- componentWillReceiveProps( nextProps ){
+ componentWillReceiveProps(nextProps){
this.setState({ tracks: this.keyifyTracks(nextProps.tracks) });
}
@@ -53,12 +53,12 @@ class TrackList extends React.Component{
}
}
- handleTouchStart(e, index){
+ handleTouchStart(e,index){
this._touch_x = Math.round(e.changedTouches[0].pageX)
this._touch_y = Math.round(e.changedTouches[0].pageY)
}
- handleTouchEnd(e, index){
+ handleTouchEnd(e,index){
var pageX = Math.round(e.changedTouches[0].pageX)
var pageY = Math.round(e.changedTouches[0].pageY)
@@ -69,45 +69,30 @@ class TrackList extends React.Component{
this._touch_x > ( pageX - this._touch_threshold ) &&
this._touch_y < ( pageY + this._touch_threshold ) &&
this._touch_y > ( pageY - this._touch_threshold ) ){
-
- // toggle selection
- var tracks = this.state.tracks
- tracks[index].selected = !tracks[index].selected
- this.setState({ tracks: tracks, lastSelectedTrack: index })
-
- // update our context menu to hide/show
- var selected_tracks = this.selectedTracks()
- if( selected_tracks.length > 0 ){
- var data = {
- items: selected_tracks,
- uris: helpers.asURIs(selected_tracks),
- indexes: this.tracksIndexes(selected_tracks)
- }
- this.props.uiActions.showContextMenu( e, data, this.props.context, 'touch' )
- }else{
- this.props.uiActions.hideContextMenu()
- }
+ this.handleTouchContextMenu(e,index)
}
e.preventDefault()
}
- handleDoubleClick(e, index){
- if( this.props.context_menu.show ) this.props.uiActions.hideContextMenu()
+ handleDoubleClick(e,index){
+ if (this.props.context_menu) this.props.uiActions.hideContextMenu()
this.playTracks()
}
- handleMouseDown(e, index){
+ handleMouseDown(e,index){
+ if (this.props.emulate_touch){
+ this.handleTouchContextMenu(e,index)
+ }else{
+ if (this.props.context_menu) this.props.uiActions.hideContextMenu()
+ if (!this.state.tracks[index].selected && !this.isRightClick(e) && !e.ctrlKey) this.toggleTrackSelections(e, index)
- if( this.props.context_menu.show ) this.props.uiActions.hideContextMenu()
-
- if( !this.state.tracks[index].selected && !this.isRightClick(e) && !e.ctrlKey ) this.toggleTrackSelections(e, index)
-
- var selected_tracks = this.selectedTracks()
- this.props.uiActions.dragStart( e, this.props.context, selected_tracks, this.tracksIndexes(selected_tracks) )
+ var selected_tracks = this.selectedTracks()
+ this.props.uiActions.dragStart( e, this.props.context, selected_tracks, this.tracksIndexes(selected_tracks) )
+ }
}
- handleMouseUp(e, index){
+ handleMouseUp(e,index){
// right-clicking on an un-highlighted track
if( !this.state.tracks[index].selected && this.isRightClick(e) ){
@@ -132,14 +117,40 @@ class TrackList extends React.Component{
}
}
- handleContextMenu(e, index){
+ handleContextMenu(e,index){
var selected_tracks = this.selectedTracks()
var data = {
+ e: e,
+ context: (this.props.context ? this.props.context : 'track'),
+ tracklist_uri: (this.props.tracklist_uri ? this.props.tracklist_uri : null),
items: selected_tracks,
uris: helpers.asURIs(selected_tracks),
indexes: this.tracksIndexes(selected_tracks)
}
- this.props.uiActions.showContextMenu( e, data, this.props.context, 'click' )
+ this.props.uiActions.showContextMenu(data)
+ }
+
+ handleTouchContextMenu(e,index){
+
+ // toggle selection
+ var tracks = this.state.tracks
+ tracks[index].selected = !tracks[index].selected
+ this.setState({ tracks: tracks, lastSelectedTrack: index })
+
+ // update our context menu to hide/show
+ var selected_tracks = this.selectedTracks()
+ if( selected_tracks.length > 0 ){
+ var data = {
+ context: (this.props.context ? this.props.context : 'track'),
+ tracklist_uri: (this.props.tracklist_uri ? this.props.tracklist_uri : null),
+ items: selected_tracks,
+ uris: helpers.asURIs(selected_tracks),
+ indexes: this.tracksIndexes(selected_tracks)
+ }
+ this.props.uiActions.showTouchContextMenu(data)
+ }else{
+ this.props.uiActions.hideTouchContextMenu()
+ }
}
toggleTrackSelections(e, index){
@@ -285,6 +296,7 @@ class TrackList extends React.Component{
const mapStateToProps = (state, ownProps) => {
return {
dragger: state.ui.dragger,
+ emulate_touch: state.ui.emulate_touch,
current_track: state.ui.current_track,
context_menu: state.ui.context_menu
}
diff --git a/src/js/helpers.js b/src/js/helpers.js
index 64bd088f..38ac6524 100755
--- a/src/js/helpers.js
+++ b/src/js/helpers.js
@@ -1,5 +1,9 @@
+export let isTouchDevice = function(){
+ return 'ontouchstart' in document.documentElement
+}
+
export let sizedImages = function( images ){
var sizes = {
diff --git a/src/js/services/ui/actions.js b/src/js/services/ui/actions.js
index f4c51895..bf219c0f 100755
--- a/src/js/services/ui/actions.js
+++ b/src/js/services/ui/actions.js
@@ -1,13 +1,11 @@
import * as helpers from '../../helpers'
-export function showContextMenu( e, data, context = false, trigger = 'click' ){
+export function showContextMenu(data){
+ data.position_x = data.e.clientX
+ data.position_y = data.e.clientY
return {
type: 'SHOW_CONTEXT_MENU',
- position_x: e.clientX,
- position_y: e.clientY,
- context: context,
- trigger: trigger,
data: data
}
}
@@ -18,6 +16,19 @@ export function hideContextMenu(){
}
}
+export function showTouchContextMenu(data){
+ return {
+ type: 'SHOW_TOUCH_CONTEXT_MENU',
+ data: data
+ }
+}
+
+export function hideTouchContextMenu(){
+ return {
+ type: 'HIDE_TOUCH_CONTEXT_MENU'
+ }
+}
+
export function startSearch( query ){
return {
type: 'SEARCH_STARTED',
diff --git a/src/js/services/ui/reducer.js b/src/js/services/ui/reducer.js
index e42aed1b..52640abd 100755
--- a/src/js/services/ui/reducer.js
+++ b/src/js/services/ui/reducer.js
@@ -22,20 +22,22 @@ export default function reducer(ui = {}, action){
/**
* Context menu
**/
+
case 'SHOW_CONTEXT_MENU':
return Object.assign({}, ui, {
- context_menu: {
- show: true,
- position_x: action.position_x,
- position_y: action.position_y,
- context: action.context,
- trigger: action.trigger,
- data: action.data
- }
+ context_menu: action.data
});
case 'HIDE_CONTEXT_MENU':
- return Object.assign({}, ui, { context_menu: { show: false } });
+ return Object.assign({}, ui, {context_menu: null});
+
+ case 'SHOW_TOUCH_CONTEXT_MENU':
+ return Object.assign({}, ui, {
+ touch_context_menu: action.data
+ });
+
+ case 'HIDE_TOUCH_CONTEXT_MENU':
+ return Object.assign({}, ui, {touch_context_menu: null});
diff --git a/src/js/views/App.js b/src/js/views/App.js
index 76605906..44f9b279 100755
--- a/src/js/views/App.js
+++ b/src/js/views/App.js
@@ -9,11 +9,13 @@ import Sidebar from '../components/Sidebar'
import MiniPlayer from '../components/MiniPlayer'
import SidebarToggleButton from '../components/SidebarToggleButton'
import ContextMenu from '../components/ContextMenu'
+import TouchContextMenu from '../components/TouchContextMenu'
import Dragger from '../components/Dragger'
import Modal from '../components/Modal/Modal'
import Notifications from '../components/Notifications'
import DebugInfo from '../components/DebugInfo'
+import * as helpers from '../helpers'
import * as uiActions from '../services/ui/actions'
import * as pusherActions from '../services/pusher/actions'
import * as mopidyActions from '../services/mopidy/actions'
@@ -117,10 +119,10 @@ class App extends React.Component{
render(){
var className = '';
- if( this.props.dragger && this.props.dragger.active ) className += ' dragging'
- if( this.props.sidebar_open ) className += ' sidebar-open'
- if( this.props.modal ) className += ' modal-open'
- if( 'ontouchstart' in document.documentElement ) className += ' has-touch-events'
+ if (this.props.dragger && this.props.dragger.active) className += ' dragging'
+ if (this.props.sidebar_open) className += ' sidebar-open'
+ if (this.props.modal) className += ' modal-open'
+ if (helpers.isTouchDevice() || this.props.emulate_touch) className += ' can-touch'
return (
@@ -129,7 +131,8 @@ class App extends React.Component{
{this.props.children}
-
+
+
diff --git a/src/js/views/Artist.js b/src/js/views/Artist.js
index 6c2ab45f..259276d1 100755
--- a/src/js/views/Artist.js
+++ b/src/js/views/Artist.js
@@ -151,7 +151,7 @@ class Artist extends React.Component{
0 ? "col w70" : "col w100"}>
Top tracks
- { this.props.artist.tracks ? : null }
+ { this.props.artist.tracks ? : null }
diff --git a/src/js/views/Playlist.js b/src/js/views/Playlist.js
index 91228e00..174528b2 100755
--- a/src/js/views/Playlist.js
+++ b/src/js/views/Playlist.js
@@ -115,8 +115,11 @@ class Playlist extends React.Component{
}
render(){
- if( !this.props.playlist || !this.props.playlist.name ) return null;
- var scheme = helpers.uriSource( this.props.playlist.uri );
+ if( !this.props.playlist || !this.props.playlist.name ) return null
+ var scheme = helpers.uriSource( this.props.playlist.uri )
+
+ var context = 'playlist-track'
+ if (this.props.playlist.can_edit) context = 'editable-playlist-track'
return (
@@ -153,7 +156,7 @@ class Playlist extends React.Component{
- { this.props.playlist.tracks ? this.removeTracks(tracks_indexes) } reorderTracks={ (indexes, index) => this.reorderTracks(indexes, index) } /> : null }
+ { this.props.playlist.tracks ? this.removeTracks(tracks_indexes) } reorderTracks={ (indexes, index) => this.reorderTracks(indexes, index) } /> : null }
this.loadMore() }/>
diff --git a/src/js/views/Queue.js b/src/js/views/Queue.js
index 7c7197d5..e454c259 100755
--- a/src/js/views/Queue.js
+++ b/src/js/views/Queue.js
@@ -98,7 +98,7 @@ class Queue extends React.Component{
this.removeTracks( tracks ) }
playTracks={ tracks => this.playTracks( tracks ) }
diff --git a/src/scss/components/_context-menu.scss b/src/scss/components/_context-menu.scss
index c3bed4a3..1b634625 100755
--- a/src/scss/components/_context-menu.scss
+++ b/src/scss/components/_context-menu.scss
@@ -99,11 +99,6 @@
}
}
- &.cancel {
- float: right;
- opacity: 0.5;
- }
-
.menu-item {
border-top: 1px solid lighten($dark_grey, 8%);
}
@@ -117,57 +112,61 @@
}
}
}
+}
- &.touch{
-
- bottom: 0 !important;
- left: 0 !important;
- right: 0 !important;
- top: auto !important;
- background: $blue;
+.touch-context-menu {
+ position: fixed;
+ z-index: 99;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ top: auto;
+ background: $blue;
+ color: #FFFFFF;
- .menu-item-wrapper {
+ .menu-item-wrapper {
+ display: inline-block;
+
+ .menu-item {
display: inline-block;
+ text-align: center;
+ width: 50px;
+ height: 44px;
+ padding-top: 6px;
+ border: 0;
+ vertical-align: top;
- .menu-item {
- display: inline-block;
- text-align: center;
- width: 56px;
- height: 56px;
- border: 0 !important;
- vertical-align: top;
-
- &:hover {
- background: inherit;
- }
-
- &:active,
- &:focus {
- background: rgba(255,255,255,0.2);
- }
-
- .icon {
- padding: 5px;
- display: inline-block;
- }
-
- .label {
- display: block;
- font-size: 10px;
- }
-
- .submenu-icon {
- display: none !important;
- }
+ &:hover {
+ background: inherit;
}
- .submenu {
+ &:active,
+ &:focus {
+ background: rgba(255,255,255,0.2);
+ }
+
+ .icon {
+ padding: 5px;
+ display: inline-block;
+ }
+
+ .label {
+ display: block;
+ font-size: 10px;
+ }
+
+ .submenu-icon {
display: none !important;
}
+ }
- &:not(:first-child) .menu-item {
- border-left: 1px solid rgba(255,255,255,0.2) !important;
- }
+ &.cancel {
+ float: right;
+ opacity: 0.5;
+ }
+
+ &:not(:first-child) .menu-item {
+ border-left: 1px solid rgba(255,255,255,0.2) !important;
}
}
}
\ No newline at end of file