Merge branch 'develop'

This commit is contained in:
James Barnsley
2018-01-30 08:15:54 +13:00
24 changed files with 5941 additions and 1961 deletions

View File

@ -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

View File

@ -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

View File

@ -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",

View File

@ -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 : {})
}
}

View 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)

View File

@ -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));
}
}

View File

@ -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} />&nbsp; </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>
)

View File

@ -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)
}

View File

@ -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

View File

@ -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',

View File

@ -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

View File

@ -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 />

View File

@ -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" />&nbsp;
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)
}
}

View File

@ -38,3 +38,4 @@
@import 'views/discover';
@import 'views/settings';
@import "../../node_modules/react-input-range/lib/css/index.css";

View File

@ -291,6 +291,11 @@
border-radius: 3px;
text-align: center;
h4 &,
h4 &:active {
margin: -8px 0 0 10px;
}
.dot {
width: 6px;
height: 6px;

View File

@ -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;

View File

@ -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){

View File

@ -46,6 +46,12 @@
}
}
&.info {
.fa {
opacity: 0.3;
}
}
&.large-tooltip {
.tooltip {
padding: 8px 12px;
@ -54,4 +60,4 @@
bottom: 50px;
}
}
}
}

View File

@ -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){

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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;
}
}
}
}