Merge branch 'feature/touch'
This commit is contained in:
@ -8,6 +8,7 @@
|
||||
<meta name="mobile-web-app-capable" content="yes" >
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="theme-color" content="#222222" />
|
||||
<meta name="viewport" content="width=device-width">
|
||||
|
||||
<link rel="shortcut icon" type="image/ico" href="/iris/assets/favicon.ico" />
|
||||
<link rel="shortcut icon" type="image/x-icon" href="/iris/assets/favicon.ico" />
|
||||
|
||||
@ -119,7 +119,7 @@ class App extends React.Component{
|
||||
if (this.props.dragger && this.props.dragger.dragging){
|
||||
this.props.uiActions.dragCancel()
|
||||
}
|
||||
if (this.props.modal){';'
|
||||
if (this.props.modal){
|
||||
this.props.uiActions.closeModal()
|
||||
}
|
||||
break
|
||||
@ -180,7 +180,7 @@ 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 (helpers.isTouchDevice() || this.props.emulate_touch){
|
||||
if (helpers.isTouchDevice()){
|
||||
className += ' touch'
|
||||
} else {
|
||||
className += ' notouch'
|
||||
|
||||
3
src/js/bootstrap.js
vendored
3
src/js/bootstrap.js
vendored
@ -28,14 +28,13 @@ var initialState = {
|
||||
mopidy: {
|
||||
connected: false,
|
||||
host: window.location.hostname,
|
||||
port: 6680,
|
||||
port: (window.location.port ? window.location.port : '80'),
|
||||
volume: 0,
|
||||
progress: 0,
|
||||
play_state: false
|
||||
},
|
||||
pusher: {
|
||||
connected: false,
|
||||
port: 6681,
|
||||
username: 'Anonymous',
|
||||
connections: {},
|
||||
version: {
|
||||
|
||||
@ -66,20 +66,18 @@ class SpotifyAuthenticationFrame extends React.Component{
|
||||
renderAuthorizeButton(){
|
||||
if( this.state.authorizing ){
|
||||
return (
|
||||
<button disabled>
|
||||
<FontAwesome name="circle-o-notch" spin />
|
||||
|
||||
<button className="working">
|
||||
Authorizing...
|
||||
</button>
|
||||
);
|
||||
)
|
||||
}else if( this.props.authorized ){
|
||||
return (
|
||||
<button className="destructive" onClick={() => this.props.spotifyActions.authorizationRevoked()}>Log out</button>
|
||||
);
|
||||
)
|
||||
}else{
|
||||
return (
|
||||
<button className="primary" onClick={() => this.startAuthorization()}>Log in</button>
|
||||
);
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -88,9 +86,7 @@ class SpotifyAuthenticationFrame extends React.Component{
|
||||
|
||||
if (this.props.refreshing_token){
|
||||
return (
|
||||
<button disabled>
|
||||
<FontAwesome name="circle-o-notch" spin />
|
||||
|
||||
<button className="working">
|
||||
Refreshing...
|
||||
</button>
|
||||
);
|
||||
|
||||
@ -135,8 +135,10 @@ 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 ? <FontAwesome name="check" className="select-state" fixedWidth /> : null }
|
||||
{ this.props.track.playing ? <FontAwesome name="play" className="play-state" fixedWidth /> : null }
|
||||
<span className="state-icon">
|
||||
{ this.props.track.selected ? <FontAwesome name="check" className="selected" fixedWidth /> : null }
|
||||
{ this.props.track.playing && !this.props.track.selected ? <FontAwesome name="play" className="playing" fixedWidth /> : null }
|
||||
</span>
|
||||
{ track_columns }
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -14,18 +14,14 @@ import * as uiActions from '../services/ui/actions'
|
||||
class TrackList extends React.Component{
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._touch_threshold = 10
|
||||
this._touch_x = null
|
||||
this._touch_y = null
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
tracks: [],
|
||||
last_selected_track: false,
|
||||
edit_mode: false
|
||||
last_selected_track: false
|
||||
}
|
||||
|
||||
this._touching = false
|
||||
this.handleKeyUp = this.handleKeyUp.bind(this)
|
||||
}
|
||||
|
||||
@ -45,6 +41,25 @@ class TrackList extends React.Component{
|
||||
this.setState({ tracks: this.keyifyTracks(nextProps.tracks) });
|
||||
}
|
||||
|
||||
/**
|
||||
* Figure out if our click/touch event is valid and we can act accordingly
|
||||
*
|
||||
* @param e = event obj
|
||||
* @return string
|
||||
**/
|
||||
triggerType(e){
|
||||
var target = $(e.target)
|
||||
|
||||
// Wide screen, so no worries
|
||||
if ($(window).width() > 800){
|
||||
return 'default'
|
||||
} else if (target.is('.state-icon') || target.closest('.state-icon').length > 0){
|
||||
return 'mobile'
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
handleKeyUp(e){
|
||||
if( this.selectedTracks().length <= 0 ) return;
|
||||
|
||||
@ -61,28 +76,15 @@ class TrackList extends React.Component{
|
||||
}
|
||||
|
||||
handleTouchStart(e,index){
|
||||
this._touch_x = Math.round(e.changedTouches[0].pageX)
|
||||
this._touch_y = Math.round(e.changedTouches[0].pageY)
|
||||
this._touching = true
|
||||
}
|
||||
|
||||
handleTouchEnd(e,index){
|
||||
var pageX = Math.round(e.changedTouches[0].pageX)
|
||||
var pageY = Math.round(e.changedTouches[0].pageY)
|
||||
clearTimeout(this._touch_hold_timer)
|
||||
|
||||
// make sure our touch was within the threshold of the touch start
|
||||
// this helps us differentiate between taps and drags but doesn't consider
|
||||
// multi-finger touches
|
||||
if( this.state.edit_mode &&
|
||||
this._touch_x < ( pageX + this._touch_threshold ) &&
|
||||
this._touch_x > ( pageX - this._touch_threshold ) &&
|
||||
this._touch_y < ( pageY + this._touch_threshold ) &&
|
||||
this._touch_y > ( pageY - this._touch_threshold ) ){
|
||||
var tracks = this.state.tracks
|
||||
tracks[index].selected = !tracks[index].selected
|
||||
this.setState({ tracks: tracks, last_selected_track: index })
|
||||
}
|
||||
this._touching = false
|
||||
this.handleMouseDown(e,index)
|
||||
|
||||
// Prevent any event bubbling. This prevents clicks and mouse events
|
||||
// from also being fired
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
@ -92,49 +94,61 @@ class TrackList extends React.Component{
|
||||
}
|
||||
|
||||
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) this.props.uiActions.hideContextMenu()
|
||||
|
||||
var selected_tracks = this.selectedTracks()
|
||||
this.props.uiActions.dragStart( e, this.props.context, this.props.uri, selected_tracks, this.tracksIndexes(selected_tracks) )
|
||||
// Regular clicking an un-selected element
|
||||
// This selects the track before we potentially drag
|
||||
switch (this.triggerType(e)){
|
||||
|
||||
case 'mobile':
|
||||
// simple toggle
|
||||
var tracks = this.state.tracks
|
||||
tracks[index].selected = !tracks[index].selected
|
||||
this.setState({tracks: tracks, last_selected_track: index})
|
||||
break
|
||||
|
||||
case 'default':
|
||||
if (!this.state.tracks[index].selected && !this.isRightClick(e) && !e.ctrlKey){
|
||||
this.toggleTrackSelections(e, index)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
var selected_tracks = this.selectedTracks()
|
||||
this.props.uiActions.dragStart( e, this.props.context, this.props.uri, selected_tracks, this.tracksIndexes(selected_tracks) )
|
||||
}
|
||||
|
||||
handleMouseUp(e,index){
|
||||
if (this.triggerType(e) == 'default'){
|
||||
|
||||
// right-clicking on an un-highlighted track
|
||||
if (!this.state.tracks[index].selected && this.isRightClick(e)){
|
||||
this.toggleTrackSelections(e, index)
|
||||
|
||||
// right-clicking on an un-highlighted track
|
||||
if( !this.state.tracks[index].selected && this.isRightClick(e) ){
|
||||
this.toggleTrackSelections(e, index)
|
||||
// selected track, regular click
|
||||
}else if (this.state.tracks[index].selected && !this.isRightClick(e)){
|
||||
this.toggleTrackSelections(e, index)
|
||||
|
||||
// selected track, regular click
|
||||
}else if( this.state.tracks[index].selected && !this.isRightClick(e) ){
|
||||
this.toggleTrackSelections(e, index)
|
||||
// ctrl key
|
||||
} else if(e.ctrlKey){
|
||||
this.toggleTrackSelections(e, index)
|
||||
}
|
||||
|
||||
// ctrl key
|
||||
}else if( e.ctrlKey ){
|
||||
this.toggleTrackSelections(e, index)
|
||||
}
|
||||
|
||||
if( this.props.dragger && this.props.dragger.active ){
|
||||
|
||||
// if this tracklist handles sorting, handle it
|
||||
if( typeof(this.props.reorderTracks) !== 'undefined' ){
|
||||
var indexes = this.props.dragger.victims_indexes
|
||||
return this.props.reorderTracks( indexes, index );
|
||||
if (this.props.dragger && this.props.dragger.active){
|
||||
|
||||
// if this tracklist handles sorting, handle it
|
||||
if (typeof(this.props.reorderTracks) !== 'undefined'){
|
||||
var indexes = this.props.dragger.victims_indexes
|
||||
return this.props.reorderTracks( indexes, index )
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleContextMenu(e, native_event = true){
|
||||
|
||||
// touch events fired? we assume the user is primarily touching,
|
||||
// so for touch devices we disable direct context menus
|
||||
// hybrid devices will only work with touch OR mouse, not both in this case
|
||||
if (this._touch_x && this._touch_y && native_event){
|
||||
this.setState({edit_mode: true})
|
||||
// disable context menu events in mobile view
|
||||
if (this.triggerType(e) == 'mobile'){
|
||||
e.preventDefault()
|
||||
return false
|
||||
}
|
||||
@ -327,9 +341,6 @@ class TrackList extends React.Component{
|
||||
if (this.props.className){
|
||||
className += ' '+this.props.className
|
||||
}
|
||||
if (this.state.edit_mode){
|
||||
className += ' edit-mode'
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
@ -342,12 +353,12 @@ class TrackList extends React.Component{
|
||||
key={track.key}
|
||||
track={track}
|
||||
context={this.props.context}
|
||||
handleDoubleClick={ e => self.handleDoubleClick(e, index)}
|
||||
handleMouseUp={ e => self.handleMouseUp(e, index)}
|
||||
handleMouseDown={ e => self.handleMouseDown(e, index)}
|
||||
handleTouchStart={ e => self.handleTouchStart(e, index)}
|
||||
handleTouchEnd={ e => self.handleTouchEnd(e, index)}
|
||||
handleContextMenu={ e => self.handleContextMenu(e)} />
|
||||
handleDoubleClick={e => self.handleDoubleClick(e, index)}
|
||||
handleMouseUp={e => self.handleMouseUp(e, index)}
|
||||
handleMouseDown={e => self.handleMouseDown(e, index)}
|
||||
handleTouchStart={e => self.handleTouchStart(e, index)}
|
||||
handleTouchEnd={e => self.handleTouchEnd(e, index)}
|
||||
handleContextMenu={e => self.handleContextMenu(e)} />
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@ -139,7 +139,7 @@ class Search extends React.Component{
|
||||
var artists_section = (
|
||||
<section>
|
||||
<div className="inner">
|
||||
<h4><Link to={global.baseURL+'search/artists/'+this.props.params.query}>Artists</Link></h4>
|
||||
<h4><Link to={global.baseURL+'search/iris:search:artists:'+this.props.params.query}>Artists</Link></h4>
|
||||
<ArtistGrid show_source_icon artists={artists.slice(0,5)} />
|
||||
</div>
|
||||
</section>
|
||||
@ -152,7 +152,7 @@ class Search extends React.Component{
|
||||
var albums_section = (
|
||||
<section>
|
||||
<div className="inner">
|
||||
<h4><Link to={global.baseURL+'search/albums/'+this.props.params.query}>Albums</Link></h4>
|
||||
<h4><Link to={global.baseURL+'search/iris:search:albums:'+this.props.params.query}>Albums</Link></h4>
|
||||
<AlbumGrid show_source_icon albums={albums.slice(0,5)} />
|
||||
</div>
|
||||
</section>
|
||||
@ -165,7 +165,7 @@ class Search extends React.Component{
|
||||
var playlists_section = (
|
||||
<section>
|
||||
<div className="inner">
|
||||
<h4><Link to={global.baseURL+'search/playlists/'+this.props.params.query}>Playlists</Link></h4>
|
||||
<h4><Link to={global.baseURL+'search/iris:search:playlists:'+this.props.params.query}>Playlists</Link></h4>
|
||||
<PlaylistGrid show_source_icon playlists={playlists.slice(0,5)} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@ -248,28 +248,32 @@
|
||||
}
|
||||
}
|
||||
|
||||
.list {
|
||||
.context-menu-trigger {
|
||||
display: none;
|
||||
}
|
||||
.list {
|
||||
&.track-list {
|
||||
|
||||
&.track-list.edit-mode {
|
||||
.context-menu-trigger {
|
||||
display: block;
|
||||
position: fixed;
|
||||
bottom: 50px;
|
||||
right: 5px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
overflow: hidden;
|
||||
z-index: 97;
|
||||
border-radius: 50%;
|
||||
background: $blue;
|
||||
color: #FFFFFF;
|
||||
|
||||
&:focus,
|
||||
&:active {
|
||||
background: darken($blue, 10%);
|
||||
display: none;
|
||||
}
|
||||
|
||||
@include responsive( $bp_medium ){
|
||||
|
||||
.context-menu-trigger {
|
||||
display: block;
|
||||
position: fixed;
|
||||
bottom: 50px;
|
||||
right: 5px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
overflow: hidden;
|
||||
z-index: 97;
|
||||
border-radius: 50%;
|
||||
background: $blue;
|
||||
color: #FFFFFF;
|
||||
|
||||
&:focus,
|
||||
&:active {
|
||||
background: darken($blue, 10%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -42,13 +42,9 @@
|
||||
|
||||
&:active,
|
||||
&:focus {
|
||||
.thumbnail {
|
||||
.image {
|
||||
margin: 2%;
|
||||
padding-bottom: 96%;
|
||||
width: 96%;
|
||||
}
|
||||
}
|
||||
-moz-transform: scale(0.98);
|
||||
-webkit-transform: scale(0.98);
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
|
||||
.list {
|
||||
|
||||
.list-item {
|
||||
@include clearfix;
|
||||
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
@ -16,12 +16,29 @@
|
||||
border-top: 1px solid $faint_grey;
|
||||
border-bottom: 1px solid $faint_grey;
|
||||
margin-bottom: -1px;
|
||||
@include clearfix;
|
||||
cursor: pointer;
|
||||
|
||||
&.selected {
|
||||
background: $yellow !important;
|
||||
}
|
||||
|
||||
&.playing {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.state-icon {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
font-size: 10px;
|
||||
|
||||
.fa {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
left: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
:root .notouch:not(.dragging) &:not(.header):not(.no-click):hover {
|
||||
background: rgba(150,150,150,0.1);
|
||||
cursor: pointer;
|
||||
@ -71,35 +88,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.select-state {
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
left: 15px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.play-state {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
left: 15px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
&.playing {
|
||||
font-weight: 600;
|
||||
.play-state {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.playing.selected {
|
||||
.play-state {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
:root .dragging &:hover {
|
||||
border-top: 3px solid $blue;
|
||||
margin-top: -3px;
|
||||
@ -222,13 +210,40 @@
|
||||
|
||||
@include responsive( $bp_medium ){
|
||||
.list-item {
|
||||
padding: 7px 30px 7px 16px !important;
|
||||
padding: 7px 30px 7px 48px !important;
|
||||
|
||||
.source {
|
||||
position: static;
|
||||
float: none;
|
||||
}
|
||||
|
||||
.state-icon {
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 48px;
|
||||
|
||||
.fa {
|
||||
top: 19px;
|
||||
left: 17px;
|
||||
}
|
||||
|
||||
&:after {
|
||||
display: block;
|
||||
content: '';
|
||||
border: 1px solid $darkest_grey;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
left: 15px;
|
||||
}
|
||||
|
||||
.playing {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.col {
|
||||
&.name {
|
||||
width: 90% !important;
|
||||
@ -275,13 +290,6 @@
|
||||
&.header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.track {
|
||||
.select-state,
|
||||
.play-state {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.queue-track-list {
|
||||
@ -300,17 +308,5 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.edit-mode {
|
||||
.list-item {
|
||||
padding-left: 40px !important;
|
||||
|
||||
&.track {
|
||||
.select-state {
|
||||
display: block !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -6,6 +6,7 @@ html {
|
||||
color: #000000;
|
||||
font-family: "Archivo Narrow", Helvetica, Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
body {
|
||||
|
||||
@ -1,4 +1,12 @@
|
||||
|
||||
:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
::-moz-focus-inner {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
button,
|
||||
textarea,
|
||||
input[type="text"],
|
||||
@ -159,6 +167,36 @@ input[type="submit"] {
|
||||
}
|
||||
}
|
||||
|
||||
&.working {
|
||||
position: relative;
|
||||
|
||||
&:after {
|
||||
position: absolute;
|
||||
display: block;
|
||||
content: '';
|
||||
background: $mid_grey;
|
||||
opacity: 1;
|
||||
animation: slideloader 1s infinite;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&.destructive:after {
|
||||
background: $red;
|
||||
}
|
||||
|
||||
&.primary:after {
|
||||
background: $turquoise;
|
||||
}
|
||||
|
||||
&.alternative:after {
|
||||
background: $blue;
|
||||
}
|
||||
}
|
||||
|
||||
&.context-menu-trigger {
|
||||
border-color: transparent;
|
||||
padding: 6px;
|
||||
|
||||
Reference in New Issue
Block a user