Basic webplayer

Now able to play a single file, in a format supported by your browser.

* Playlists not yet supported.
* Volume is a simple on/off switch.
* Repeat / Random not yet supported.
This commit is contained in:
Lucas Verney 2016-08-07 00:58:36 +02:00
parent 4cef2c2014
commit 4d4ce6c14e
22 changed files with 373 additions and 84 deletions

View File

@ -5,7 +5,7 @@ import { CALL_API } from "../middleware/api";
import { artist, track, album } from "../models/api"; import { artist, track, album } from "../models/api";
export const DEFAULT_LIMIT = 30; /** Default max number of elements to retrieve. */ export const DEFAULT_LIMIT = 32; /** Default max number of elements to retrieve. */
export default function (action, requestType, successType, failureType) { export default function (action, requestType, successType, failureType) {
const itemName = action.rstrip("s"); const itemName = action.rstrip("s");

View File

@ -11,3 +11,4 @@ export var { loadSongs } = APIAction("songs", API_REQUEST, API_SUCCESS, API_FAIL
export * from "./paginate"; export * from "./paginate";
export * from "./store"; export * from "./store";
export * from "./webplayer";

92
app/actions/webplayer.js Normal file
View File

@ -0,0 +1,92 @@
export const PLAY_PAUSE = "PLAY_PAUSE";
/**
* true to play, false to pause.
*/
export function togglePlaying(playPause) {
return (dispatch, getState) => {
let isPlaying = false;
if (typeof playPause !== "undefined") {
isPlaying = playPause;
} else {
isPlaying = !(getState().webplayer.isPlaying);
}
dispatch({
type: PLAY_PAUSE,
payload: {
isPlaying: isPlaying
}
});
};
}
export const PUSH_PLAYLIST = "PUSH_PLAYLIST";
export function playTrack(trackID) {
return (dispatch, getState) => {
const track = getState().api.entities.getIn(["track", trackID]);
const album = getState().api.entities.getIn(["album", track.get("album")]);
const artist = getState().api.entities.getIn(["artist", track.get("artist")]);
dispatch({
type: PUSH_PLAYLIST,
payload: {
playlist: [trackID],
tracks: [
[trackID, track]
],
albums: [
[album.get("id"), album]
],
artists: [
[artist.get("id"), artist]
]
}
});
dispatch(togglePlaying(true));
};
}
export const CHANGE_TRACK = "CHANGE_TRACK";
export function playPrevious() {
// TODO: Playlist overflow
return (dispatch, getState) => {
let { index } = getState().webplayer;
dispatch({
type: CHANGE_TRACK,
payload: {
index: index - 1
}
});
};
}
export function playNext() {
// TODO: Playlist overflow
return (dispatch, getState) => {
let { index } = getState().webplayer;
dispatch({
type: CHANGE_TRACK,
payload: {
index: index + 1
}
});
};
}
export const TOGGLE_RANDOM = "TOGGLE_RANDOM";
export function toggleRandom() {
return {
type: TOGGLE_RANDOM
};
}
export const TOGGLE_REPEAT = "TOGGLE_REPEAT";
export function toggleRepeat() {
return {
type: TOGGLE_REPEAT
};
}
export const TOGGLE_MUTE = "TOGGLE_MUTE";
export function toggleMute() {
return {
type: TOGGLE_MUTE
};
}

View File

@ -1,6 +1,6 @@
import React, { Component, PropTypes } from "react"; import React, { Component, PropTypes } from "react";
import CSSModules from "react-css-modules"; import CSSModules from "react-css-modules";
import { defineMessages, FormattedMessage } from "react-intl"; import { defineMessages, FormattedMessage, injectIntl, intlShape } from "react-intl";
import FontAwesome from "react-fontawesome"; import FontAwesome from "react-fontawesome";
import Immutable from "immutable"; import Immutable from "immutable";
@ -12,13 +12,14 @@ import css from "../styles/Album.scss";
const albumMessages = defineMessages(messagesMap(Array.concat([], commonMessages))); const albumMessages = defineMessages(messagesMap(Array.concat([], commonMessages)));
class AlbumTrackRowCSS extends Component { class AlbumTrackRowCSSIntl extends Component {
render () { render () {
const { formatMessage } = this.props.intl;
const length = formatLength(this.props.track.get("time")); const length = formatLength(this.props.track.get("time"));
return ( return (
<tr> <tr>
<td> <td>
<button styleName="play"> <button styleName="play" title={formatMessage(albumMessages["app.common.play"])} onClick={() => this.props.playAction(this.props.track.get("id"))}>
<span className="sr-only"> <span className="sr-only">
<FormattedMessage {...albumMessages["app.common.play"]} /> <FormattedMessage {...albumMessages["app.common.play"]} />
</span> </span>
@ -33,18 +34,21 @@ class AlbumTrackRowCSS extends Component {
} }
} }
AlbumTrackRowCSS.propTypes = { AlbumTrackRowCSSIntl.propTypes = {
track: PropTypes.instanceOf(Immutable.Map).isRequired playAction: PropTypes.func.isRequired,
track: PropTypes.instanceOf(Immutable.Map).isRequired,
intl: intlShape.isRequired
}; };
export let AlbumTrackRow = CSSModules(AlbumTrackRowCSS, css); export let AlbumTrackRow = injectIntl(CSSModules(AlbumTrackRowCSSIntl, css));
class AlbumTracksTableCSS extends Component { class AlbumTracksTableCSS extends Component {
render () { render () {
let rows = []; let rows = [];
const playAction = this.props.playAction;
this.props.tracks.forEach(function (item) { this.props.tracks.forEach(function (item) {
rows.push(<AlbumTrackRow track={item} key={item.get("id")} />); rows.push(<AlbumTrackRow playAction={playAction} track={item} key={item.get("id")} />);
}); });
return ( return (
<table className="table table-hover" styleName="songs"> <table className="table table-hover" styleName="songs">
@ -57,6 +61,7 @@ class AlbumTracksTableCSS extends Component {
} }
AlbumTracksTableCSS.propTypes = { AlbumTracksTableCSS.propTypes = {
playAction: PropTypes.func.isRequired,
tracks: PropTypes.instanceOf(Immutable.List).isRequired tracks: PropTypes.instanceOf(Immutable.List).isRequired
}; };
@ -75,7 +80,7 @@ class AlbumRowCSS extends Component {
<div className="col-xs-9 col-sm-10 table-responsive"> <div className="col-xs-9 col-sm-10 table-responsive">
{ {
this.props.songs.size > 0 ? this.props.songs.size > 0 ?
<AlbumTracksTable tracks={this.props.songs} /> : <AlbumTracksTable playAction={this.props.playAction} tracks={this.props.songs} /> :
null null
} }
</div> </div>
@ -85,6 +90,7 @@ class AlbumRowCSS extends Component {
} }
AlbumRowCSS.propTypes = { AlbumRowCSS.propTypes = {
playAction: PropTypes.func.isRequired,
album: PropTypes.instanceOf(Immutable.Map).isRequired, album: PropTypes.instanceOf(Immutable.Map).isRequired,
songs: PropTypes.instanceOf(Immutable.List).isRequired songs: PropTypes.instanceOf(Immutable.List).isRequired
}; };

View File

@ -26,7 +26,7 @@ class ArtistCSS extends Component {
</div> </div>
); );
if (this.props.isFetching && !this.props.artist) { if (this.props.isFetching && !this.props.artist.size > 0) {
// Loading // Loading
return loading; return loading;
} }
@ -37,7 +37,7 @@ class ArtistCSS extends Component {
} }
let albumsRows = []; let albumsRows = [];
const { albums, songs } = this.props; const { albums, songs, playAction } = this.props;
const artistAlbums = this.props.artist.get("albums"); const artistAlbums = this.props.artist.get("albums");
if (albums && songs && artistAlbums && artistAlbums.size > 0) { if (albums && songs && artistAlbums && artistAlbums.size > 0) {
this.props.artist.get("albums").forEach(function (album) { this.props.artist.get("albums").forEach(function (album) {
@ -45,7 +45,7 @@ class ArtistCSS extends Component {
const albumSongs = album.get("tracks").map( const albumSongs = album.get("tracks").map(
id => songs.get(id) id => songs.get(id)
); );
albumsRows.push(<AlbumRow album={album} songs={albumSongs} key={album.get("id")} />); albumsRows.push(<AlbumRow playAction={playAction} album={album} songs={albumSongs} key={album.get("id")} />);
}); });
} }
else { else {
@ -76,6 +76,7 @@ class ArtistCSS extends Component {
} }
ArtistCSS.propTypes = { ArtistCSS.propTypes = {
playAction: PropTypes.func.isRequired,
isFetching: PropTypes.bool.isRequired, isFetching: PropTypes.bool.isRequired,
error: PropTypes.string, error: PropTypes.string,
artist: PropTypes.instanceOf(Immutable.Map), artist: PropTypes.instanceOf(Immutable.Map),

View File

@ -1,7 +1,7 @@
import React, { Component, PropTypes } from "react"; import React, { Component, PropTypes } from "react";
import { Link} from "react-router"; import { Link} from "react-router";
import CSSModules from "react-css-modules"; import CSSModules from "react-css-modules";
import { defineMessages, FormattedMessage } from "react-intl"; import { defineMessages, injectIntl, intlShape, FormattedMessage } from "react-intl";
import FontAwesome from "react-fontawesome"; import FontAwesome from "react-fontawesome";
import Immutable from "immutable"; import Immutable from "immutable";
import Fuse from "fuse.js"; import Fuse from "fuse.js";
@ -18,15 +18,16 @@ import css from "../styles/Songs.scss";
const songsMessages = defineMessages(messagesMap(Array.concat([], commonMessages, messages))); const songsMessages = defineMessages(messagesMap(Array.concat([], commonMessages, messages)));
class SongsTableRowCSS extends Component { class SongsTableRowCSSIntl extends Component {
render () { render () {
const { formatMessage } = this.props.intl;
const length = formatLength(this.props.song.get("time")); const length = formatLength(this.props.song.get("time"));
const linkToArtist = "/artist/" + this.props.song.getIn(["artist", "id"]); const linkToArtist = "/artist/" + this.props.song.getIn(["artist", "id"]);
const linkToAlbum = "/album/" + this.props.song.getIn(["album", "id"]); const linkToAlbum = "/album/" + this.props.song.getIn(["album", "id"]);
return ( return (
<tr> <tr>
<td> <td>
<button styleName="play"> <button styleName="play" title={formatMessage(songsMessages["app.common.play"])} onClick={() => this.props.playAction(this.props.song.get("id"))}>
<span className="sr-only"> <span className="sr-only">
<FormattedMessage {...songsMessages["app.common.play"]} /> <FormattedMessage {...songsMessages["app.common.play"]} />
</span> </span>
@ -43,11 +44,13 @@ class SongsTableRowCSS extends Component {
} }
} }
SongsTableRowCSS.propTypes = { SongsTableRowCSSIntl.propTypes = {
song: PropTypes.instanceOf(Immutable.Map).isRequired playAction: PropTypes.func.isRequired,
song: PropTypes.instanceOf(Immutable.Map).isRequired,
intl: intlShape.isRequired
}; };
export let SongsTableRow = CSSModules(SongsTableRowCSS, css); export let SongsTableRow = injectIntl(CSSModules(SongsTableRowCSSIntl, css));
class SongsTableCSS extends Component { class SongsTableCSS extends Component {
@ -67,8 +70,9 @@ class SongsTableCSS extends Component {
} }
let rows = []; let rows = [];
const { playAction } = this.props;
displayedSongs.forEach(function (song) { displayedSongs.forEach(function (song) {
rows.push(<SongsTableRow song={song} key={song.get("id")} />); rows.push(<SongsTableRow playAction={playAction} song={song} key={song.get("id")} />);
}); });
let loading = null; let loading = null;
if (rows.length == 0 && this.props.isFetching) { if (rows.length == 0 && this.props.isFetching) {
@ -112,6 +116,7 @@ class SongsTableCSS extends Component {
} }
SongsTableCSS.propTypes = { SongsTableCSS.propTypes = {
playAction: PropTypes.func.isRequired,
songs: PropTypes.instanceOf(Immutable.List).isRequired, songs: PropTypes.instanceOf(Immutable.List).isRequired,
filterText: PropTypes.string filterText: PropTypes.string
}; };
@ -145,7 +150,7 @@ export default class FilterablePaginatedSongsTable extends Component {
<div> <div>
{ error } { error }
<FilterBar filterText={this.state.filterText} onUserInput={this.handleUserInput} /> <FilterBar filterText={this.state.filterText} onUserInput={this.handleUserInput} />
<SongsTable isFetching={this.props.isFetching} songs={this.props.songs} filterText={this.state.filterText} /> <SongsTable playAction={this.props.playAction} isFetching={this.props.isFetching} songs={this.props.songs} filterText={this.state.filterText} />
<Pagination {...this.props.pagination} /> <Pagination {...this.props.pagination} />
</div> </div>
); );
@ -153,6 +158,7 @@ export default class FilterablePaginatedSongsTable extends Component {
} }
FilterablePaginatedSongsTable.propTypes = { FilterablePaginatedSongsTable.propTypes = {
playAction: PropTypes.func.isRequired,
isFetching: PropTypes.bool.isRequired, isFetching: PropTypes.bool.isRequired,
error: PropTypes.string, error: PropTypes.string,
songs: PropTypes.instanceOf(Immutable.List).isRequired, songs: PropTypes.instanceOf(Immutable.List).isRequired,

View File

@ -23,15 +23,24 @@ class WebPlayerCSSIntl extends Component {
artOpacityHandler (ev) { artOpacityHandler (ev) {
if (ev.type == "mouseover") { if (ev.type == "mouseover") {
this.refs.art.style.opacity = "1"; this.refs.art.style.opacity = "1";
this.refs.artText.style.display = "none";
} else { } else {
this.refs.art.style.opacity = "0.75"; this.refs.art.style.opacity = "0.75";
this.refs.artText.style.display = "block";
} }
} }
render () { render () {
const { formatMessage } = this.props.intl; const { formatMessage } = this.props.intl;
const song = this.props.currentTrack;
if (!song) {
return (<div></div>);
}
const playPause = this.props.isPlaying ? "pause" : "play"; const playPause = this.props.isPlaying ? "pause" : "play";
const volumeMute = this.props.isMute ? "volume-off" : "volume-up";
const randomBtnStyles = ["randomBtn"]; const randomBtnStyles = ["randomBtn"];
const repeatBtnStyles = ["repeatBtn"]; const repeatBtnStyles = ["repeatBtn"];
if (this.props.isRandom) { if (this.props.isRandom) {
@ -46,36 +55,38 @@ class WebPlayerCSSIntl extends Component {
<div className="col-xs-12"> <div className="col-xs-12">
<div className="row" styleName="artRow" onMouseOver={this.artOpacityHandler} onMouseOut={this.artOpacityHandler}> <div className="row" styleName="artRow" onMouseOver={this.artOpacityHandler} onMouseOut={this.artOpacityHandler}>
<div className="col-xs-12"> <div className="col-xs-12">
<img src={this.props.song.get("art")} width="200" height="200" alt={formatMessage(webplayerMessages["app.common.art"])} ref="art" styleName="art" /> <img src={song.get("art")} width="200" height="200" alt={formatMessage(webplayerMessages["app.common.art"])} ref="art" styleName="art" />
<h2>{this.props.song.get("title")}</h2> <div ref="artText">
<h3> <h2>{song.get("title")}</h2>
<span className="text-capitalize"> <h3>
<FormattedMessage {...webplayerMessages["app.webplayer.by"]} /> <span className="text-capitalize">
</span> {this.props.song.get("artist")} <FormattedMessage {...webplayerMessages["app.webplayer.by"]} />
</h3> </span> { this.props.currentArtist.get("name") }
</h3>
</div>
</div> </div>
</div> </div>
<div className="row text-center" styleName="controls"> <div className="row text-center" styleName="controls">
<div className="col-xs-12"> <div className="col-xs-12">
<button styleName="prevBtn" aria-label={formatMessage(webplayerMessages["app.webplayer.previous"])} title={formatMessage(webplayerMessages["app.webplayer.previous"])}> <button styleName="prevBtn" aria-label={formatMessage(webplayerMessages["app.webplayer.previous"])} title={formatMessage(webplayerMessages["app.webplayer.previous"])} onClick={this.props.onPrev}>
<FontAwesome name="step-backward" /> <FontAwesome name="step-backward" />
</button> </button>
<button className="play" styleName="playPauseBtn" aria-label={formatMessage(webplayerMessages["app.common." + playPause])} title={formatMessage(webplayerMessages["app.common." + playPause])}> <button className="play" styleName="playPauseBtn" aria-label={formatMessage(webplayerMessages["app.common." + playPause])} title={formatMessage(webplayerMessages["app.common." + playPause])} onClick={this.props.onPlayPause}>
<FontAwesome name={playPause} /> <FontAwesome name={playPause} />
</button> </button>
<button styleName="nextBtn" aria-label={formatMessage(webplayerMessages["app.webplayer.next"])} title={formatMessage(webplayerMessages["app.webplayer.next"])}> <button styleName="nextBtn" aria-label={formatMessage(webplayerMessages["app.webplayer.next"])} title={formatMessage(webplayerMessages["app.webplayer.next"])} onClick={this.props.onSkip}>
<FontAwesome name="step-forward" /> <FontAwesome name="step-forward" />
</button> </button>
</div> </div>
<div className="col-xs-12"> <div className="col-xs-12">
<button styleName="volumeBtn" aria-label={formatMessage(webplayerMessages["app.webplayer.volume"])} title={formatMessage(webplayerMessages["app.webplayer.volume"])}> <button styleName="volumeBtn" aria-label={formatMessage(webplayerMessages["app.webplayer.volume"])} title={formatMessage(webplayerMessages["app.webplayer.volume"])} onClick={this.props.onMute}>
<FontAwesome name="volume-up" /> <FontAwesome name={volumeMute} />
</button> </button>
<button styleName={repeatBtnStyles.join(" ")} aria-label={formatMessage(webplayerMessages["app.webplayer.repeat"])} title={formatMessage(webplayerMessages["app.webplayer.repeat"])} aria-pressed={this.props.isRepeat}> <button styleName={repeatBtnStyles.join(" ")} aria-label={formatMessage(webplayerMessages["app.webplayer.repeat"])} title={formatMessage(webplayerMessages["app.webplayer.repeat"])} aria-pressed={this.props.isRepeat} onClick={this.props.onRepeat}>
<FontAwesome name="repeat" /> <FontAwesome name="repeat" />
</button> </button>
<button styleName={randomBtnStyles.join(" ")} aria-label={formatMessage(webplayerMessages["app.webplayer.random"])} title={formatMessage(webplayerMessages["app.webplayer.random"])} aria-pressed={this.props.isRandom}> <button styleName={randomBtnStyles.join(" ")} aria-label={formatMessage(webplayerMessages["app.webplayer.random"])} title={formatMessage(webplayerMessages["app.webplayer.random"])} aria-pressed={this.props.isRandom} onClick={this.props.onRandom}>
<FontAwesome name="random" /> <FontAwesome name="random" />
</button> </button>
<button styleName="playlistBtn" aria-label={formatMessage(webplayerMessages["app.webplayer.playlist"])} title={formatMessage(webplayerMessages["app.webplayer.playlist"])}> <button styleName="playlistBtn" aria-label={formatMessage(webplayerMessages["app.webplayer.playlist"])} title={formatMessage(webplayerMessages["app.webplayer.playlist"])}>
@ -90,10 +101,18 @@ class WebPlayerCSSIntl extends Component {
} }
WebPlayerCSSIntl.propTypes = { WebPlayerCSSIntl.propTypes = {
song: PropTypes.instanceOf(Immutable.Map).isRequired,
isPlaying: PropTypes.bool.isRequired, isPlaying: PropTypes.bool.isRequired,
isRandom: PropTypes.bool.isRequired, isRandom: PropTypes.bool.isRequired,
isRepeat: PropTypes.bool.isRequired, isRepeat: PropTypes.bool.isRequired,
isMute: PropTypes.bool.isRequired,
currentTrack: PropTypes.instanceOf(Immutable.Map),
currentArtist: PropTypes.instanceOf(Immutable.Map),
onPlayPause: PropTypes.func.isRequired,
onPrev: PropTypes.func.isRequired,
onSkip: PropTypes.func.isRequired,
onRandom: PropTypes.func.isRequired,
onRepeat: PropTypes.func.isRequired,
onMute: PropTypes.func.isRequired,
intl: intlShape.isRequired intl: intlShape.isRequired
}; };

17
app/models/webplayer.js Normal file
View File

@ -0,0 +1,17 @@
import Immutable from "immutable";
export const entitiesRecord = new Immutable.Record({
artists: new Immutable.Map(),
albums: new Immutable.Map(),
tracks: new Immutable.Map()
});
export const stateRecord = new Immutable.Record({
isPlaying: false,
isRandom: false,
isRepeat: false,
isMute: false,
currentIndex: 0,
playlist: new Immutable.List(),
entities: new entitiesRecord()
});

View File

@ -3,6 +3,7 @@ import { combineReducers } from "redux";
import auth from "./auth"; import auth from "./auth";
import paginate from "./paginate"; import paginate from "./paginate";
import webplayer from "./webplayer";
import * as ActionTypes from "../actions"; import * as ActionTypes from "../actions";
@ -16,7 +17,8 @@ const api = paginate([
const rootReducer = combineReducers({ const rootReducer = combineReducers({
routing, routing,
auth, auth,
api api,
webplayer
}); });
export default rootReducer; export default rootReducer;

51
app/reducers/webplayer.js Normal file
View File

@ -0,0 +1,51 @@
import Immutable from "immutable";
import {
PUSH_PLAYLIST,
CHANGE_TRACK,
PLAY_PAUSE,
TOGGLE_RANDOM,
TOGGLE_REPEAT,
TOGGLE_MUTE } from "../actions";
import { createReducer } from "../utils";
import { stateRecord } from "../models/webplayer";
/**
* Initial state
*/
var initialState = new stateRecord();
/**
* Reducers
*/
export default createReducer(initialState, {
[PLAY_PAUSE]: (state, payload) => {
return state.set("isPlaying", payload.isPlaying);
},
[CHANGE_TRACK]: (state, payload) => {
return state.set("currentIndex", payload.index);
},
[PUSH_PLAYLIST]: (state, payload) => {
return (
state
.set("playlist", new Immutable.List(payload.playlist))
.setIn(["entities", "artists"], new Immutable.Map(payload.artists))
.setIn(["entities", "albums"], new Immutable.Map(payload.albums))
.setIn(["entities", "tracks"], new Immutable.Map(payload.tracks))
.set("currentIndex", 0)
.set("isPlaying", true)
);
},
[TOGGLE_RANDOM]: (state) => {
return state.set("isRandom", !state.get("isRandom"));
},
[TOGGLE_REPEAT]: (state) => {
return state.set("isRepeat", !state.get("isRepeat"));
},
[TOGGLE_MUTE]: (state) => {
return state.set("isMute", !state.get("isMute"));
},
});

View File

@ -25,11 +25,14 @@ $controlsMarginTop: 10px;
.btn { .btn {
background: transparent; background: transparent;
border: none; border: none;
opacity: 0.8; opacity: 0.4;
} }
.btn:hover { .btn:hover,
.btn:active,
.btn:focus {
opacity: 1; opacity: 1;
outline: none;
} }
.prevBtn, .prevBtn,

View File

@ -27,14 +27,14 @@ class ArtistPageIntl extends Component {
const {formatMessage} = this.props.intl; const {formatMessage} = this.props.intl;
const error = handleErrorI18nObject(this.props.error, formatMessage, artistMessages); const error = handleErrorI18nObject(this.props.error, formatMessage, artistMessages);
return ( return (
<Artist isFetching={this.props.isFetching} error={error} artist={this.props.artist} albums={this.props.albums} songs={this.props.songs} /> <Artist playAction={this.props.actions.playTrack} isFetching={this.props.isFetching} error={error} artist={this.props.artist} albums={this.props.albums} songs={this.props.songs} />
); );
} }
} }
const mapStateToProps = (state, ownProps) => { const mapStateToProps = (state, ownProps) => {
const artists = state.api.entities.get("artist"); const artists = state.api.entities.get("artist");
let artist = undefined; let artist = new Immutable.Map();
let albums = new Immutable.Map(); let albums = new Immutable.Map();
let songs = new Immutable.Map(); let songs = new Immutable.Map();
if (artists) { if (artists) {

View File

@ -35,7 +35,7 @@ class SongsPageIntl extends Component {
const {formatMessage} = this.props.intl; const {formatMessage} = this.props.intl;
const error = handleErrorI18nObject(this.props.error, formatMessage, songsMessages); const error = handleErrorI18nObject(this.props.error, formatMessage, songsMessages);
return ( return (
<Songs isFetching={this.props.isFetching} error={error} songs={this.props.songsList} pagination={pagination} /> <Songs playAction={this.props.actions.playTrack} isFetching={this.props.isFetching} error={error} songs={this.props.songsList} pagination={pagination} />
); );
} }
} }

View File

@ -1,23 +1,112 @@
import React, { Component } from "react"; import React, { Component } from "react";
import { bindActionCreators } from "redux";
import { connect } from "react-redux";
import { Howl } from "howler";
import Immutable from "immutable"; import Immutable from "immutable";
import * as actionCreators from "../actions";
import WebPlayerComponent from "../components/elements/WebPlayer"; import WebPlayerComponent from "../components/elements/WebPlayer";
class WebPlayer extends Component {
constructor (props) {
super(props);
this.play = this.play.bind(this);
this.howl = null;
}
componentDidMount () {
this.play(this.props.isPlaying);
}
componentWillUpdate (nextProps) {
// Toggle play / pause
if (nextProps.isPlaying != this.props.isPlaying) {
// This check ensure we do not start multiple times the same music.
this.play(nextProps);
}
// Toggle mute / unmute
if (this.howl) {
this.howl.mute(nextProps.isMute);
}
}
getCurrentTrackPath (props) {
return [
"tracks",
props.playlist.get(props.currentIndex)
];
}
play (props) {
if (props.isPlaying) {
if (!this.howl) {
const url = props.entities.getIn(
Array.concat([], this.getCurrentTrackPath(props), ["url"])
);
if (!url) {
// TODO: Error handling
return;
}
this.howl = new Howl({
src: [url],
html5: true,
loop: false,
mute: props.isMute,
autoplay: false,
});
}
this.howl.play();
}
else {
if (this.howl) {
this.howl.pause();
}
}
}
export default class WebPlayer extends Component {
render () { render () {
const currentTrack = this.props.entities.getIn(this.getCurrentTrackPath(this.props));
let currentArtist = new Immutable.Map();
if (currentTrack) {
currentArtist = this.props.entities.getIn(["artists", currentTrack.get("artist")]);
}
const webplayerProps = { const webplayerProps = {
song: new Immutable.Map({ isPlaying: this.props.isPlaying,
art: "http://albumartcollection.com/wp-content/uploads/2011/07/summer-album-art.jpg", isRandom: this.props.isRandom,
title: "Tel-ho", isRepeat: this.props.isRepeat,
artist: "Lapso Laps", isMute: this.props.isMute,
}), currentTrack: currentTrack,
isPlaying: false, currentArtist: currentArtist,
isRandom: false, onPlayPause: (() => this.props.actions.togglePlaying()),
isRepeat: true onPrev: this.props.actions.playPrevious,
onSkip: this.props.actions.playNext,
onRandom: this.props.actions.toggleRandom,
onRepeat: this.props.actions.toggleRepeat,
onMute: this.props.actions.toggleMute
}; };
return ( return (
<WebPlayerComponent {...webplayerProps} /> <WebPlayerComponent {...webplayerProps} />
); );
} }
} }
const mapStateToProps = (state) => ({
isPlaying: state.webplayer.isPlaying,
isRandom: state.webplayer.isRandom,
isRepeat: state.webplayer.isRepeat,
isMute: state.webplayer.isMute,
currentIndex: state.webplayer.currentIndex,
playlist: state.webplayer.playlist,
entities: state.webplayer.entities
});
const mapDispatchToProps = (dispatch) => ({
actions: bindActionCreators(actionCreators, dispatch)
});
export default connect(mapStateToProps, mapDispatchToProps)(WebPlayer);

View File

@ -26,6 +26,7 @@
"eslint": "^3.2.2", "eslint": "^3.2.2",
"font-awesome": "^4.6.3", "font-awesome": "^4.6.3",
"fuse.js": "^2.4.1", "fuse.js": "^2.4.1",
"howler": "^2.0.0",
"html5shiv": "^3.7.3", "html5shiv": "^3.7.3",
"humps": "^1.1.0", "humps": "^1.1.0",
"imagesloaded": "^4.1.0", "imagesloaded": "^4.1.0",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,2 +1,2 @@
!function(e){function t(r){if(n[r])return n[r].exports;var a=n[r]={exports:{},id:r,loaded:!1};return e[r].call(a.exports,a,a.exports,t),a.loaded=!0,a.exports}var n={};return t.m=e,t.c=n,t.p="./",t(0)}({0:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n(626);Object.keys(r).forEach(function(e){"default"!==e&&Object.defineProperty(t,e,{enumerable:!0,get:function(){return r[e]}})})},626:function(e,t){!function(t,n){function r(e,t){var n=e.createElement("p"),r=e.getElementsByTagName("head")[0]||e.documentElement;return n.innerHTML="x<style>"+t+"</style>",r.insertBefore(n.lastChild,r.firstChild)}function a(){var e=b.elements;return"string"==typeof e?e.split(" "):e}function o(e,t){var n=b.elements;"string"!=typeof n&&(n=n.join(" ")),"string"!=typeof e&&(e=e.join(" ")),b.elements=n+" "+e,s(t)}function c(e){var t=E[e[v]];return t||(t={},y++,e[v]=y,E[y]=t),t}function i(e,t,r){if(t||(t=n),f)return t.createElement(e);r||(r=c(t));var a;return a=r.cache[e]?r.cache[e].cloneNode():g.test(e)?(r.cache[e]=r.createElem(e)).cloneNode():r.createElem(e),!a.canHaveChildren||p.test(e)||a.tagUrn?a:r.frag.appendChild(a)}function l(e,t){if(e||(e=n),f)return e.createDocumentFragment();t=t||c(e);for(var r=t.frag.cloneNode(),o=0,i=a(),l=i.length;o<l;o++)r.createElement(i[o]);return r}function u(e,t){t.cache||(t.cache={},t.createElem=e.createElement,t.createFrag=e.createDocumentFragment,t.frag=t.createFrag()),e.createElement=function(n){return b.shivMethods?i(n,e,t):t.createElem(n)},e.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+a().join().replace(/[\w\-:]+/g,function(e){return t.createElem(e),t.frag.createElement(e),'c("'+e+'")'})+");return n}")(b,t.frag)}function s(e){e||(e=n);var t=c(e);return!b.shivCSS||d||t.hasCSS||(t.hasCSS=!!r(e,"article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}mark{background:#FF0;color:#000}template{display:none}")),f||u(e,t),e}var d,f,m="3.7.3-pre",h=t.html5||{},p=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,g=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,v="_html5shiv",y=0,E={};!function(){try{var e=n.createElement("a");e.innerHTML="<xyz></xyz>",d="hidden"in e,f=1==e.childNodes.length||function(){n.createElement("a");var e=n.createDocumentFragment();return"undefined"==typeof e.cloneNode||"undefined"==typeof e.createDocumentFragment||"undefined"==typeof e.createElement}()}catch(t){d=!0,f=!0}}();var b={elements:h.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output picture progress section summary template time video",version:m,shivCSS:h.shivCSS!==!1,supportsUnknownElements:f,shivMethods:h.shivMethods!==!1,type:"default",shivDocument:s,createElement:i,createDocumentFragment:l,addElements:o};t.html5=b,s(n),"object"==typeof e&&e.exports&&(e.exports=b)}("undefined"!=typeof window?window:this,document)}}); !function(e){function t(r){if(n[r])return n[r].exports;var a=n[r]={exports:{},id:r,loaded:!1};return e[r].call(a.exports,a,a.exports,t),a.loaded=!0,a.exports}var n={};return t.m=e,t.c=n,t.p="./",t(0)}({0:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n(630);Object.keys(r).forEach(function(e){"default"!==e&&Object.defineProperty(t,e,{enumerable:!0,get:function(){return r[e]}})})},630:function(e,t){!function(t,n){function r(e,t){var n=e.createElement("p"),r=e.getElementsByTagName("head")[0]||e.documentElement;return n.innerHTML="x<style>"+t+"</style>",r.insertBefore(n.lastChild,r.firstChild)}function a(){var e=b.elements;return"string"==typeof e?e.split(" "):e}function o(e,t){var n=b.elements;"string"!=typeof n&&(n=n.join(" ")),"string"!=typeof e&&(e=e.join(" ")),b.elements=n+" "+e,s(t)}function c(e){var t=E[e[v]];return t||(t={},y++,e[v]=y,E[y]=t),t}function i(e,t,r){if(t||(t=n),f)return t.createElement(e);r||(r=c(t));var a;return a=r.cache[e]?r.cache[e].cloneNode():g.test(e)?(r.cache[e]=r.createElem(e)).cloneNode():r.createElem(e),!a.canHaveChildren||p.test(e)||a.tagUrn?a:r.frag.appendChild(a)}function l(e,t){if(e||(e=n),f)return e.createDocumentFragment();t=t||c(e);for(var r=t.frag.cloneNode(),o=0,i=a(),l=i.length;o<l;o++)r.createElement(i[o]);return r}function u(e,t){t.cache||(t.cache={},t.createElem=e.createElement,t.createFrag=e.createDocumentFragment,t.frag=t.createFrag()),e.createElement=function(n){return b.shivMethods?i(n,e,t):t.createElem(n)},e.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+a().join().replace(/[\w\-:]+/g,function(e){return t.createElem(e),t.frag.createElement(e),'c("'+e+'")'})+");return n}")(b,t.frag)}function s(e){e||(e=n);var t=c(e);return!b.shivCSS||d||t.hasCSS||(t.hasCSS=!!r(e,"article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}mark{background:#FF0;color:#000}template{display:none}")),f||u(e,t),e}var d,f,m="3.7.3-pre",h=t.html5||{},p=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,g=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,v="_html5shiv",y=0,E={};!function(){try{var e=n.createElement("a");e.innerHTML="<xyz></xyz>",d="hidden"in e,f=1==e.childNodes.length||function(){n.createElement("a");var e=n.createDocumentFragment();return"undefined"==typeof e.cloneNode||"undefined"==typeof e.createDocumentFragment||"undefined"==typeof e.createElement}()}catch(t){d=!0,f=!0}}();var b={elements:h.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output picture progress section summary template time video",version:m,shivCSS:h.shivCSS!==!1,supportsUnknownElements:f,shivMethods:h.shivMethods!==!1,type:"default",shivDocument:s,createElement:i,createDocumentFragment:l,addElements:o};t.html5=b,s(n),"object"==typeof e&&e.exports&&(e.exports=b)}("undefined"!=typeof window?window:this,document)}});
//# sourceMappingURL=fix.ie9.js.map //# sourceMappingURL=fix.ie9.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long