Merge branch 'develop'
This commit is contained in:
@ -1,4 +1,84 @@
|
||||
@charset "UTF-8";
|
||||
.input-range__slider {
|
||||
appearance: none;
|
||||
background: #3f51b5;
|
||||
border: 1px solid #3f51b5;
|
||||
border-radius: 100%;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
height: 1rem;
|
||||
margin-left: -0.5rem;
|
||||
margin-top: -0.65rem;
|
||||
outline: none;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transition: transform 0.3s ease-out, box-shadow 0.3s ease-out;
|
||||
width: 1rem; }
|
||||
.input-range__slider:active {
|
||||
transform: scale(1.3); }
|
||||
.input-range__slider:focus {
|
||||
box-shadow: 0 0 0 5px rgba(63, 81, 181, 0.2); }
|
||||
.input-range--disabled .input-range__slider {
|
||||
background: #cccccc;
|
||||
border: 1px solid #cccccc;
|
||||
box-shadow: none;
|
||||
transform: none; }
|
||||
|
||||
.input-range__slider-container {
|
||||
transition: left 0.3s ease-out; }
|
||||
|
||||
.input-range__label {
|
||||
color: #aaaaaa;
|
||||
font-family: "Helvetica Neue", san-serif;
|
||||
font-size: 0.8rem;
|
||||
transform: translateZ(0);
|
||||
white-space: nowrap; }
|
||||
|
||||
.input-range__label--min,
|
||||
.input-range__label--max {
|
||||
bottom: -1.4rem;
|
||||
position: absolute; }
|
||||
|
||||
.input-range__label--min {
|
||||
left: 0; }
|
||||
|
||||
.input-range__label--max {
|
||||
right: 0; }
|
||||
|
||||
.input-range__label--value {
|
||||
position: absolute;
|
||||
top: -1.8rem; }
|
||||
|
||||
.input-range__label-container {
|
||||
left: -50%;
|
||||
position: relative; }
|
||||
.input-range__label--max .input-range__label-container {
|
||||
left: 50%; }
|
||||
|
||||
.input-range__track {
|
||||
background: #eeeeee;
|
||||
border-radius: 0.3rem;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
height: 0.3rem;
|
||||
position: relative;
|
||||
transition: left 0.3s ease-out, width 0.3s ease-out; }
|
||||
.input-range--disabled .input-range__track {
|
||||
background: #eeeeee; }
|
||||
|
||||
.input-range__track--background {
|
||||
left: 0;
|
||||
margin-top: -0.15rem;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 50%; }
|
||||
|
||||
.input-range__track--active {
|
||||
background: #3f51b5; }
|
||||
|
||||
.input-range {
|
||||
height: 1rem;
|
||||
position: relative;
|
||||
width: 100%; }@charset "UTF-8";
|
||||
/*!
|
||||
* Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome
|
||||
* License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License)
|
||||
@ -2935,6 +3015,22 @@ input[type="submit"] {
|
||||
background: #f2f2f2;
|
||||
margin-top: 1px;
|
||||
margin-bottom: 9px; }
|
||||
.button.small,
|
||||
button.small,
|
||||
input[type="button"].small,
|
||||
input[type="submit"].small {
|
||||
padding: 6px 18px 4px; }
|
||||
.button.large,
|
||||
button.large,
|
||||
input[type="button"].large,
|
||||
input[type="submit"].large {
|
||||
padding: 16px 50px 14px; }
|
||||
.button.pull-right,
|
||||
button.pull-right,
|
||||
input[type="button"].pull-right,
|
||||
input[type="submit"].pull-right {
|
||||
margin-right: 0;
|
||||
margin-left: 30px; }
|
||||
.button.primary,
|
||||
button.primary,
|
||||
input[type="button"].primary,
|
||||
@ -3094,7 +3190,7 @@ input[type="submit"] {
|
||||
display: inline-block;
|
||||
float: none;
|
||||
width: auto;
|
||||
padding: 5px 30px 5px 30px;
|
||||
padding: 5px 5px 5px 26px;
|
||||
position: relative;
|
||||
cursor: pointer; }
|
||||
.field.radio input + .label:before {
|
||||
@ -3125,6 +3221,37 @@ input[type="submit"] {
|
||||
left: 0; }
|
||||
.field.checkbox input:checked + .label:before {
|
||||
content: '\F00C'; }
|
||||
.field.range {
|
||||
position: relative; }
|
||||
.field.range .input-range {
|
||||
height: auto; }
|
||||
.field.range .label {
|
||||
padding-bottom: 6px; }
|
||||
.field.range .input-range__track,
|
||||
.field.range .input-range__slider-container {
|
||||
transition: none;
|
||||
height: 6px;
|
||||
border-radius: 3px;
|
||||
cursor: col-resize; }
|
||||
.field.range .input-range__track--background {
|
||||
margin: 0;
|
||||
background: rgba(100, 100, 100, 0.5); }
|
||||
.field.range .input-range__track--active {
|
||||
background: #32b5f2; }
|
||||
.field.range .input-range__slider-container .input-range__slider {
|
||||
cursor: col-resize;
|
||||
background: #32b5f2;
|
||||
border: none;
|
||||
top: 0;
|
||||
height: 6px;
|
||||
width: 20px;
|
||||
margin: -6px 0 0 0;
|
||||
border-radius: 3px 0 0 3px; }
|
||||
.field.range .input-range__slider-container:last-child .input-range__slider {
|
||||
border-radius: 0 3px 3px 0;
|
||||
margin-left: -12px; }
|
||||
.field.sub-field {
|
||||
padding: 0; }
|
||||
.field .description {
|
||||
display: inline-block;
|
||||
padding: 10px 14px;
|
||||
@ -3413,6 +3540,9 @@ input[type="submit"] {
|
||||
background: -moz-linear-gradient(to bottom right, rgba(255, 255, 255, 0.75), black);
|
||||
background: -o-linear-gradient(to bottom right, rgba(255, 255, 255, 0.75), black);
|
||||
background: linear-gradient(to bottom right, rgba(255, 255, 255, 0.75), black); }
|
||||
h4 .context-menu-trigger,
|
||||
h4 .context-menu-trigger:active {
|
||||
margin: -8px 0 0 10px; }
|
||||
.context-menu-trigger .dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
@ -4986,19 +5116,21 @@ main header {
|
||||
font-family: "Overpass";
|
||||
position: relative;
|
||||
display: inline-block; }
|
||||
.dropdown-field .label {
|
||||
.dropdown-field .label:not(.button) {
|
||||
-webkit-transition: all 0.2s ease-in-out;
|
||||
-moz-transition: all 0.2s ease-in-out;
|
||||
-o-transition: all 0.2s ease-in-out;
|
||||
transition: all 0.2s ease-in-out;
|
||||
padding: 14px 10px;
|
||||
cursor: pointer; }
|
||||
.notouch .dropdown-field .label:hover {
|
||||
.notouch .dropdown-field .label:not(.button):hover {
|
||||
color: #32b5f2; }
|
||||
.dropdown-field.expanded .label {
|
||||
.dropdown-field.expanded .label:not(.button) {
|
||||
color: #32b5f2; }
|
||||
.dropdown-field.expanded .options {
|
||||
display: block; }
|
||||
.dropdown-field.no-status-icon .options .option {
|
||||
padding-left: 12px; }
|
||||
.dropdown-field .options {
|
||||
color: #000000;
|
||||
position: absolute;
|
||||
@ -5226,6 +5358,8 @@ main header {
|
||||
.has-tooltip:hover .tooltip {
|
||||
opacity: 1;
|
||||
bottom: 35px; }
|
||||
.has-tooltip.info .fa {
|
||||
opacity: 0.3; }
|
||||
.has-tooltip.large-tooltip .tooltip {
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
@ -5690,34 +5824,64 @@ main .track-view {
|
||||
color: #FFFFFF; }
|
||||
.discover-view .intro .liner h2 {
|
||||
padding-bottom: 8px; }
|
||||
.discover-view .intro .liner h3 {
|
||||
padding-bottom: 40px;
|
||||
opacity: 0.5; }
|
||||
|
||||
.discover-view .seeds {
|
||||
padding-top: 40px; }
|
||||
.discover-view .seeds .seed {
|
||||
.discover-view .seeds,
|
||||
.discover-view .tunabilities {
|
||||
padding-top: 30px; }
|
||||
.discover-view .seeds h3,
|
||||
.discover-view .tunabilities h3 {
|
||||
padding-bottom: 6px; }
|
||||
|
||||
.discover-view .seeds .seed {
|
||||
display: inline-block;
|
||||
background: #32b5f2;
|
||||
color: #FFFFFF;
|
||||
padding: 8px 26px 8px 14px;
|
||||
font-size: 16px;
|
||||
position: relative;
|
||||
vertical-align: top;
|
||||
margin: 0 10px 10px 0;
|
||||
border-radius: 3px; }
|
||||
.discover-view .seeds .seed .type {
|
||||
display: inline-block;
|
||||
background: #32b5f2;
|
||||
color: #FFFFFF;
|
||||
padding: 8px 26px 8px 14px;
|
||||
font-size: 16px;
|
||||
position: relative;
|
||||
vertical-align: top;
|
||||
margin: 0 10px 10px 0;
|
||||
border-radius: 3px; }
|
||||
.discover-view .seeds .seed .type {
|
||||
display: inline-block;
|
||||
font-size: 12px;
|
||||
opacity: 0.5;
|
||||
padding: 0 10px 0 5px; }
|
||||
.discover-view .seeds .seed .remove {
|
||||
font-size: 10px;
|
||||
padding: 12px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
cursor: pointer; }
|
||||
font-size: 12px;
|
||||
opacity: 0.5;
|
||||
padding: 0 10px 0 5px; }
|
||||
.discover-view .seeds .seed .remove {
|
||||
font-size: 10px;
|
||||
padding: 12px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
cursor: pointer; }
|
||||
|
||||
.discover-view .tunabilities::after {
|
||||
content: '';
|
||||
clear: both;
|
||||
display: block; }
|
||||
|
||||
.discover-view .tunabilities .tunability {
|
||||
width: 200px;
|
||||
float: left;
|
||||
box-sizing: border-box;
|
||||
padding-right: 20px;
|
||||
position: relative; }
|
||||
.discover-view .tunabilities .tunability .input {
|
||||
float: none;
|
||||
width: 100%; }
|
||||
.discover-view .tunabilities .tunability .input-range .input-range__label-container {
|
||||
display: none; }
|
||||
.discover-view .tunabilities .tunability .remove {
|
||||
-webkit-transition: all 0.2s ease-in-out;
|
||||
-moz-transition: all 0.2s ease-in-out;
|
||||
-o-transition: all 0.2s ease-in-out;
|
||||
transition: all 0.2s ease-in-out;
|
||||
cursor: pointer;
|
||||
font-size: 10px;
|
||||
padding: 4px;
|
||||
margin-left: 4px; }
|
||||
.discover-view .tunabilities .tunability .remove:hover {
|
||||
color: #cf2d2d; }
|
||||
|
||||
.discover-view .autocomplete-field {
|
||||
width: 250px;
|
||||
@ -5738,6 +5902,10 @@ main .track-view {
|
||||
.discover-view .autocomplete-field .input:after {
|
||||
background: #32b5f2; }
|
||||
|
||||
.discover-view .actions {
|
||||
padding-top: 60px;
|
||||
text-align: center; }
|
||||
|
||||
.discover-view section .grid {
|
||||
padding-bottom: 20px; }
|
||||
|
||||
@ -5745,7 +5913,17 @@ main .track-view {
|
||||
.discover-view .intro {
|
||||
padding: 40px 20px; }
|
||||
.discover-view .seeds {
|
||||
padding-top: 20px; } }
|
||||
padding-top: 20px; }
|
||||
.discover-view .tunabilities .tunability {
|
||||
width: 100%;
|
||||
float: none;
|
||||
padding: 15px 0; }
|
||||
.discover-view .tunabilities .dropdown-field {
|
||||
margin-top: 20px; }
|
||||
.discover-view .col.tracks {
|
||||
width: 100%; }
|
||||
.discover-view .col.others, .discover-view .col.w5 {
|
||||
display: none; } }
|
||||
|
||||
.discover-featured-view .intro,
|
||||
.discover-new-releases-view .intro {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@ -38,7 +38,7 @@
|
||||
|
||||
// Release details
|
||||
// These are automatically injected to built HTML
|
||||
var build = "1515141197";
|
||||
var build = "1516660429";
|
||||
var version = "3.11.0";
|
||||
|
||||
// Construct the script tag
|
||||
|
||||
@ -41,7 +41,8 @@
|
||||
"style-loader": "^0.13.2",
|
||||
"url-loader": "^0.5.9",
|
||||
"webpack": "^3.6.0",
|
||||
"webpack-strip": "0.1.0"
|
||||
"webpack-strip": "0.1.0",
|
||||
"react-input-range": "1.3.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "node build_tools/build.js",
|
||||
|
||||
@ -78,12 +78,12 @@ class AddSeedField extends React.Component{
|
||||
}
|
||||
|
||||
results(){
|
||||
if (typeof(this.props.results) === 'undefined'){
|
||||
return null
|
||||
} else if (typeof(this.props.results[this.id]) === 'undefined'){
|
||||
return null
|
||||
if (this.props.results === undefined){
|
||||
return null;
|
||||
} else if (this.props.results[this.id] === undefined){
|
||||
return null;
|
||||
} else {
|
||||
return this.props.results[this.id]
|
||||
return this.props.results[this.id];
|
||||
}
|
||||
}
|
||||
|
||||
@ -137,7 +137,7 @@ class AddSeedField extends React.Component{
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
return {
|
||||
genres: (state.ui.genres ? state.ui.genres : null),
|
||||
genres: (state.spotify.genres ? state.spotify.genres : null),
|
||||
results: (state.spotify.autocomplete_results ? state.spotify.autocomplete_results : {})
|
||||
}
|
||||
}
|
||||
|
||||
153
src/js/components/AddTunabilityField.js
Executable file
153
src/js/components/AddTunabilityField.js
Executable file
@ -0,0 +1,153 @@
|
||||
|
||||
import React, { PropTypes } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { bindActionCreators } from 'redux'
|
||||
import { Link, hashHistory } from 'react-router'
|
||||
|
||||
import ArtistSentence from './ArtistSentence'
|
||||
import * as helpers from '../helpers'
|
||||
import * as coreActions from '../services/core/actions'
|
||||
import * as uiActions from '../services/ui/actions'
|
||||
import * as spotifyActions from '../services/spotify/actions'
|
||||
|
||||
class AddSeedField extends React.Component{
|
||||
|
||||
constructor(props){
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
value: ''
|
||||
}
|
||||
this.id = helpers.generateGuid()
|
||||
this.timer = null
|
||||
this.handleClick = this.handleClick.bind(this)
|
||||
}
|
||||
|
||||
componentDidMount(){
|
||||
window.addEventListener("click", this.handleClick, false)
|
||||
|
||||
if (!this.props.genres){
|
||||
this.props.spotifyActions.getGenres()
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount(){
|
||||
window.removeEventListener("click", this.handleClick, false)
|
||||
}
|
||||
|
||||
handleClick(e){
|
||||
if ($(e.target).closest('.add-seed-field').length <= 0){
|
||||
this.props.spotifyActions.clearAutocompleteResults(this.id)
|
||||
}
|
||||
}
|
||||
|
||||
handleChange(e,value){
|
||||
var self = this
|
||||
|
||||
// update our local state
|
||||
this.setState({value: value})
|
||||
|
||||
// start a timer to perform the actual search
|
||||
// this provides a wee delay between key presses to avoid request spamming
|
||||
clearTimeout(this.timer)
|
||||
this.timer = setTimeout(
|
||||
function(){
|
||||
self.setState({searching: true})
|
||||
self.props.spotifyActions.getAutocompleteResults(self.id,value,['artist','track','genre'])
|
||||
},
|
||||
500
|
||||
)
|
||||
}
|
||||
|
||||
handleSelect(e,item){
|
||||
this.setState({value: ''})
|
||||
this.props.onSelect(e,item.uri)
|
||||
this.props.spotifyActions.clearAutocompleteResults(this.id)
|
||||
|
||||
// Add our selected item to our global index
|
||||
switch (helpers.uriType(item.uri)){
|
||||
|
||||
case 'artist':
|
||||
this.props.coreActions.artistsLoaded(item);
|
||||
break;
|
||||
|
||||
case 'track':
|
||||
this.props.coreActions.tracksLoaded(item);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
results(){
|
||||
if (typeof(this.props.results) === 'undefined'){
|
||||
return null
|
||||
} else if (typeof(this.props.results[this.id]) === 'undefined'){
|
||||
return null
|
||||
} else {
|
||||
return this.props.results[this.id]
|
||||
}
|
||||
}
|
||||
|
||||
renderResults(type){
|
||||
var results = this.results()
|
||||
if (!results || typeof(results[type]) === 'undefined' || results[type].length <= 0) return null
|
||||
|
||||
// only show the first 3
|
||||
var items = results[type].slice(0,3)
|
||||
|
||||
return (
|
||||
<div className="type">
|
||||
<h4 className="grey-text">{type}</h4>
|
||||
{
|
||||
items.map(item => {
|
||||
return (
|
||||
<div className="result" key={item.uri} onClick={e => this.handleSelect(e,item)}>
|
||||
{item.name}
|
||||
{type == 'tracks' ? <span className="grey-text"> <ArtistSentence artists={item.artists} nolinks /></span> : null}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render(){
|
||||
var className = "field autocomplete-field add-seed-field"
|
||||
if (this.results() && this.results().loading){
|
||||
className += " loading"
|
||||
}
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className="input">
|
||||
<input
|
||||
type="text"
|
||||
value={this.state.value}
|
||||
onChange={e => this.handleChange(e,e.target.value)}
|
||||
placeholder={this.props.placeholder ? this.props.placeholder : "Start typing..."} />
|
||||
</div>
|
||||
<div className="results">
|
||||
{this.renderResults('artists')}
|
||||
{this.renderResults('tracks')}
|
||||
{this.renderResults('genres')}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
return {
|
||||
genres: (state.ui.genres ? state.ui.genres : null),
|
||||
results: (state.spotify.autocomplete_results ? state.spotify.autocomplete_results : {})
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
coreActions: bindActionCreators(coreActions, dispatch),
|
||||
uiActions: bindActionCreators(uiActions, dispatch),
|
||||
spotifyActions: bindActionCreators(spotifyActions, dispatch)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(AddSeedField)
|
||||
@ -108,7 +108,7 @@ class ContextMenu extends React.Component{
|
||||
case 'playlist':
|
||||
case 'editable-playlist':
|
||||
context.nice_name = 'playlist';
|
||||
break
|
||||
break;
|
||||
|
||||
case 'track':
|
||||
case 'queue-track':
|
||||
@ -116,7 +116,7 @@ class ContextMenu extends React.Component{
|
||||
case 'editable-playlist-track':
|
||||
context.nice_name = 'track';
|
||||
context.is_track = true;
|
||||
break
|
||||
break;
|
||||
}
|
||||
|
||||
// Consider the object(s) themselves
|
||||
@ -138,21 +138,21 @@ class ContextMenu extends React.Component{
|
||||
|
||||
inLibrary(item = null){
|
||||
if (!item){
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (helpers.uriType(item.uri)){
|
||||
case 'artist':
|
||||
return (this.props.spotify_library_artists && this.props.spotify_library_artists.indexOf(item.uri) > -1)
|
||||
break
|
||||
return (this.props.spotify_library_artists && this.props.spotify_library_artists.indexOf(item.uri) > -1);
|
||||
break;
|
||||
case 'album':
|
||||
return (this.props.spotify_library_albums && this.props.spotify_library_albums.indexOf(item.uri) > -1)
|
||||
break
|
||||
return (this.props.spotify_library_albums && this.props.spotify_library_albums.indexOf(item.uri) > -1);
|
||||
break;
|
||||
case 'playlist':
|
||||
return (this.props.spotify_library_playlists && this.props.spotify_library_playlists.indexOf(item.uri) > -1)
|
||||
break
|
||||
return (this.props.spotify_library_playlists && this.props.spotify_library_playlists.indexOf(item.uri) > -1);
|
||||
break;
|
||||
}
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -191,7 +191,7 @@ class ContextMenu extends React.Component{
|
||||
playQueueItem(e){
|
||||
this.props.uiActions.hideContextMenu()
|
||||
var tracks = this.props.menu.items;
|
||||
this.props.mopidyActions.changeTrack(tracks[0].tlid )
|
||||
this.props.mopidyActions.changeTrack(tracks[0].tlid);
|
||||
}
|
||||
|
||||
removeFromQueue(e){
|
||||
@ -201,102 +201,102 @@ class ContextMenu extends React.Component{
|
||||
for(var i = 0; i < tracks.length; i++){
|
||||
tracks_tlids.push(tracks[i].tlid );
|
||||
}
|
||||
this.props.mopidyActions.removeTracks(tracks_tlids )
|
||||
this.props.mopidyActions.removeTracks(tracks_tlids);
|
||||
}
|
||||
|
||||
playURIs(e){
|
||||
this.props.uiActions.hideContextMenu()
|
||||
this.props.mopidyActions.playURIs(this.props.menu.uris, this.props.menu.tracklist_uri)
|
||||
this.props.uiActions.hideContextMenu();
|
||||
this.props.mopidyActions.playURIs(this.props.menu.uris, this.props.menu.tracklist_uri);
|
||||
}
|
||||
|
||||
playPlaylist(e){
|
||||
this.props.uiActions.hideContextMenu()
|
||||
this.props.mopidyActions.playPlaylist(this.props.menu.uris[0])
|
||||
this.props.uiActions.hideContextMenu();
|
||||
this.props.mopidyActions.playPlaylist(this.props.menu.uris[0]);
|
||||
}
|
||||
|
||||
playArtistTopTracks(e){
|
||||
this.props.uiActions.hideContextMenu()
|
||||
this.props.spotifyActions.playArtistTopTracks(this.props.menu.uris[0])
|
||||
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)
|
||||
this.props.uiActions.hideContextMenu();
|
||||
this.props.mopidyActions.enqueueURIs(this.props.menu.uris, this.props.menu.tracklist_uri, next);
|
||||
}
|
||||
|
||||
addTracksToPlaylist(e, playlist_uri){
|
||||
this.props.uiActions.hideContextMenu()
|
||||
this.props.coreActions.addTracksToPlaylist(playlist_uri, this.props.menu.uris)
|
||||
this.props.uiActions.hideContextMenu();
|
||||
this.props.coreActions.addTracksToPlaylist(playlist_uri, this.props.menu.uris);
|
||||
}
|
||||
|
||||
toggleLoved(e, is_loved){
|
||||
this.props.uiActions.hideContextMenu()
|
||||
this.props.uiActions.hideContextMenu();
|
||||
if (is_loved){
|
||||
this.props.lastfmActions.unloveTrack(this.props.menu.items[0].uri)
|
||||
this.props.lastfmActions.unloveTrack(this.props.menu.items[0].uri);
|
||||
} else {
|
||||
this.props.lastfmActions.loveTrack(this.props.menu.items[0].uri)
|
||||
this.props.lastfmActions.loveTrack(this.props.menu.items[0].uri);
|
||||
}
|
||||
}
|
||||
|
||||
unloveTrack(e){
|
||||
this.props.uiActions.hideContextMenu()
|
||||
this.props.lastfmActions.unloveTrack(this.props.menu.items[0])
|
||||
this.props.uiActions.hideContextMenu();
|
||||
this.props.lastfmActions.unloveTrack(this.props.menu.items[0]);
|
||||
}
|
||||
|
||||
removeFromPlaylist(e){
|
||||
this.props.uiActions.hideContextMenu()
|
||||
this.props.coreActions.removeTracksFromPlaylist(this.props.menu.tracklist_uri, this.props.menu.indexes)
|
||||
this.props.uiActions.hideContextMenu();
|
||||
this.props.coreActions.removeTracksFromPlaylist(this.props.menu.tracklist_uri, this.props.menu.indexes);
|
||||
}
|
||||
|
||||
deletePlaylist(e){
|
||||
this.props.uiActions.hideContextMenu()
|
||||
this.props.coreActions.deletePlaylist(this.props.menu.uris[0])
|
||||
this.props.uiActions.hideContextMenu();
|
||||
this.props.coreActions.deletePlaylist(this.props.menu.uris[0]);
|
||||
}
|
||||
|
||||
startRadio(e){
|
||||
this.props.uiActions.hideContextMenu()
|
||||
this.props.pusherActions.startRadio(this.props.menu.uris)
|
||||
this.props.uiActions.hideContextMenu();
|
||||
this.props.pusherActions.startRadio(this.props.menu.uris);
|
||||
}
|
||||
|
||||
goToRecommendations(e){
|
||||
this.props.uiActions.hideContextMenu()
|
||||
var uris_string = helpers.arrayOf('uri',this.props.menu.items).join(',')
|
||||
hashHistory.push(global.baseURL +'discover/recommendations/'+ uris_string )
|
||||
this.props.uiActions.hideContextMenu();
|
||||
var uris_string = helpers.arrayOf('uri',this.props.menu.items).join(',');
|
||||
hashHistory.push(global.baseURL +'discover/recommendations/'+ uris_string );
|
||||
}
|
||||
|
||||
goToArtist(e){
|
||||
if (!this.props.menu.items || this.props.menu.items.length <= 0 || !this.props.menu.items[0].artists || this.props.menu.items[0].artists.length <= 0){
|
||||
return null
|
||||
return null;
|
||||
} else {
|
||||
this.props.uiActions.hideContextMenu()
|
||||
hashHistory.push(global.baseURL +'artist/'+ this.props.menu.items[0].artists[0].uri )
|
||||
this.props.uiActions.hideContextMenu();
|
||||
hashHistory.push(global.baseURL +'artist/'+ this.props.menu.items[0].artists[0].uri);
|
||||
}
|
||||
}
|
||||
|
||||
goToAlbum(e){
|
||||
if (!this.props.menu.items || this.props.menu.items.length <= 0 || !this.props.menu.items[0].album){
|
||||
return null
|
||||
return null;
|
||||
} else {
|
||||
this.props.uiActions.hideContextMenu()
|
||||
hashHistory.push(global.baseURL +'album/'+ this.props.menu.items[0].album.uri )
|
||||
this.props.uiActions.hideContextMenu();
|
||||
hashHistory.push(global.baseURL +'album/'+ this.props.menu.items[0].album.uri);
|
||||
}
|
||||
}
|
||||
|
||||
goToUser(e){
|
||||
if (!this.props.menu.items || this.props.menu.items.length <= 0){
|
||||
return null
|
||||
return null;
|
||||
} else {
|
||||
this.props.uiActions.hideContextMenu()
|
||||
hashHistory.push(global.baseURL +'user/'+ this.props.menu.items[0].owner.uri )
|
||||
this.props.uiActions.hideContextMenu();
|
||||
hashHistory.push(global.baseURL +'user/'+ this.props.menu.items[0].owner.uri);
|
||||
}
|
||||
}
|
||||
|
||||
goToTrack(e){
|
||||
if (!this.props.menu.items || this.props.menu.items.length <= 0){
|
||||
return null
|
||||
return null;
|
||||
} else {
|
||||
this.props.uiActions.hideContextMenu()
|
||||
hashHistory.push(global.baseURL +'track/'+ encodeURIComponent(this.props.menu.items[0].uri))
|
||||
this.props.uiActions.hideContextMenu();
|
||||
hashHistory.push(global.baseURL +'track/'+ encodeURIComponent(this.props.menu.items[0].uri));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -40,16 +40,27 @@ export default class DropdownField extends React.Component{
|
||||
}
|
||||
|
||||
render(){
|
||||
if (!this.props.options) return null
|
||||
|
||||
var classname = 'dropdown-field'
|
||||
if (this.state.expanded) classname += ' expanded'
|
||||
if (!this.props.options){
|
||||
return null;
|
||||
}
|
||||
|
||||
var className = 'dropdown-field';
|
||||
if (this.state.expanded){
|
||||
className += ' expanded';
|
||||
}
|
||||
if (this.props.no_status_icon){
|
||||
className += ' no-status-icon';
|
||||
}
|
||||
if (this.props.button){
|
||||
className += ' buttonify';
|
||||
}
|
||||
var current_value = this.props.options[0].value
|
||||
if (this.props.value) current_value = this.props.value
|
||||
if (this.props.value){
|
||||
current_value = this.props.value;
|
||||
}
|
||||
|
||||
var icon = <FontAwesome name="check" />
|
||||
if (typeof(this.props.reverse) !== 'undefined'){
|
||||
if (this.props.reverse !== undefined){
|
||||
if (this.props.reverse){
|
||||
icon = <FontAwesome className="reverse" name="caret-down" />
|
||||
} else {
|
||||
@ -58,8 +69,8 @@ export default class DropdownField extends React.Component{
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classname} data-key={this.props.name.replace(' ','_').toLowerCase()}>
|
||||
<div className="label" onClick={ () => this.handleToggle() }>
|
||||
<div className={className} data-key={this.props.name.replace(' ','_').toLowerCase()}>
|
||||
<div className={"label"+(this.props.button ? " button "+this.props.button : "")} onClick={ () => this.handleToggle() }>
|
||||
{this.props.icon ? <span><FontAwesome name={this.props.icon} /> </span> : null}
|
||||
<span className="text">{ this.props.name }</span>
|
||||
</div>
|
||||
@ -68,7 +79,7 @@ export default class DropdownField extends React.Component{
|
||||
this.props.options.map(option => {
|
||||
return (
|
||||
<div className="option" key={ option.value } onClick={ e => this.handleChange(option.value) }>
|
||||
{option.value == current_value ? icon : null}
|
||||
{!this.props.no_status_icon && (option.value == current_value) ? icon : null}
|
||||
{option.label}
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -791,4 +791,17 @@ export let getIndexedRecords = function(index, uris){
|
||||
}
|
||||
|
||||
return records
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Uppercase-ify the first character of a string
|
||||
*
|
||||
* @param string = string
|
||||
* @return string
|
||||
**/
|
||||
export let titleCase = function(string){
|
||||
return string.charAt(0).toUpperCase() + string.slice(1)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -653,7 +653,7 @@ export function getAutocompleteResults(field_id, query, types = ['album','artist
|
||||
response => {
|
||||
var genres = []
|
||||
if (genre_included){
|
||||
var available_genres = getState().ui.genres
|
||||
var available_genres = getState().spotify.genres
|
||||
if (available_genres){
|
||||
for (var i = 0; i < available_genres.length; i++){
|
||||
if (available_genres[i].includes(query)){
|
||||
@ -666,6 +666,35 @@ export function getAutocompleteResults(field_id, query, types = ['album','artist
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (response.artists && response.artists.items){
|
||||
dispatch({
|
||||
type: 'ARTISTS_LOADED',
|
||||
artists: response.artists.items
|
||||
});
|
||||
}
|
||||
|
||||
if (response.albums && response.albums.items){
|
||||
dispatch({
|
||||
type: 'ALBUMS_LOADED',
|
||||
albums: response.albums.items
|
||||
});
|
||||
}
|
||||
|
||||
if (response.playlists && response.playlists.items){
|
||||
dispatch({
|
||||
type: 'PLAYLISTS_LOADED',
|
||||
playlists: response.playlists.items
|
||||
});
|
||||
}
|
||||
|
||||
if (response.tracks && response.tracks.items){
|
||||
dispatch({
|
||||
type: 'TRACKS_LOADED',
|
||||
tracks: response.tracks.items
|
||||
});
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: 'SPOTIFY_AUTOCOMPLETE_LOADED',
|
||||
field_id: field_id,
|
||||
@ -873,7 +902,7 @@ export function getFavorites(limit = 50, term = 'long_term'){
|
||||
*
|
||||
* @param uris = array of artist or track URIs or a genre string
|
||||
**/
|
||||
export function getRecommendations(uris = [], limit = 20){
|
||||
export function getRecommendations(uris = [], limit = 20, tunabilities = null){
|
||||
return (dispatch, getState) => {
|
||||
|
||||
dispatch({type: 'CLEAR_SPOTIFY_RECOMMENDATIONS'});
|
||||
@ -907,11 +936,19 @@ export function getRecommendations(uris = [], limit = 20){
|
||||
}
|
||||
|
||||
// construct our endpoint URL with all the appropriate arguments
|
||||
var endpoint = 'recommendations'
|
||||
endpoint += '?seed_artists='+artists_ids.join(',')
|
||||
endpoint += '&seed_tracks='+tracks_ids.join(',')
|
||||
endpoint += '&seed_genres='+genres.join(',')
|
||||
endpoint += '&limit='+limit
|
||||
var endpoint = 'recommendations';
|
||||
endpoint += '?seed_artists='+artists_ids.join(',');
|
||||
endpoint += '&seed_tracks='+tracks_ids.join(',');
|
||||
endpoint += '&seed_genres='+genres.join(',');
|
||||
endpoint += '&limit='+limit;
|
||||
|
||||
if (tunabilities){
|
||||
for (var key in tunabilities){
|
||||
if (tunabilities.hasOwnProperty(key)){
|
||||
endpoint += '&'+key+'='+tunabilities[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sendRequest(dispatch, getState, endpoint)
|
||||
.then(
|
||||
@ -923,7 +960,7 @@ export function getRecommendations(uris = [], limit = 20){
|
||||
// anyway so we can proceed in the meantime
|
||||
var artists_uris = []
|
||||
if (tracks.length > artists_ids.length && tracks.length > 10){
|
||||
while (artists_uris.length < 5){
|
||||
while (artists_uris.length < 6){
|
||||
var random_index = Math.round(Math.random() * (tracks.length - 1))
|
||||
var artist = tracks[random_index].artists[0]
|
||||
|
||||
@ -940,7 +977,7 @@ export function getRecommendations(uris = [], limit = 20){
|
||||
var albums = []
|
||||
var albums_uris = []
|
||||
if (tracks.length > 10){
|
||||
while (albums.length < 5){
|
||||
while (albums.length < 6){
|
||||
var random_index = Math.round(Math.random() * (tracks.length - 1))
|
||||
var album = tracks[random_index].album
|
||||
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
|
||||
import ReactGA from 'react-ga'
|
||||
import ReactGA from 'react-ga';
|
||||
import md5 from 'md5';
|
||||
|
||||
var helpers = require('./../../helpers')
|
||||
var spotifyActions = require('./actions')
|
||||
var uiActions = require('../ui/actions')
|
||||
var pusherActions = require('../pusher/actions')
|
||||
var helpers = require('./../../helpers');
|
||||
var spotifyActions = require('./actions');
|
||||
var uiActions = require('../ui/actions');
|
||||
var pusherActions = require('../pusher/actions');
|
||||
|
||||
const SpotifyMiddleware = (function(){
|
||||
|
||||
@ -17,15 +18,12 @@ const SpotifyMiddleware = (function(){
|
||||
switch(action.type){
|
||||
|
||||
case 'SPOTIFY_CONNECTED':
|
||||
var label = null
|
||||
var hashed_username = null
|
||||
if (store.getState().spotify.me){
|
||||
if (store.getState().core.anonymise_analytics){
|
||||
label = "anonymised_"+store.getState().pusher.client_id;
|
||||
} else {
|
||||
label = store.getState().spotify.me.id;
|
||||
}
|
||||
hashed_username = md5(store.getState().spotify.me.id);
|
||||
ReactGA.set({userId: hashed_username});
|
||||
}
|
||||
ReactGA.event({ category: 'Spotify', action: 'Connected', label: label })
|
||||
ReactGA.event({category: 'Spotify', action: 'Connected', label: hashed_username})
|
||||
|
||||
// TODO: remove this so we don't tap out our API limits before we even get started
|
||||
// Perhaps fire this on demand? Context menu, playlists loading or AddToPlaylistModal
|
||||
@ -37,7 +35,7 @@ const SpotifyMiddleware = (function(){
|
||||
break
|
||||
|
||||
case 'SPOTIFY_AUTHORIZATION_GRANTED':
|
||||
ReactGA.event({ category: 'Spotify', action: 'Authorization granted'});
|
||||
ReactGA.event({category: 'Spotify', action: 'Authorization granted'});
|
||||
|
||||
// Flush out the previous user's library
|
||||
store.dispatch(spotifyActions.flushLibrary());
|
||||
@ -46,15 +44,12 @@ const SpotifyMiddleware = (function(){
|
||||
break;
|
||||
|
||||
case 'SPOTIFY_AUTHORIZATION_REVOKED':
|
||||
var label = null
|
||||
var hashed_username = null
|
||||
if (store.getState().spotify.me){
|
||||
if (store.getState().core.anonymise_analytics){
|
||||
label = "anonymised_"+store.getState().pusher.client_id;
|
||||
} else {
|
||||
label = store.getState().spotify.me.id;
|
||||
}
|
||||
hashed_username = md5(store.getState().spotify.me);
|
||||
ReactGA.set({userId: hashed_username});
|
||||
}
|
||||
ReactGA.event({ category: 'Spotify', action: 'Authorization revoked', label: label})
|
||||
ReactGA.event({ category: 'Spotify', action: 'Authorization revoked', label: hashed_username})
|
||||
next(action)
|
||||
|
||||
// Now dispatch a getMe to get the backend-provided user
|
||||
@ -66,15 +61,12 @@ const SpotifyMiddleware = (function(){
|
||||
break;
|
||||
|
||||
case 'SPOTIFY_IMPORT_AUTHORIZATION':
|
||||
var label = null;
|
||||
if (action.me && action.me.id){
|
||||
if (store.getState().core.anonymise_analytics){
|
||||
label = "anonymised_"+store.getState().pusher.client_id;
|
||||
} else {
|
||||
label = action.me.id;
|
||||
}
|
||||
var hashed_username = null
|
||||
if (store.getState().spotify.me){
|
||||
hashed_username = md5(store.getState().spotify.me);
|
||||
ReactGA.set({userId: hashed_username});
|
||||
}
|
||||
ReactGA.event({ category: 'Spotify', action: 'Authorization imported', label: label });
|
||||
ReactGA.event({category: 'Spotify', action: 'Authorization imported', label: hashed_username});
|
||||
|
||||
// Flush out the previous user's library
|
||||
store.dispatch(spotifyActions.flushLibrary());
|
||||
@ -435,14 +427,9 @@ const SpotifyMiddleware = (function(){
|
||||
store.dispatch(pusherActions.setUsername(name));
|
||||
}
|
||||
|
||||
var label = null;
|
||||
if (store.getState().core.anonymise_analytics){
|
||||
label = "anonymised_"+store.getState().pusher.client_id;
|
||||
} else {
|
||||
ReactGA.set({userId: action.data.id});
|
||||
label = action.data.id;
|
||||
}
|
||||
ReactGA.event({category: 'Spotify', action: 'Authorization verified', label: label});
|
||||
var hashed_username = md5(action.data.id);
|
||||
ReactGA.set({userId: hashed_username});
|
||||
ReactGA.event({category: 'Spotify', action: 'Authorization verified', label: hashed_username});
|
||||
|
||||
store.dispatch({
|
||||
type: 'USERS_LOADED',
|
||||
|
||||
@ -162,6 +162,14 @@ export default function reducer(spotify = {}, action){
|
||||
autocomplete_results: autocomplete_results
|
||||
})
|
||||
|
||||
case 'SPOTIFY_GENRES_LOADED':
|
||||
return Object.assign(
|
||||
{},
|
||||
spotify,
|
||||
{
|
||||
genres: action.genres
|
||||
})
|
||||
|
||||
|
||||
/**
|
||||
* Library
|
||||
|
||||
@ -427,17 +427,6 @@ class Settings extends React.Component {
|
||||
<span className="tooltip">Playing one or more URIs will clear the current play queue first</span>
|
||||
</span>
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="log_actions"
|
||||
checked={ this.props.core.anonymise_analytics }
|
||||
onChange={ e => this.props.coreActions.set({ anonymise_analytics: !this.props.core.anonymise_analytics })} />
|
||||
<span className="label has-tooltip">
|
||||
Exclude personal data from collection
|
||||
<span className="tooltip">Personal data used for debugging Iris is not collected by Google Analytics</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -480,7 +469,7 @@ class Settings extends React.Component {
|
||||
<em><a href="https://github.com/jaedb/Iris" target="_blank">Iris</a></em> is an open-source project by <a href="https://github.com/jaedb" target="_blank">James Barnsley</a>. It is provided free and with absolutely no warranty. If you paid someone for this software, please let me know.
|
||||
<br />
|
||||
<br />
|
||||
Google Analytics is used to help trace issues and provide valuable insight into how we can continue to make improvements. This may include personal information (eg Spotify Username). For more information, see <a href="https://github.com/jaedb/Iris/wiki/Terms-of-use" target="_blank">terms and conditions</a>.
|
||||
Google Analytics is used to collect usage data and errors to help trace issues and provide valuable insight into how we can continue to make improvements. Personal information is anonymized prior to collection. For more information, see <a href="https://github.com/jaedb/Iris/wiki/Terms-of-use" target="_blank">terms and conditions</a>.
|
||||
<br />
|
||||
</div>
|
||||
<br /><br />
|
||||
|
||||
@ -3,17 +3,24 @@ import React, { PropTypes } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { bindActionCreators } from 'redux'
|
||||
import FontAwesome from 'react-fontawesome'
|
||||
import InputRange from 'react-input-range';
|
||||
|
||||
import Header from '../../components/Header'
|
||||
import ArtistSentence from '../../components/ArtistSentence'
|
||||
import ArtistGrid from '../../components/ArtistGrid'
|
||||
import AlbumGrid from '../../components/AlbumGrid'
|
||||
import TrackList from '../../components/TrackList'
|
||||
import Thumbnail from '../../components/Thumbnail'
|
||||
import Parallax from '../../components/Parallax'
|
||||
import DropdownField from '../../components/DropdownField'
|
||||
import AddSeedField from '../../components/AddSeedField'
|
||||
import URILink from '../../components/URILink'
|
||||
import ContextMenuTrigger from '../../components/ContextMenuTrigger'
|
||||
import RelatedArtists from '../../components/RelatedArtists'
|
||||
|
||||
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 Discover extends React.Component{
|
||||
@ -24,11 +31,121 @@ class Discover extends React.Component{
|
||||
this._autocomplete_timer = false
|
||||
|
||||
this.state = {
|
||||
seeds: [
|
||||
'spotify:genre:chill'
|
||||
],
|
||||
add_seed: '',
|
||||
adding_seed: false
|
||||
adding_seed: false,
|
||||
seeds: [],
|
||||
tunabilities: {
|
||||
acousticness: {
|
||||
enabled: false,
|
||||
convert_to_decimal: true,
|
||||
min: 0,
|
||||
max: 100,
|
||||
value: {
|
||||
min: 25,
|
||||
max: 75
|
||||
}
|
||||
},
|
||||
danceability: {
|
||||
enabled: false,
|
||||
convert_to_decimal: true,
|
||||
min: 0,
|
||||
max: 100,
|
||||
value: {
|
||||
min: 25,
|
||||
max: 75
|
||||
}
|
||||
},
|
||||
energy: {
|
||||
enabled: false,
|
||||
convert_to_decimal: true,
|
||||
min: 0,
|
||||
max: 100,
|
||||
value: {
|
||||
min: 25,
|
||||
max: 75
|
||||
}
|
||||
},
|
||||
instrumentalness: {
|
||||
enabled: false,
|
||||
convert_to_decimal: true,
|
||||
min: 0,
|
||||
max: 100,
|
||||
value: {
|
||||
min: 25,
|
||||
max: 75
|
||||
}
|
||||
},
|
||||
key: {
|
||||
enabled: false,
|
||||
min: 0,
|
||||
max: 11,
|
||||
value: {
|
||||
min: 3,
|
||||
max: 8
|
||||
}
|
||||
},
|
||||
liveness: {
|
||||
enabled: false,
|
||||
convert_to_decimal: true,
|
||||
min: 0,
|
||||
max: 100,
|
||||
value: {
|
||||
min: 25,
|
||||
max: 75
|
||||
}
|
||||
},
|
||||
loudness: {
|
||||
enabled: false,
|
||||
convert_to_decimal: true,
|
||||
min: 0,
|
||||
max: 100,
|
||||
value: {
|
||||
min: 25,
|
||||
max: 75
|
||||
}
|
||||
},
|
||||
popularity: {
|
||||
enabled: true,
|
||||
min: 0,
|
||||
max: 100,
|
||||
value: {
|
||||
min: 0,
|
||||
max: 100
|
||||
}
|
||||
},
|
||||
speechiness: {
|
||||
enabled: false,
|
||||
convert_to_decimal: true,
|
||||
description: "The presence of spoken words in a track",
|
||||
min: 0,
|
||||
max: 100,
|
||||
value: {
|
||||
min: 25,
|
||||
max: 75
|
||||
}
|
||||
},
|
||||
tempo: {
|
||||
enabled: false,
|
||||
convert_to_decimal: true,
|
||||
min: 0,
|
||||
max: 100,
|
||||
value: {
|
||||
min: 25,
|
||||
max: 75
|
||||
}
|
||||
},
|
||||
valence: {
|
||||
enabled: false,
|
||||
convert_to_decimal: true,
|
||||
description: "The musical positiveness conveyed by a track",
|
||||
min: 0,
|
||||
max: 100,
|
||||
value: {
|
||||
min: 25,
|
||||
max: 75
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -36,12 +153,8 @@ class Discover extends React.Component{
|
||||
|
||||
// We have seeds provided in the URL
|
||||
if (this.props.params.seeds){
|
||||
this.handleURLSeeds(this.props.params.seeds)
|
||||
|
||||
// BAU
|
||||
} else {
|
||||
this.props.spotifyActions.getFavorites()
|
||||
}
|
||||
this.handleURLSeeds(this.props.params.seeds);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(newProps, newState){
|
||||
@ -50,17 +163,38 @@ class Discover extends React.Component{
|
||||
if (newProps.params.seeds != this.props.params.seeds){
|
||||
this.handleURLSeeds(newProps.params.seeds)
|
||||
}
|
||||
}
|
||||
|
||||
// When we've loaded favorite_artists.
|
||||
// This indirectly listens for when the action has
|
||||
// loaded new data.
|
||||
if (this.props.favorite_artists.length <= 0 && newProps.favorite_artists.length > 0){
|
||||
var initial_seeds = newProps.favorite_artists.sort(() => .5 - Math.random())
|
||||
initial_seeds = initial_seeds.slice(0,2)
|
||||
handleContextMenu(e){
|
||||
|
||||
this.setState({seeds: initial_seeds})
|
||||
this.getRecommendations(initial_seeds)
|
||||
var uri = 'iris:discover';
|
||||
if (this.state.seeds){
|
||||
uri += ':';
|
||||
for (var i = 0; i < this.state.seeds.length; i++){
|
||||
if (i > 0){
|
||||
uri += ',';
|
||||
}
|
||||
uri += this.state.seeds[i].split(':').join('_');
|
||||
}
|
||||
}
|
||||
|
||||
var tracks = [];
|
||||
if (this.props.recommendations.tracks_uris && this.props.tracks){
|
||||
for (var i = 0; i < this.props.recommendations.tracks_uris.length; i++){
|
||||
var uri = this.props.recommendations.tracks_uris[i];
|
||||
if (this.props.tracks.hasOwnProperty(uri)){
|
||||
tracks.push(this.props.tracks[uri]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var data = {
|
||||
e: e,
|
||||
context: 'track',
|
||||
items: tracks,
|
||||
uris: helpers.arrayOf('uri',tracks)
|
||||
}
|
||||
this.props.uiActions.showContextMenu(data);
|
||||
}
|
||||
|
||||
handleURLSeeds(seeds_string = this.props.params.seeds){
|
||||
@ -87,24 +221,43 @@ class Discover extends React.Component{
|
||||
this.getRecommendations(seeds)
|
||||
}
|
||||
|
||||
getRecommendations(seeds = this.state.seeds){
|
||||
getRecommendations(seeds = this.state.seeds, tunabilities = this.state.tunabilities){
|
||||
if (seeds.length > 0){
|
||||
this.props.spotifyActions.getRecommendations(seeds, 50)
|
||||
var digested_tunabilities = {};
|
||||
for (var key in tunabilities){
|
||||
if (tunabilities.hasOwnProperty(key) && tunabilities[key].enabled){
|
||||
var tunability = tunabilities[key];
|
||||
|
||||
var max = tunability.value.max;
|
||||
var min = tunability.value.min;
|
||||
|
||||
if (tunability.convert_to_decimal){
|
||||
max = max / 100;
|
||||
min = min / 100;
|
||||
}
|
||||
|
||||
digested_tunabilities[key+"_max"] = max.toString();
|
||||
digested_tunabilities[key+"_min"] = min.toString();
|
||||
}
|
||||
}
|
||||
this.props.spotifyActions.getRecommendations(seeds, 50, digested_tunabilities)
|
||||
}
|
||||
}
|
||||
|
||||
playTracks(){
|
||||
this.props.mopidyActions.playURIs(this.props.recommendations.tracks_uris);
|
||||
}
|
||||
|
||||
removeSeed(index){
|
||||
var seeds = this.state.seeds
|
||||
seeds.splice(index,1)
|
||||
this.setState({seeds: seeds})
|
||||
this.getRecommendations(seeds)
|
||||
var seeds = this.state.seeds;
|
||||
seeds.splice(index,1);
|
||||
this.setState({seeds: seeds});
|
||||
}
|
||||
|
||||
handleSelect(e,uri){
|
||||
var seeds = this.state.seeds
|
||||
seeds.push(uri)
|
||||
this.setState({seeds: seeds})
|
||||
this.getRecommendations(seeds)
|
||||
var seeds = this.state.seeds;
|
||||
seeds.push(uri);
|
||||
this.setState({seeds: seeds});
|
||||
}
|
||||
|
||||
renderSeeds(){
|
||||
@ -118,7 +271,7 @@ class Discover extends React.Component{
|
||||
|
||||
case 'track':
|
||||
if (typeof(this.props.tracks[uri]) !== 'undefined'){
|
||||
seeds_objects.push(this.props.tracks[uri])
|
||||
seeds_objects.push(this.props.tracks[uri]);
|
||||
} else {
|
||||
seeds_objects.push({
|
||||
name: 'Loading...',
|
||||
@ -129,7 +282,7 @@ class Discover extends React.Component{
|
||||
|
||||
case 'artist':
|
||||
if (typeof(this.props.artists[uri]) !== 'undefined'){
|
||||
seeds_objects.push(this.props.artists[uri])
|
||||
seeds_objects.push(this.props.artists[uri]);
|
||||
} else {
|
||||
seeds_objects.push({
|
||||
name: 'Loading...',
|
||||
@ -155,38 +308,100 @@ class Discover extends React.Component{
|
||||
seeds_objects.map((seed,index) => {
|
||||
var type = helpers.uriType(seed.uri)
|
||||
return (
|
||||
<span className="seed" key={seed.uri}>
|
||||
{seed.name}
|
||||
<URILink className="type" type={type} uri={seed.uri}>({type})</URILink>
|
||||
<FontAwesome name="close" className="remove" onClick={() => this.removeSeed(index)} />
|
||||
</span>
|
||||
<div className={"seed"+(seed.images ? " has-thumbnail" : "")} key={seed.uri}>
|
||||
{seed.images ? <URILink className="thumbnail-wrapper" type={type} uri={seed.uri}><Thumbnail images={seed.images} circle={seed.type == "artist"} size="small" /></URILink> : null}
|
||||
<div className="label">
|
||||
{helpers.titleCase(type)}
|
||||
<FontAwesome name="close" className="remove" onClick={() => this.removeSeed(index)} />
|
||||
</div>
|
||||
<div className="name">{seed.name}</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
<AddSeedField onSelect={(e,uri) => this.handleSelect(e,uri)} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderResults(){
|
||||
if (helpers.isLoading(this.props.load_queue,['spotify_recommendations'])){
|
||||
return (
|
||||
<div className="body-loader loading">
|
||||
<div className="loader"></div>
|
||||
</div>
|
||||
)
|
||||
setTunability(name, value){
|
||||
var tunabilities = this.state.tunabilities;
|
||||
tunabilities[name].value = value;
|
||||
this.setState({ tunabilities: tunabilities });
|
||||
}
|
||||
|
||||
toggleTunability(name){
|
||||
var tunabilities = this.state.tunabilities;
|
||||
tunabilities[name].enabled = !tunabilities[name].enabled;
|
||||
this.setState({ tunabilities: tunabilities });
|
||||
}
|
||||
|
||||
renderTunabilities(){
|
||||
var addable_tunabilities = [];
|
||||
var enabled_tunabilities = [];
|
||||
for (var key in this.state.tunabilities){
|
||||
if (this.state.tunabilities.hasOwnProperty(key)){
|
||||
|
||||
var tunability = Object.assign(
|
||||
{},
|
||||
this.state.tunabilities[key],
|
||||
{
|
||||
name: key
|
||||
}
|
||||
);
|
||||
|
||||
if (tunability.enabled){
|
||||
enabled_tunabilities.push(tunability);
|
||||
} else {
|
||||
addable_tunabilities.push({
|
||||
label: helpers.titleCase(tunability.name),
|
||||
value: tunability.name
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.props.recommendations || typeof(this.props.recommendations.albums_uris) === 'undefined' || typeof(this.props.recommendations.artists_uris) === 'undefined'){
|
||||
|
||||
return (
|
||||
<div className="tunabilities">
|
||||
{
|
||||
enabled_tunabilities.map(tunability => {
|
||||
return (
|
||||
<div className="field tunability range" key={tunability.name}>
|
||||
<div className="label">
|
||||
{helpers.titleCase(tunability.name)}
|
||||
<span className="remove" onClick={e => this.toggleTunability(tunability.name)}>
|
||||
<FontAwesome name="close" />
|
||||
</span>
|
||||
</div>
|
||||
<div className="input">
|
||||
<InputRange
|
||||
disabled={!tunability.enabled}
|
||||
minValue={tunability.min}
|
||||
maxValue={tunability.max}
|
||||
value={tunability.value}
|
||||
onChange={value => this.setTunability(tunability.name, value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderResults(){
|
||||
|
||||
// Results not in
|
||||
if (!this.props.recommendations || this.props.recommendations.albums_uris === undefined || this.props.recommendations.artists_uris === undefined){
|
||||
return null;
|
||||
}
|
||||
|
||||
var albums = [];
|
||||
if (this.props.recommendations.albums_uris && this.props.albums){
|
||||
for (var i = 0; i < this.props.recommendations.albums_uris.length; i++){
|
||||
var uri = this.props.recommendations.albums_uris[i];
|
||||
if (this.props.albums.hasOwnProperty(uri)){
|
||||
albums.push(this.props.albums[uri]);
|
||||
var tracks = [];
|
||||
if (this.props.recommendations.tracks_uris && this.props.tracks){
|
||||
for (var i = 0; i < this.props.recommendations.tracks_uris.length; i++){
|
||||
var uri = this.props.recommendations.tracks_uris[i];
|
||||
if (this.props.tracks.hasOwnProperty(uri)){
|
||||
tracks.push(this.props.tracks[uri]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -201,40 +416,86 @@ class Discover extends React.Component{
|
||||
}
|
||||
}
|
||||
|
||||
var tracks = [];
|
||||
if (this.props.recommendations.tracks_uris && this.props.tracks){
|
||||
for (var i = 0; i < this.props.recommendations.tracks_uris.length; i++){
|
||||
var uri = this.props.recommendations.tracks_uris[i];
|
||||
if (this.props.tracks.hasOwnProperty(uri)){
|
||||
tracks.push(this.props.tracks[uri]);
|
||||
var albums = [];
|
||||
if (this.props.recommendations.albums_uris && this.props.albums){
|
||||
for (var i = 0; i < this.props.recommendations.albums_uris.length; i++){
|
||||
var uri = this.props.recommendations.albums_uris[i];
|
||||
if (this.props.albums.hasOwnProperty(uri)){
|
||||
albums.push(this.props.albums[uri]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Complete records not yet in our index
|
||||
if (tracks.length <= 0 && artists.length <= 0 && albums.length <= 0){
|
||||
return null;
|
||||
}
|
||||
|
||||
var uri = 'iris:discover';
|
||||
if (this.props.params.seeds){
|
||||
uri += ':'+this.props.params.seeds.split(':').join('_');
|
||||
if (this.state.seeds){
|
||||
uri += ':';
|
||||
for (var i = 0; i < this.state.seeds.length; i++){
|
||||
if (i > 0){
|
||||
uri += ',';
|
||||
}
|
||||
uri += this.state.seeds[i].split(':').join('_');
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="content-wrapper recommendations-results">
|
||||
<section>
|
||||
<h4>Artists</h4>
|
||||
<ArtistGrid single_row artists={artists} />
|
||||
</section>
|
||||
<section>
|
||||
<h4>Albums</h4>
|
||||
<AlbumGrid single_row albums={albums} />
|
||||
</section>
|
||||
<section>
|
||||
<h4>Tracks</h4>
|
||||
<div className="content-wrapper recommendations-results cf">
|
||||
|
||||
<section className="col w70 tracks">
|
||||
<h4>
|
||||
Tracks
|
||||
<ContextMenuTrigger onTrigger={e => this.handleContextMenu(e)} />
|
||||
<button className="primary pull-right" onClick={e => this.playTracks(e)}>Play all</button>
|
||||
</h4>
|
||||
<TrackList className="discover-track-list" uri={uri} tracks={tracks} />
|
||||
</section>
|
||||
|
||||
<div className="col w5"></div>
|
||||
|
||||
<div className="col w25 others">
|
||||
<section>
|
||||
<h4>Artists</h4>
|
||||
<RelatedArtists artists={artists} />
|
||||
</section>
|
||||
<br />
|
||||
<br />
|
||||
<section>
|
||||
<h4>Albums</h4>
|
||||
<AlbumGrid className="mini" albums={albums} />
|
||||
</section>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render(){
|
||||
var is_loading = helpers.isLoading(this.props.load_queue,['spotify_recommendations']);
|
||||
var addable_tunabilities = [];
|
||||
for (var key in this.state.tunabilities){
|
||||
if (this.state.tunabilities.hasOwnProperty(key)){
|
||||
|
||||
var tunability = Object.assign(
|
||||
{},
|
||||
this.state.tunabilities[key],
|
||||
{
|
||||
name: key
|
||||
}
|
||||
);
|
||||
|
||||
if (!tunability.enabled){
|
||||
addable_tunabilities.push({
|
||||
label: helpers.titleCase(tunability.name),
|
||||
value: tunability.name
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="view discover-view">
|
||||
<div className="intro">
|
||||
@ -244,10 +505,25 @@ class Discover extends React.Component{
|
||||
<div className="liner">
|
||||
<h1>Explore new music</h1>
|
||||
<h2 className="grey-text">
|
||||
Add seeds below to build your sound
|
||||
{!this.props.params.seeds ? ". Let's start with two of your favorite artists." : null}
|
||||
Add seeds and musical properties below to build your sound
|
||||
</h2>
|
||||
{this.renderSeeds()}
|
||||
<div className="parameters">
|
||||
|
||||
{this.renderSeeds()}
|
||||
{this.renderTunabilities()}
|
||||
{this.state.seeds.length > 5 ? <p className="message error">Too many seeds! You can use up to a total of 5 seed tracks, artists and genres.</p> : null}
|
||||
|
||||
</div>
|
||||
<div className="actions">
|
||||
|
||||
<AddSeedField onSelect={(e,uri) => this.handleSelect(e,uri)} />
|
||||
<DropdownField className="add-properties" name="Properties" options={addable_tunabilities} no_status_icon button="default" handleChange={val => {this.toggleTunability(val)}} />
|
||||
<span className={"submit button primary large"+(is_loading ? " working" : "")} onClick={e => this.getRecommendations()}>
|
||||
<FontAwesome name="compass" />
|
||||
Find recommendations
|
||||
</span>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@ -275,14 +551,14 @@ const mapStateToProps = (state, ownProps) => {
|
||||
authorized: state.spotify.authorization,
|
||||
load_queue: state.ui.load_queue,
|
||||
quick_search_results: (state.spotify.quick_search_results ? state.spotify.quick_search_results : {artists: [], tracks: []}),
|
||||
recommendations: (state.spotify.recommendations ? state.spotify.recommendations : {}),
|
||||
favorite_artists: (state.spotify.favorite_artists ? state.spotify.favorite_artists : []),
|
||||
favorite_tracks: (state.spotify.favorite_tracks ? state.spotify.favorite_tracks : [])
|
||||
recommendations: (state.spotify.recommendations ? state.spotify.recommendations : {})
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
uiActions: bindActionCreators(uiActions, dispatch),
|
||||
mopidyActions: bindActionCreators(mopidyActions, dispatch),
|
||||
spotifyActions: bindActionCreators(spotifyActions, dispatch)
|
||||
}
|
||||
}
|
||||
|
||||
@ -38,3 +38,4 @@
|
||||
@import 'views/discover';
|
||||
@import 'views/settings';
|
||||
|
||||
@import "../../node_modules/react-input-range/lib/css/index.css";
|
||||
@ -291,6 +291,11 @@
|
||||
border-radius: 3px;
|
||||
text-align: center;
|
||||
|
||||
h4 &,
|
||||
h4 &:active {
|
||||
margin: -8px 0 0 10px;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
|
||||
@ -6,9 +6,9 @@
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
|
||||
.label {
|
||||
.label:not(.button) {
|
||||
@include animate();
|
||||
padding: 14px 10px;
|
||||
padding: 14px 10px;
|
||||
cursor: pointer;
|
||||
|
||||
.notouch &:hover {
|
||||
@ -17,7 +17,7 @@
|
||||
}
|
||||
|
||||
&.expanded {
|
||||
.label {
|
||||
.label:not(.button) {
|
||||
color: $blue;
|
||||
}
|
||||
.options {
|
||||
@ -25,6 +25,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.no-status-icon {
|
||||
.options {
|
||||
.option {
|
||||
padding-left: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.options {
|
||||
color: $black;
|
||||
position: absolute;
|
||||
|
||||
@ -75,7 +75,7 @@
|
||||
@include responsive(null, 1900px){
|
||||
&.mini {
|
||||
.grid-item {
|
||||
@include grid_item(3 )
|
||||
@include grid_item(3)
|
||||
}
|
||||
&.single-row {
|
||||
.grid-item:nth-child(1n+4){
|
||||
|
||||
@ -46,6 +46,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.info {
|
||||
.fa {
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
&.large-tooltip {
|
||||
.tooltip {
|
||||
padding: 8px 12px;
|
||||
@ -54,4 +60,4 @@
|
||||
bottom: 50px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -388,6 +388,7 @@ footer {
|
||||
color: $white;
|
||||
}
|
||||
|
||||
&.error,
|
||||
&.bad {
|
||||
background: $red;
|
||||
color: $white;
|
||||
@ -410,6 +411,35 @@ footer {
|
||||
}
|
||||
}
|
||||
|
||||
.message {
|
||||
padding: 10px 12px 8px;
|
||||
background: $yellow;
|
||||
color: $dark_grey;
|
||||
clear: both;
|
||||
font-size: 14px;
|
||||
border-radius: 3px;
|
||||
|
||||
&.info {
|
||||
background: $yellow;
|
||||
}
|
||||
|
||||
&.notice {
|
||||
background: $blue;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
background: $orange;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
&.error,
|
||||
&.bad {
|
||||
background: $red;
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@include responsive($bp_medium){
|
||||
|
||||
@ -99,6 +99,19 @@ input[type="submit"] {
|
||||
margin-bottom: 9px;
|
||||
}
|
||||
|
||||
&.small {
|
||||
padding: 6px 18px 4px;
|
||||
}
|
||||
|
||||
&.large {
|
||||
padding: 16px 50px 14px;
|
||||
}
|
||||
|
||||
&.pull-right {
|
||||
margin-right: 0;
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
&.primary {
|
||||
background: $turquoise;
|
||||
color: $white;
|
||||
@ -155,6 +168,8 @@ input[type="submit"] {
|
||||
&.working {
|
||||
position: relative;
|
||||
cursor: wait !important;
|
||||
background: $grey;
|
||||
color: $mid_grey;
|
||||
|
||||
&:after {
|
||||
pointer-events: none;
|
||||
@ -162,7 +177,7 @@ input[type="submit"] {
|
||||
display: block;
|
||||
content: '';
|
||||
animation: slideloader 1s infinite;
|
||||
background: rgba(200,200,200,0.4);
|
||||
background: rgba(200,200,200,0.3);
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
@ -238,7 +253,7 @@ input[type="submit"] {
|
||||
display: inline-block;
|
||||
float: none;
|
||||
width: auto;
|
||||
padding: 5px 30px 5px 30px;
|
||||
padding: 5px 5px 5px 26px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
@ -284,6 +299,59 @@ input[type="submit"] {
|
||||
}
|
||||
}
|
||||
|
||||
&.range {
|
||||
position: relative;
|
||||
|
||||
.input-range {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.label {
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
|
||||
.input-range__track,
|
||||
.input-range__slider-container {
|
||||
transition: none;
|
||||
height: 6px;
|
||||
border-radius: 3px;
|
||||
cursor: col-resize;
|
||||
}
|
||||
|
||||
.input-range__track--background {
|
||||
margin: 0;
|
||||
background: rgba(100,100,100,0.5);
|
||||
}
|
||||
|
||||
.input-range__track--active {
|
||||
background: $blue;
|
||||
}
|
||||
|
||||
.input-range__slider-container {
|
||||
.input-range__slider {
|
||||
cursor: col-resize;
|
||||
background: $blue;
|
||||
border: none;
|
||||
top: 0;
|
||||
height: 6px;
|
||||
width: 20px;
|
||||
margin: -6px 0 0 0;
|
||||
border-radius: 3px 0 0 3px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
.input-range__slider {
|
||||
border-radius: 0 3px 3px 0;
|
||||
margin-left: -12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.sub-field {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.description {
|
||||
display: inline-block;
|
||||
padding: 10px 14px;
|
||||
|
||||
@ -117,9 +117,21 @@ $bp_shallow: 650px;
|
||||
}
|
||||
|
||||
@keyframes slideloader {
|
||||
0% { left: 0%; right: 100%; }
|
||||
50% { left: 0%; right: 0%; }
|
||||
100% { left: 100%; right: 0%; }
|
||||
0% {
|
||||
left: 0%;
|
||||
right: 100%;
|
||||
border-radius: 3px 0 0 3px;
|
||||
}
|
||||
50% {
|
||||
left: 0%;
|
||||
right: 0%;
|
||||
border-radius: 3px;
|
||||
}
|
||||
100% {
|
||||
left: 100%;
|
||||
right: 0%;
|
||||
border-radius: 0 3px 3px 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -28,74 +28,153 @@
|
||||
h2 {
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
padding-bottom: 40px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.parameters {
|
||||
@include clearfix();
|
||||
padding-top: 50px;
|
||||
|
||||
.seeds,
|
||||
.tunabilities {
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
|
||||
.seeds {
|
||||
padding-top: 40px;
|
||||
|
||||
.seed {
|
||||
display: inline-block;
|
||||
background: $blue;
|
||||
color: $white;
|
||||
padding: 8px 26px 8px 14px;
|
||||
font-size: 16px;
|
||||
position: relative;
|
||||
vertical-align: top;
|
||||
margin: 0 10px 10px 0;
|
||||
border-radius: 3px;
|
||||
color: $white;
|
||||
padding: 0 30px 30px 0;
|
||||
float: left;
|
||||
|
||||
.type {
|
||||
display: inline-block;
|
||||
font-size: 12px;
|
||||
opacity: 0.5;
|
||||
padding: 0 10px 0 5px;
|
||||
.thumbnail-wrapper {
|
||||
float: left;
|
||||
width: 44px;
|
||||
border: 0 !important;
|
||||
}
|
||||
|
||||
.remove {
|
||||
font-size: 10px;
|
||||
padding: 12px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
cursor: pointer;
|
||||
.label {
|
||||
.remove {
|
||||
@include animate();
|
||||
cursor: pointer;
|
||||
font-size: 10px;
|
||||
padding: 4px;
|
||||
margin-left: 4px;
|
||||
|
||||
&:hover {
|
||||
color: $red;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 16px;
|
||||
padding-top: 3px;
|
||||
}
|
||||
|
||||
&.has-thumbnail {
|
||||
.label,
|
||||
.name {
|
||||
padding-left: 52px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.autocomplete-field {
|
||||
width: 250px;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
padding: 0;
|
||||
.tunabilities {
|
||||
@include clearfix();
|
||||
|
||||
.tunability {
|
||||
width: 200px;
|
||||
float: left;
|
||||
box-sizing: border-box;
|
||||
padding: 0 30px 30px 0;
|
||||
position: relative;
|
||||
|
||||
.input {
|
||||
float: none;
|
||||
margin: 0 10px 10px 0;
|
||||
.input {
|
||||
float: none;
|
||||
width: 100%;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
input {
|
||||
background: rgba(100,100,100,0.5);
|
||||
color: $white;
|
||||
padding: 7px 13px;
|
||||
font-size: 16px;
|
||||
border: 1px solid transparent;
|
||||
|
||||
&:focus,
|
||||
&:active {
|
||||
border-color: $blue;
|
||||
.input-range {
|
||||
.input-range__label-container {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:after {
|
||||
background: $blue;
|
||||
.remove {
|
||||
@include animate();
|
||||
cursor: pointer;
|
||||
font-size: 10px;
|
||||
padding: 4px;
|
||||
margin-left: 4px;
|
||||
|
||||
&:hover {
|
||||
color: $red;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
padding-top: 40px;
|
||||
|
||||
.autocomplete-field {
|
||||
width: 250px;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
padding: 0;
|
||||
margin-right: 1px;
|
||||
|
||||
.input {
|
||||
float: none;
|
||||
margin: 0 10px 10px 0;
|
||||
|
||||
input {
|
||||
background: rgba(100,100,100,0.5);
|
||||
color: $white;
|
||||
padding: 15px 13px 10px;
|
||||
font-size: 16px;
|
||||
border: 1px solid transparent;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
|
||||
&:focus,
|
||||
&:active {
|
||||
border-color: $blue;
|
||||
}
|
||||
}
|
||||
|
||||
&:after {
|
||||
background: $blue;
|
||||
}
|
||||
}
|
||||
|
||||
.results {
|
||||
top: 49px;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-field {
|
||||
.button {
|
||||
background: rgba(100,100,100,0.5);
|
||||
color: $white;
|
||||
padding: 16px 20px 14px 20px;
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.options {
|
||||
top: 50px;
|
||||
right: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.submit {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
section {
|
||||
.grid {
|
||||
@ -109,8 +188,42 @@
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.seeds {
|
||||
padding-top: 20px;
|
||||
.parameters {
|
||||
|
||||
.seeds {
|
||||
padding-top: 20px;
|
||||
float: none;
|
||||
|
||||
.seed {
|
||||
float: none;
|
||||
}
|
||||
}
|
||||
|
||||
.tunabilities {
|
||||
float: none;
|
||||
|
||||
.tunability {
|
||||
width: 100%;
|
||||
float: none;
|
||||
}
|
||||
}
|
||||
|
||||
.add {
|
||||
.add-properties {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.col {
|
||||
&.tracks {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&.others,
|
||||
&.w5 {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user