Creating sticky playback controls in foot
This commit is contained in:
@ -14,7 +14,7 @@
|
||||
<link rel="shortcut icon" type="image/x-icon" href="/iris/assets/favicon.ico" />
|
||||
<link rel="shortcut-icon" href="/iris/assets/favicon.ico" />
|
||||
|
||||
<link href="//fonts.googleapis.com/css?family=Roboto+Condensed:400,700|Montserrat:500,700" rel="stylesheet">
|
||||
<link href="//fonts.googleapis.com/css?family=Archivo+Narrow:400,700|Raleway:500,700,900" rel="stylesheet">
|
||||
<link href="/iris/app.css" rel="stylesheet" />
|
||||
|
||||
<link rel="manifest" href="/iris/assets/manifest.json">
|
||||
|
||||
@ -6,7 +6,7 @@ import { hashHistory, Link } from 'react-router'
|
||||
import { connect } from 'react-redux'
|
||||
|
||||
import Sidebar from './components/Sidebar'
|
||||
import MiniPlayer from './components/MiniPlayer'
|
||||
import PlaybackControls from './components/PlaybackControls'
|
||||
import SidebarToggleButton from './components/SidebarToggleButton'
|
||||
import ContextMenu from './components/ContextMenu'
|
||||
import Dragger from './components/Dragger'
|
||||
@ -190,7 +190,7 @@ class App extends React.Component{
|
||||
return (
|
||||
<div className={className}>
|
||||
<Sidebar />
|
||||
<MiniPlayer />
|
||||
<PlaybackControls />
|
||||
<main>
|
||||
{this.props.children}
|
||||
</main>
|
||||
|
||||
@ -27,8 +27,15 @@ class MiniPlayer extends React.Component{
|
||||
}
|
||||
|
||||
render(){
|
||||
var images = []
|
||||
if (this.props.current_track && this.props.current_track.album && this.props.current_track.album.images){
|
||||
images = this.props.current_track.album.images
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="player mini-player">
|
||||
|
||||
<Thumbnail size="small" images={images} />
|
||||
|
||||
<div className="current-track">
|
||||
<div className="title">{ this.props.current_track ? this.props.current_track.name : <span>-</span> }</div>
|
||||
|
||||
131
src/js/components/PlaybackControls.js
Executable file
131
src/js/components/PlaybackControls.js
Executable file
@ -0,0 +1,131 @@
|
||||
|
||||
import React, { PropTypes } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { bindActionCreators } from 'redux'
|
||||
import FontAwesome from 'react-fontawesome'
|
||||
|
||||
import ProgressSlider from './ProgressSlider'
|
||||
import VolumeControl from './VolumeControl'
|
||||
import Dater from './Dater'
|
||||
import ArtistSentence from './ArtistSentence'
|
||||
import Thumbnail from './Thumbnail'
|
||||
|
||||
import * as mopidyActions from '../services/mopidy/actions'
|
||||
|
||||
class PlaybackControls extends React.Component{
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
renderPlayButton(){
|
||||
var button = <a className="control play" onClick={() => this.props.mopidyActions.play()}><FontAwesome name="play" /> </a>
|
||||
if( this.props.play_state == 'playing' ){
|
||||
button = <a className="control play" onClick={() => this.props.mopidyActions.pause()}><FontAwesome name="pause" /> </a>
|
||||
}
|
||||
return button;
|
||||
}
|
||||
|
||||
renderConsumeButton(){
|
||||
var button = <a className="control has-tooltip" onClick={() => this.props.mopidyActions.instruct('tracklist.setConsume', [true])}><FontAwesome name="fire" /><span className="tooltip">Consume</span></a>
|
||||
if( this.props.consume ){
|
||||
button = <a className="control active has-tooltip" onClick={() => this.props.mopidyActions.instruct('tracklist.setConsume', [false])}><FontAwesome name="fire" /><span className="tooltip">Consume</span></a>
|
||||
}
|
||||
return button;
|
||||
}
|
||||
|
||||
renderRandomButton(){
|
||||
var button = <a className="control has-tooltip" onClick={() => this.props.mopidyActions.instruct('tracklist.setRandom', [true])}><FontAwesome name="random" /><span className="tooltip">Shuffle</span></a>
|
||||
if( this.props.random ){
|
||||
button = <a className="control active has-tooltip" onClick={() => this.props.mopidyActions.instruct('tracklist.setRandom', [false])}><FontAwesome name="random" /><span className="tooltip">Shuffle</span></a>
|
||||
}
|
||||
return button;
|
||||
}
|
||||
|
||||
renderRepeatButton(){
|
||||
var button = <a className="control has-tooltip" onClick={() => this.props.mopidyActions.instruct('tracklist.setRepeat', [true])}><FontAwesome name="repeat" /><span className="tooltip">Repeat</span></a>
|
||||
if( this.props.repeat ){
|
||||
button = <a className="control active has-tooltip" onClick={() => this.props.mopidyActions.instruct('tracklist.setRepeat', [false])}><FontAwesome name="repeat" /><span className="tooltip">Repeat</span></a>
|
||||
}
|
||||
return button;
|
||||
}
|
||||
|
||||
render(){
|
||||
var images = []
|
||||
if (this.props.current_track && this.props.current_track.album && this.props.current_track.album.images){
|
||||
images = this.props.current_track.album.images
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="playback-controls">
|
||||
|
||||
<div className="current-track">
|
||||
<Thumbnail size="small" images={images} />
|
||||
<div className="title">
|
||||
{ this.props.current_track ? this.props.current_track.name : <span>-</span> }
|
||||
</div>
|
||||
<div className="artist">
|
||||
{ this.props.current_track ? <ArtistSentence artists={ this.props.current_track.artists } /> : <ArtistSentence /> }
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section className="playback">
|
||||
<a className="control" onClick={() => this.props.mopidyActions.previous()}>
|
||||
<FontAwesome name="step-backward" />
|
||||
</a>
|
||||
{ this.renderPlayButton() }
|
||||
<a className="control" onClick={() => this.props.mopidyActions.stop()}>
|
||||
<FontAwesome name="stop" />
|
||||
</a>
|
||||
<a className="control" onClick={() => this.props.mopidyActions.next()}>
|
||||
<FontAwesome name="step-forward" />
|
||||
</a>
|
||||
</section>
|
||||
|
||||
<section className="settings">
|
||||
{ this.renderConsumeButton() }
|
||||
{ this.renderRandomButton() }
|
||||
{ this.renderRepeatButton() }
|
||||
</section>
|
||||
|
||||
<section className="progress">
|
||||
<ProgressSlider />
|
||||
<span className="current">{ this.props.time_position ? <Dater type="length" data={this.props.time_position} /> : '-' }</span>
|
||||
<span className="total">{ this.props.current_track ? <Dater type="length" data={this.props.current_track.length} /> : '-' }</span>
|
||||
</section>
|
||||
|
||||
<section className="volume">
|
||||
<VolumeControl />
|
||||
</section>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Export our component
|
||||
*
|
||||
* We also integrate our global store, using connect()
|
||||
**/
|
||||
|
||||
const mapStateToProps = (state, ownProps) => {
|
||||
return {
|
||||
current_track: (typeof(state.ui.current_track) !== 'undefined' && typeof(state.ui.tracks) !== 'undefined' && typeof(state.ui.tracks[state.ui.current_track.uri]) !== 'undefined' ? state.ui.tracks[state.ui.current_track.uri] : null),
|
||||
radio_enabled: (state.ui.radio && state.ui.radio.enabled ? true : false),
|
||||
play_state: state.mopidy.play_state,
|
||||
time_position: state.mopidy.time_position,
|
||||
consume: state.mopidy.consume,
|
||||
repeat: state.mopidy.repeat,
|
||||
random: state.mopidy.random
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch) => {
|
||||
return {
|
||||
mopidyActions: bindActionCreators(mopidyActions, dispatch)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(PlaybackControls)
|
||||
@ -5,7 +5,6 @@ import { bindActionCreators } from 'redux'
|
||||
import { Link } from 'react-router'
|
||||
|
||||
import Icon from './Icon'
|
||||
import Thumbnail from './Thumbnail'
|
||||
import SearchForm from './SearchForm'
|
||||
import Dropzones from './Dropzones'
|
||||
|
||||
@ -21,8 +20,6 @@ class Sidebar extends React.Component{
|
||||
render(){
|
||||
return (
|
||||
<aside>
|
||||
|
||||
{ this.props.current_track && this.props.current_track.album && this.props.current_track.album.images ? <Thumbnail size="large" images={this.props.current_track.album.images} /> : null }
|
||||
|
||||
<div className="liner">
|
||||
|
||||
@ -113,7 +110,6 @@ const mapStateToProps = (state, ownProps) => {
|
||||
pusher_connected: state.pusher.connected,
|
||||
spotify_connected: state.spotify.connected,
|
||||
spotify_authorized: state.spotify.authorized,
|
||||
current_track: (typeof(state.ui.current_track) !== 'undefined' && typeof(state.ui.tracks) !== 'undefined' && typeof(state.ui.tracks[state.ui.current_track.uri]) !== 'undefined' ? state.ui.tracks[state.ui.current_track.uri] : null),
|
||||
dragger: state.ui.dragger
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,8 +18,8 @@ class VolumeControl extends React.Component{
|
||||
var slider = e.target;
|
||||
|
||||
var sliderY = e.clientY - slider.getBoundingClientRect().top;
|
||||
var sliderHeight = slider.getBoundingClientRect().height;
|
||||
var percent = 100 - parseInt( sliderY / sliderHeight * 100 );
|
||||
var sliderWidth = slider.getBoundingClientRect().width;
|
||||
var percent = 100 - parseInt( sliderY / sliderWidth * 100 );
|
||||
|
||||
this.props.mopidyActions.setVolume( percent )
|
||||
}
|
||||
@ -33,14 +33,14 @@ class VolumeControl extends React.Component{
|
||||
{this.props.mute ? <FontAwesome className="muted" name="volume-off" /> : <FontAwesome name="volume-down" />}
|
||||
</a>
|
||||
|
||||
<span className="hover-trigger">
|
||||
<a onClick={() => this.props.mopidyActions.setMute(!this.props.mute)}>
|
||||
<span className="default">
|
||||
<a className="control" onClick={() => this.props.mopidyActions.setMute(!this.props.mute)}>
|
||||
{this.props.mute ? <FontAwesome className="muted" name="volume-off" /> : <FontAwesome name="volume-down" />}
|
||||
</a>
|
||||
<div className="slider-wrapper">
|
||||
<div className={this.props.mute ? "slider vertical disabled" : "slider vertical"} onClick={ (e) => this.handleClick(e) } >
|
||||
<div className={this.props.mute ? "slider horizontal disabled" : "slider horizontal"} onClick={ (e) => this.handleClick(e) } >
|
||||
<div className="track">
|
||||
<div className="progress" style={{ height: this.props.volume+'%' }}></div>
|
||||
<div className="progress" style={{ width: this.props.volume+'%' }}></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -51,7 +51,7 @@ class DiscoverFeatured extends React.Component{
|
||||
<Thumbnail images={playlist.images} />
|
||||
</Link>
|
||||
<Link to={global.baseURL+'playlist/'+playlist.uri}>
|
||||
<h1>{playlist.name}</h1>
|
||||
<h2>{playlist.name}</h2>
|
||||
</Link>
|
||||
{playlist.description ? <h3 dangerouslySetInnerHTML={{__html: playlist.description}}></h3> : null}
|
||||
<div className="actions">
|
||||
|
||||
@ -57,7 +57,7 @@ class DiscoverNewReleases extends React.Component{
|
||||
<Thumbnail images={album.images} />
|
||||
</Link>
|
||||
<Link to={global.baseURL+'album/'+album.uri}>
|
||||
<h1>{album.name}</h1>
|
||||
<h2>{album.name}</h2>
|
||||
</Link>
|
||||
<h3>
|
||||
<ArtistSentence artists={album.artists} />
|
||||
|
||||
@ -224,7 +224,7 @@ class Discover extends React.Component{
|
||||
<div className="intro">
|
||||
<Parallax image="/iris/assets/backgrounds/discover.jpg" />
|
||||
<div className="liner">
|
||||
<h1>Explore new music</h1>
|
||||
<h2>Explore new music</h2>
|
||||
<h3>
|
||||
Add seeds below to build your sound. Let's start with
|
||||
{this.props.authorized ? " two of your favorite artists" : " the chill genre"}.
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
@import 'components/icon';
|
||||
@import 'components/search-form';
|
||||
@import 'components/slider';
|
||||
@import 'components/player';
|
||||
@import 'components/playback-controls';
|
||||
@import 'components/sidebar';
|
||||
@import 'components/sidebar-toggle-button';
|
||||
@import 'components/lists';
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
|
||||
main {
|
||||
header {
|
||||
padding: 14px 20px;
|
||||
line-height: 24px;
|
||||
padding: 30px 40px;
|
||||
box-sizing: border-box;
|
||||
|
||||
h1 {
|
||||
@ -14,7 +13,7 @@ main {
|
||||
padding-right: 14px;
|
||||
height: 2.5rem;
|
||||
vertical-align: bottom;
|
||||
margin-bottom: -5px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.options {
|
||||
|
||||
118
src/scss/components/_playback-controls.scss
Executable file
118
src/scss/components/_playback-controls.scss
Executable file
@ -0,0 +1,118 @@
|
||||
|
||||
.playback-controls {
|
||||
position: fixed;
|
||||
z-index: 99;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
height: 50px;
|
||||
background: $white;
|
||||
color: $black;
|
||||
border-top: 1px solid $light_grey;
|
||||
|
||||
.current-track {
|
||||
position: absolute;
|
||||
bottom: 51px;
|
||||
background: $white;
|
||||
height: 100px;
|
||||
width: 220px;
|
||||
border-top: 1px solid $light_grey;
|
||||
}
|
||||
|
||||
.control {
|
||||
@include animate();
|
||||
font-size: 16px;
|
||||
color: $dark_grey;
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
|
||||
&.play {
|
||||
font-size: 24px;
|
||||
margin-top: -4px;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: $turquoise;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
|
||||
section {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
|
||||
&.playback {
|
||||
left: 20px;
|
||||
top: 11px;
|
||||
width: 120px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&.progress {
|
||||
top: 12px;
|
||||
left: 140px;
|
||||
right: 280px;
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
color: $mid_grey;
|
||||
|
||||
.slider {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50px;
|
||||
right: 50px;
|
||||
}
|
||||
|
||||
.current {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
left: 0;
|
||||
width: 45px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.total {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 0;
|
||||
width: 45px;
|
||||
}
|
||||
}
|
||||
|
||||
&.settings {
|
||||
top: 11px;
|
||||
right: 170px;
|
||||
width: 120px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&.volume {
|
||||
top: 12px;
|
||||
right: 20px;
|
||||
width: 150px;
|
||||
|
||||
.modal-trigger {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.control {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.slider-wrapper {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -111,16 +111,18 @@
|
||||
&.mini-player {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
width: 220px;
|
||||
height: 50px;
|
||||
background: $white;
|
||||
border-top: 1px solid $light_grey;
|
||||
box-sizing: border-box;
|
||||
z-index: 97;
|
||||
padding: 14px;
|
||||
color: #FFFFFF;
|
||||
color: $black;
|
||||
|
||||
.artist-sentence {
|
||||
.artist:hover {
|
||||
border-color: #FFFFFF !important;
|
||||
border-color: $black !important;
|
||||
}
|
||||
}
|
||||
|
||||
@ -194,3 +196,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.playback-controls {
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -7,7 +7,7 @@ aside{
|
||||
z-index: 96;
|
||||
width: 220px;
|
||||
overflow: hidden;
|
||||
border-right: 2px solid $light_grey;
|
||||
border-right: 1px solid $light_grey;
|
||||
|
||||
.liner {
|
||||
overflow-y: auto;
|
||||
@ -57,6 +57,8 @@ aside{
|
||||
display: block;
|
||||
padding: 6px 18px 6px 6px;
|
||||
border-left: 4px solid transparent;
|
||||
font-family: "Raleway";
|
||||
font-weight: 700;
|
||||
|
||||
.icon {
|
||||
height: 14px;
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
|
||||
.track {
|
||||
position: absolute;
|
||||
background: rgba(255,255,255,0.3);
|
||||
background: $light_grey;
|
||||
|
||||
.progress {
|
||||
position: absolute;
|
||||
@ -22,12 +22,11 @@
|
||||
}
|
||||
|
||||
&.horizontal {
|
||||
margin: 10px 0;
|
||||
height: 20px;
|
||||
|
||||
.track {
|
||||
height: 2px;
|
||||
top: 9px;
|
||||
height: 5px;
|
||||
top: 12px;
|
||||
right: 0;
|
||||
left: 0;
|
||||
|
||||
@ -43,10 +42,10 @@
|
||||
height: 100%;
|
||||
|
||||
.track {
|
||||
width: 2px;
|
||||
width: 5px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 9px;
|
||||
left: 12px;
|
||||
|
||||
.progress {
|
||||
bottom: 0;
|
||||
|
||||
@ -3,8 +3,8 @@
|
||||
body,
|
||||
html {
|
||||
background: $off_white;
|
||||
color: #000000;
|
||||
font-family: "Roboto Condensed", Helvetica, Arial, sans-serif;
|
||||
color: $black;
|
||||
font-family: "Archivo Narrow", Helvetica, Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
@ -40,6 +40,7 @@ footer {
|
||||
main {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
padding-bottom: 50px;
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
@ -90,13 +91,13 @@ main {
|
||||
h1 {
|
||||
font-weight: 700;
|
||||
font-size: 3rem;
|
||||
font-family: "Montserrat";
|
||||
font-family: "Raleway";
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 500;
|
||||
font-family: "Montserrat";
|
||||
font-family: "Raleway";
|
||||
}
|
||||
|
||||
h3 {
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
position: relative;
|
||||
color: $white;
|
||||
|
||||
h1 {
|
||||
h2 {
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
@ -109,7 +109,7 @@
|
||||
.discover-new-releases-view {
|
||||
.intro {
|
||||
position: relative;
|
||||
padding-top: 50px;
|
||||
padding-top: 120px;
|
||||
|
||||
.parallax {
|
||||
position: absolute;
|
||||
@ -132,12 +132,13 @@
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
.actions {
|
||||
padding-left: 240px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
h2 {
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
@ -160,7 +161,7 @@
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
.actions {
|
||||
padding-left: 120px;
|
||||
|
||||
Reference in New Issue
Block a user