diff --git a/package.json b/package.json index 8204e836..d0d99530 100755 --- a/package.json +++ b/package.json @@ -10,39 +10,34 @@ }, "main": "app.js", "dependencies": { - "webpack": "2.2.1", - "webpack-strip": "*", - "sass-loader": "6.0.2", - "style-loader": "0.13.2", - "css-loader": "0.26.2", - "url-loader": "0.5.8", - "file-loader": "0.10.1", - "expose-loader": "0.7.3", - - "node-sass": "4.5.0", - "extract-text-webpack-plugin": "2.0.0", - "babel-core": "^6.23.1", - "babel-loader": "^6.3.0", + "babel-loader": "^7.1.1", "babel-preset-es2015": "^6.22.0", "babel-preset-react": "^6.22.0", "babel-preset-stage-0": "^6.22.0", "babel-preset-stage-2": "^6.22.0", - - "flux": "^2.1.1", - "redux": "*", - "react": "*", - "react-dom": "*", - "react-router": "3.0.2", - "redux": "*", - "react-redux": "*", - "redux-devtools": "*", - "redux-thunk": "*", - + "css-loader": "0.28.4", + "expose-loader": "0.7.3", + "extract-text-webpack-plugin": "3.0.0", + "file-loader": "^0.11.2", + "flux": "^3.1.2", "jquery": "3.1.1", - "mopidy": "^0.5.0", + "mopidy": "^0.5.0", + "node-sass": "4.5.0", + "react": "15.6.1", + "react-dom": "15.6.1", "react-fontawesome": "*", - "react-ga": "2.1.2" + "react-ga": "2.1.2", + "react-redux": "5.0.5", + "react-router": "^3.0.5", + "redux": "3.7.2", + "redux-devtools": "3.4.0", + "redux-thunk": "2.2.0", + "sass-loader": "6.0.2", + "style-loader": "0.13.2", + "url-loader": "0.5.8", + "webpack": "3.3.0", + "webpack-strip": "0.1.0" }, "scripts": { "dev": "./build.sh && NODE_ENV=development webpack --watch", diff --git a/src/js/App.js b/src/js/App.js index 5c769576..f661585e 100755 --- a/src/js/App.js +++ b/src/js/App.js @@ -30,11 +30,19 @@ class App extends React.Component{ super(props); this.handleKeyUp = this.handleKeyUp.bind(this); this.handleKeyDown = this.handleKeyDown.bind(this); + this.handleWindowResize = this.handleWindowResize.bind(this); } componentWillMount(){ window.addEventListener("keyup", this.handleKeyUp, false); window.addEventListener("keydown", this.handleKeyDown, false); + window.addEventListener("resize", this.handleWindowResize, false); + } + + componentWillUnmount(){ + window.removeEventListener("keyup", this.handleKeyUp, false); + window.removeEventListener("keydown", this.handleKeyDown, false); + window.removeEventListener("resize", this.handleWindowResize, false); } componentDidMount(){ @@ -56,6 +64,9 @@ class App extends React.Component{ // hide our sidebar this.props.uiActions.toggleSidebar( false ) }); + + // Check our slim_mode + this.handleWindowResize(null) } componentWillReceiveProps(nextProps){ @@ -76,11 +87,6 @@ class App extends React.Component{ } } - componentWillUnmount(){ - window.removeEventListener("keyup", this.handleKeyUp, false); - window.removeEventListener("keydown", this.handleKeyDown, false); - } - shouldTriggerShortcut(e){ var ignoreNodes = ['INPUT', 'TEXTAREA'] var keyCodes = [32,27,191,37,38,39,40,70] @@ -95,6 +101,21 @@ class App extends React.Component{ } } + handleWindowResize(e){ + var width = Math.max(document.documentElement.clientWidth, window.innerWidth || 0) + var height = Math.max(document.documentElement.clientHeight, window.innerHeight || 0) + + if (width <= 800){ + if (!this.props.slim_mode){ + this.props.uiActions.setSlimMode(true) + } + } else { + if (this.props.slim_mode){ + this.props.uiActions.setSlimMode(false) + } + } + } + handleKeyDown(e){ this.shouldTriggerShortcut(e) } @@ -197,6 +218,8 @@ class App extends React.Component{ 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 (this.props.touch_dragging) className += ' touch-dragging' + if (this.props.slim_mode) className += ' slim-mode' if (helpers.isTouchDevice()){ className += ' touch' } else { @@ -236,6 +259,8 @@ class App extends React.Component{ const mapStateToProps = (state, ownProps) => { return { + touch_dragging: state.ui.touch_dragging, + slim_mode: state.ui.slim_mode, broadcasts: (state.ui.broadcasts ? state.ui.broadcasts : []), volume: (state.mopidy.volume ? state.mopidy.volume : false), notifications: (state.ui.notifications ? state.ui.notifications : []), diff --git a/src/js/bootstrap.js b/src/js/bootstrap.js index 8d749e94..2594daef 100755 --- a/src/js/bootstrap.js +++ b/src/js/bootstrap.js @@ -53,6 +53,7 @@ var initialState = { autocomplete_results: {} }, ui: { + slim_mode: false, current_tracklist: [], current_tltrack: false, notifications: [], diff --git a/src/js/components/ContextMenu.js b/src/js/components/ContextMenu.js index e351e4dd..94327e14 100755 --- a/src/js/components/ContextMenu.js +++ b/src/js/components/ContextMenu.js @@ -174,6 +174,11 @@ class ContextMenu extends React.Component{ this.props.mopidyActions.playPlaylist(this.props.menu.uris[0]) } + playArtistTopTracks(e){ + this.props.uiActions.hideContextMenu() + this.props.spotifyActions.playArtistTopTracks(this.props.menu.uris[0]) + } + addToQueue(e, next = false){ this.props.uiActions.hideContextMenu() this.props.mopidyActions.enqueueURIs(this.props.menu.uris, this.props.menu.tracklist_uri, next) @@ -361,6 +366,14 @@ class ContextMenu extends React.Component{ ) + var play_artist_top_tracks = ( + + this.playArtistTopTracks(e)}> + Play top tracks + + + ) + var add_to_queue = ( this.addToQueue(e)}> @@ -463,6 +476,7 @@ class ContextMenu extends React.Component{ case 'artist': return (
+ {context.source == 'spotify' ? play_artist_top_tracks : null} {start_radio} {context.source == 'spotify' ? go_to_recommendations : null} {copy_uris} diff --git a/src/js/components/DebugInfo.js b/src/js/components/DebugInfo.js index 1c2d8564..41ff9e96 100755 --- a/src/js/components/DebugInfo.js +++ b/src/js/components/DebugInfo.js @@ -81,6 +81,9 @@ class DebugInfo extends React.Component{ Enqueue batches: {this.props.mopidy.enqueue_uris_batches ? this.props.mopidy.enqueue_uris_batches.length : '0'}

+
+ Slim mode: {this.props.ui.slim_mode ? 'on' : 'off'} +
_testMode: {window._testMode ? 'on' : 'off'}
diff --git a/src/js/components/Header.js b/src/js/components/Header.js index ca5fb39a..a233350e 100755 --- a/src/js/components/Header.js +++ b/src/js/components/Header.js @@ -16,26 +16,32 @@ export default class Header extends React.Component{ } } - handleContextMenuTrigger(e,options){ - e.preventDefault() - var data = { - e: e, - context: 'custom', - title: this.props.title, - options: options + handleContextMenuTrigger(e,options){ + + // We have an override trigger (eg Album, Playlist) + if (this.props.handleContextMenuTrigger){ + return this.props.handleContextMenuTrigger(e) + } else { + e.preventDefault() + var data = { + e: e, + context: 'custom', + title: this.props.title, + options: options + } + this.props.uiActions.showContextMenu(data) } - this.props.uiActions.showContextMenu(data) } renderOptions(){ - if (!this.props.options) return null + if (!this.props.options && !this.props.handleContextMenuTrigger) return null return (
this.handleContextMenuTrigger(e,this.props.options)} /> - { this.props.options } + {this.props.options ? this.props.options : null}
diff --git a/src/js/components/PlaybackControls.js b/src/js/components/PlaybackControls.js index 32acfd33..44f04690 100755 --- a/src/js/components/PlaybackControls.js +++ b/src/js/components/PlaybackControls.js @@ -62,7 +62,7 @@ class PlaybackControls extends React.Component{ } render(){ - var images = [] + var images = false if (this.props.current_track && this.props.current_track.album && this.props.current_track.album.images){ images = this.props.current_track.album.images } diff --git a/src/js/components/Track.js b/src/js/components/Track.js index 48aaa2a6..4b44b725 100755 --- a/src/js/components/Track.js +++ b/src/js/components/Track.js @@ -59,6 +59,9 @@ export default class Track extends React.Component{ var track_columns = ( + + {this.props.track.selected ? : null} + {track.name ? track.name : {track.uri}} @@ -89,8 +92,16 @@ export default class Track extends React.Component{ var added = '-' } + className+= ' has-drag-zone' + var track_columns = ( + + {this.props.track.selected ? : null} + + + + {track.name ? track.name : {track.uri}} {track.explicit ? EXPLICIT : null} @@ -116,6 +127,9 @@ export default class Track extends React.Component{ var track_columns = ( + + {this.props.track.selected ? : null} + {track.name ? track.name : {track.uri}} {track.explicit ? EXPLICIT : null} @@ -146,9 +160,6 @@ export default class Track extends React.Component{ onMouseUp={ e => this.props.handleMouseUp(e) } onDoubleClick={ e => this.props.handleDoubleClick(e) } onContextMenu={ e => this.handleContextMenu(e) }> - - {this.props.track.selected ? : null} - { track_columns } ); diff --git a/src/js/components/TrackList.js b/src/js/components/TrackList.js index ecddb44c..cccd014e 100755 --- a/src/js/components/TrackList.js +++ b/src/js/components/TrackList.js @@ -21,16 +21,19 @@ class TrackList extends React.Component{ last_selected_track: false } - this._touching = false + this.touch_dragging = false this.handleKeyUp = this.handleKeyUp.bind(this) + this.handleTouchMove = this.handleTouchMove.bind(this) } componentWillMount(){ - window.addEventListener("keyup", this.handleKeyUp, false); + window.addEventListener("keyup", this.handleKeyUp, false) + window.addEventListener("touchmove", this.handleTouchMove, false) } componentWillUnmount(){ - window.removeEventListener("keyup", this.handleKeyUp, false); + window.removeEventListener("keyup", this.handleKeyUp, false) + window.removeEventListener("touchmove", this.handleTouchMove, false) } componentDidMount(){ @@ -51,9 +54,9 @@ class TrackList extends React.Component{ var target = $(e.target) // Wide screen, so no worries - if ($(window).width() > 800){ + if (!this.props.slim_mode){ return 'default' - } else if (target.is('.state-icon') || target.closest('.state-icon').length > 0){ + } else if (target.is('.select-zone') || target.closest('.select-zone').length > 0){ return 'mobile' } @@ -76,12 +79,52 @@ class TrackList extends React.Component{ } handleTouchStart(e,index){ - this._touching = true + var target = $(e.target) + if (target.hasClass('drag-zone')){ + $('body').addClass('touch-dragging') + this.touch_dragging = true + e.preventDefault() + } + } + + handleTouchMove(e){ + if (this.touch_dragging){ + e.preventDefault() + let touch = e.touches[0] + let over = $(document.elementFromPoint(touch.clientX, touch.clientY)) + if (!over.is('.track')){ + over = over.closest('.track') + } + $(document).find('.touch-drag-hover').removeClass('touch-drag-hover') + if (over.length > 0){ + over.addClass('touch-drag-hover') + } + } } handleTouchEnd(e,index){ - this._touching = false - this.handleMouseDown(e,index) + if (this.touch_dragging){ + let touch = e.changedTouches[0] + let over = $(document.elementFromPoint(touch.clientX, touch.clientY)) + if (!over.is('.track')){ + over = over.closest('.track') + } + if (over.length > 0){ + let siblings = over.parent().children('.track') + let dropped_at = siblings.index(over) - 1 + + if (typeof(this.props.reorderTracks) !== 'undefined'){ + this.props.reorderTracks([index],dropped_at) + this.render() + } + } + + $(document).find('.touch-drag-hover').removeClass('touch-drag-hover') + $('body').removeClass('touch-dragging') + this.touch_dragging = false + } else { + this.handleMouseDown(e,index) + } // Prevent any event bubbling. This prevents clicks and mouse events // from also being fired @@ -348,7 +391,8 @@ class TrackList extends React.Component{ { this.state.tracks.map( (track, index) => { - return self.handleMouseDown(e, index)} handleTouchStart={e => self.handleTouchStart(e, index)} handleTouchEnd={e => self.handleTouchEnd(e, index)} - handleContextMenu={e => self.handleContextMenu(e)} /> + handleContextMenu={e => self.handleContextMenu(e)} + /> + ) } ) } @@ -377,8 +423,8 @@ class TrackList extends React.Component{ const mapStateToProps = (state, ownProps) => { return { + slim_mode: state.ui.slim_mode, 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/services/spotify/actions.js b/src/js/services/spotify/actions.js index d03e1766..9a0faf5c 100755 --- a/src/js/services/spotify/actions.js +++ b/src/js/services/spotify/actions.js @@ -1,5 +1,6 @@ var uiActions = require('../../services/ui/actions') +var mopidyActions = require('../../services/mopidy/actions') var lastfmActions = require('../../services/lastfm/actions') var helpers = require('../../helpers') @@ -912,6 +913,27 @@ export function getLibraryArtists(){ } +export function playArtistTopTracks(uri){ + return (dispatch, getState) => { + const artists = getState().ui.artists + + // Do we have this artist (and their tracks) in our index already? + if (typeof(artists[uri]) !== 'undefined' && typeof(artists[uri].tracks) !== 'undefined'){ + const uris = helpers.asURIs(artists[uri].tracks) + dispatch(mopidyActions.playURIs(uris, uri)) + + // We need to load the artist's top tracks first + } else { + sendRequest( dispatch, getState, 'artists/'+ helpers.getFromUri('artistid', uri) +'/top-tracks?country='+getState().spotify.country ) + .then( response => { + const uris = helpers.asURIs(response.tracks) + dispatch(mopidyActions.playURIs(uris, uri)) + }) + } + } +} + + /** * =============================================================== USER(S) ============== diff --git a/src/js/services/ui/actions.js b/src/js/services/ui/actions.js index 89df0ffe..5a7c8c75 100755 --- a/src/js/services/ui/actions.js +++ b/src/js/services/ui/actions.js @@ -30,6 +30,13 @@ export function showContextMenu(data){ } } +export function setSlimMode(slim_mode){ + return { + type: 'SET_SLIM_MODE', + slim_mode: slim_mode + } +} + export function hideContextMenu(){ return { type: 'HIDE_CONTEXT_MENU' diff --git a/src/js/services/ui/middleware.js b/src/js/services/ui/middleware.js index 3e1a821c..4218771a 100755 --- a/src/js/services/ui/middleware.js +++ b/src/js/services/ui/middleware.js @@ -263,6 +263,8 @@ const UIMiddleware = (function(){ case 'OPEN_MODAL': ReactGA.event({ category: 'Modal', action: 'Opened', label: action.modal.name }) $('body').addClass('modal-open') + store.dispatch(uiActions.hideContextMenu()) + store.dispatch(uiActions.hideTouchContextMenu()) next(action) break diff --git a/src/js/services/ui/reducer.js b/src/js/services/ui/reducer.js index 9012205e..1aedd621 100755 --- a/src/js/services/ui/reducer.js +++ b/src/js/services/ui/reducer.js @@ -7,6 +7,9 @@ export default function reducer(ui = {}, action){ case 'LAZY_LOADING': return Object.assign({}, ui, { lazy_loading: action.start }); + case 'SET_SLIM_MODE': + return Object.assign({}, ui, { slim_mode: action.slim_mode }); + case 'DEBUG': return Object.assign({}, ui, { debug_response: action.response }) diff --git a/src/js/views/Album.js b/src/js/views/Album.js index e3738c51..cf82040a 100755 --- a/src/js/views/Album.js +++ b/src/js/views/Album.js @@ -4,6 +4,7 @@ import { connect } from 'react-redux' import { bindActionCreators } from 'redux' import FontAwesome from 'react-fontawesome' +import Header from '../components/Header' import TrackList from '../components/TrackList' import Thumbnail from '../components/Thumbnail' import Parallax from '../components/Parallax' @@ -12,7 +13,6 @@ import ArtistGrid from '../components/ArtistGrid' import FollowButton from '../components/FollowButton' import Dater from '../components/Dater' import LazyLoadListener from '../components/LazyLoadListener' -import SidebarToggleButton from '../components/SidebarToggleButton' import ContextMenuTrigger from '../components/ContextMenuTrigger' import * as helpers from '../helpers' @@ -121,7 +121,7 @@ class Album extends React.Component{ return (
- + {this.props.slim_mode ?
this.handleContextMenu(e)} uiActions={this.props.uiActions} /> : null}
@@ -132,13 +132,8 @@ class Album extends React.Component{

{ this.props.album.name }