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:
parent
4cef2c2014
commit
4d4ce6c14e
@ -5,7 +5,7 @@ import { CALL_API } from "../middleware/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) {
|
||||
const itemName = action.rstrip("s");
|
||||
|
@ -11,3 +11,4 @@ export var { loadSongs } = APIAction("songs", API_REQUEST, API_SUCCESS, API_FAIL
|
||||
|
||||
export * from "./paginate";
|
||||
export * from "./store";
|
||||
export * from "./webplayer";
|
||||
|
92
app/actions/webplayer.js
Normal file
92
app/actions/webplayer.js
Normal 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
|
||||
};
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import React, { Component, PropTypes } from "react";
|
||||
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 Immutable from "immutable";
|
||||
|
||||
@ -12,13 +12,14 @@ import css from "../styles/Album.scss";
|
||||
|
||||
const albumMessages = defineMessages(messagesMap(Array.concat([], commonMessages)));
|
||||
|
||||
class AlbumTrackRowCSS extends Component {
|
||||
class AlbumTrackRowCSSIntl extends Component {
|
||||
render () {
|
||||
const { formatMessage } = this.props.intl;
|
||||
const length = formatLength(this.props.track.get("time"));
|
||||
return (
|
||||
<tr>
|
||||
<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">
|
||||
<FormattedMessage {...albumMessages["app.common.play"]} />
|
||||
</span>
|
||||
@ -33,18 +34,21 @@ class AlbumTrackRowCSS extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
AlbumTrackRowCSS.propTypes = {
|
||||
track: PropTypes.instanceOf(Immutable.Map).isRequired
|
||||
AlbumTrackRowCSSIntl.propTypes = {
|
||||
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 {
|
||||
render () {
|
||||
let rows = [];
|
||||
const playAction = this.props.playAction;
|
||||
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 (
|
||||
<table className="table table-hover" styleName="songs">
|
||||
@ -57,6 +61,7 @@ class AlbumTracksTableCSS extends Component {
|
||||
}
|
||||
|
||||
AlbumTracksTableCSS.propTypes = {
|
||||
playAction: PropTypes.func.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">
|
||||
{
|
||||
this.props.songs.size > 0 ?
|
||||
<AlbumTracksTable tracks={this.props.songs} /> :
|
||||
<AlbumTracksTable playAction={this.props.playAction} tracks={this.props.songs} /> :
|
||||
null
|
||||
}
|
||||
</div>
|
||||
@ -85,6 +90,7 @@ class AlbumRowCSS extends Component {
|
||||
}
|
||||
|
||||
AlbumRowCSS.propTypes = {
|
||||
playAction: PropTypes.func.isRequired,
|
||||
album: PropTypes.instanceOf(Immutable.Map).isRequired,
|
||||
songs: PropTypes.instanceOf(Immutable.List).isRequired
|
||||
};
|
||||
|
@ -26,7 +26,7 @@ class ArtistCSS extends Component {
|
||||
</div>
|
||||
);
|
||||
|
||||
if (this.props.isFetching && !this.props.artist) {
|
||||
if (this.props.isFetching && !this.props.artist.size > 0) {
|
||||
// Loading
|
||||
return loading;
|
||||
}
|
||||
@ -37,7 +37,7 @@ class ArtistCSS extends Component {
|
||||
}
|
||||
|
||||
let albumsRows = [];
|
||||
const { albums, songs } = this.props;
|
||||
const { albums, songs, playAction } = this.props;
|
||||
const artistAlbums = this.props.artist.get("albums");
|
||||
if (albums && songs && artistAlbums && artistAlbums.size > 0) {
|
||||
this.props.artist.get("albums").forEach(function (album) {
|
||||
@ -45,7 +45,7 @@ class ArtistCSS extends Component {
|
||||
const albumSongs = album.get("tracks").map(
|
||||
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 {
|
||||
@ -76,6 +76,7 @@ class ArtistCSS extends Component {
|
||||
}
|
||||
|
||||
ArtistCSS.propTypes = {
|
||||
playAction: PropTypes.func.isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
error: PropTypes.string,
|
||||
artist: PropTypes.instanceOf(Immutable.Map),
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { Component, PropTypes } from "react";
|
||||
import { Link} from "react-router";
|
||||
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 Immutable from "immutable";
|
||||
import Fuse from "fuse.js";
|
||||
@ -18,15 +18,16 @@ import css from "../styles/Songs.scss";
|
||||
|
||||
const songsMessages = defineMessages(messagesMap(Array.concat([], commonMessages, messages)));
|
||||
|
||||
class SongsTableRowCSS extends Component {
|
||||
class SongsTableRowCSSIntl extends Component {
|
||||
render () {
|
||||
const { formatMessage } = this.props.intl;
|
||||
const length = formatLength(this.props.song.get("time"));
|
||||
const linkToArtist = "/artist/" + this.props.song.getIn(["artist", "id"]);
|
||||
const linkToAlbum = "/album/" + this.props.song.getIn(["album", "id"]);
|
||||
return (
|
||||
<tr>
|
||||
<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">
|
||||
<FormattedMessage {...songsMessages["app.common.play"]} />
|
||||
</span>
|
||||
@ -43,11 +44,13 @@ class SongsTableRowCSS extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
SongsTableRowCSS.propTypes = {
|
||||
song: PropTypes.instanceOf(Immutable.Map).isRequired
|
||||
SongsTableRowCSSIntl.propTypes = {
|
||||
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 {
|
||||
@ -67,8 +70,9 @@ class SongsTableCSS extends Component {
|
||||
}
|
||||
|
||||
let rows = [];
|
||||
const { playAction } = this.props;
|
||||
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;
|
||||
if (rows.length == 0 && this.props.isFetching) {
|
||||
@ -112,6 +116,7 @@ class SongsTableCSS extends Component {
|
||||
}
|
||||
|
||||
SongsTableCSS.propTypes = {
|
||||
playAction: PropTypes.func.isRequired,
|
||||
songs: PropTypes.instanceOf(Immutable.List).isRequired,
|
||||
filterText: PropTypes.string
|
||||
};
|
||||
@ -145,7 +150,7 @@ export default class FilterablePaginatedSongsTable extends Component {
|
||||
<div>
|
||||
{ error }
|
||||
<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} />
|
||||
</div>
|
||||
);
|
||||
@ -153,6 +158,7 @@ export default class FilterablePaginatedSongsTable extends Component {
|
||||
}
|
||||
|
||||
FilterablePaginatedSongsTable.propTypes = {
|
||||
playAction: PropTypes.func.isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
error: PropTypes.string,
|
||||
songs: PropTypes.instanceOf(Immutable.List).isRequired,
|
||||
|
@ -23,15 +23,24 @@ class WebPlayerCSSIntl extends Component {
|
||||
artOpacityHandler (ev) {
|
||||
if (ev.type == "mouseover") {
|
||||
this.refs.art.style.opacity = "1";
|
||||
this.refs.artText.style.display = "none";
|
||||
} else {
|
||||
this.refs.art.style.opacity = "0.75";
|
||||
this.refs.artText.style.display = "block";
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const { formatMessage } = this.props.intl;
|
||||
|
||||
const song = this.props.currentTrack;
|
||||
if (!song) {
|
||||
return (<div></div>);
|
||||
}
|
||||
|
||||
const playPause = this.props.isPlaying ? "pause" : "play";
|
||||
const volumeMute = this.props.isMute ? "volume-off" : "volume-up";
|
||||
|
||||
const randomBtnStyles = ["randomBtn"];
|
||||
const repeatBtnStyles = ["repeatBtn"];
|
||||
if (this.props.isRandom) {
|
||||
@ -46,36 +55,38 @@ class WebPlayerCSSIntl extends Component {
|
||||
<div className="col-xs-12">
|
||||
<div className="row" styleName="artRow" onMouseOver={this.artOpacityHandler} onMouseOut={this.artOpacityHandler}>
|
||||
<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" />
|
||||
<h2>{this.props.song.get("title")}</h2>
|
||||
<h3>
|
||||
<span className="text-capitalize">
|
||||
<FormattedMessage {...webplayerMessages["app.webplayer.by"]} />
|
||||
</span> {this.props.song.get("artist")}
|
||||
</h3>
|
||||
<img src={song.get("art")} width="200" height="200" alt={formatMessage(webplayerMessages["app.common.art"])} ref="art" styleName="art" />
|
||||
<div ref="artText">
|
||||
<h2>{song.get("title")}</h2>
|
||||
<h3>
|
||||
<span className="text-capitalize">
|
||||
<FormattedMessage {...webplayerMessages["app.webplayer.by"]} />
|
||||
</span> { this.props.currentArtist.get("name") }
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="row text-center" styleName="controls">
|
||||
<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" />
|
||||
</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} />
|
||||
</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" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="col-xs-12">
|
||||
<button styleName="volumeBtn" aria-label={formatMessage(webplayerMessages["app.webplayer.volume"])} title={formatMessage(webplayerMessages["app.webplayer.volume"])}>
|
||||
<FontAwesome name="volume-up" />
|
||||
<button styleName="volumeBtn" aria-label={formatMessage(webplayerMessages["app.webplayer.volume"])} title={formatMessage(webplayerMessages["app.webplayer.volume"])} onClick={this.props.onMute}>
|
||||
<FontAwesome name={volumeMute} />
|
||||
</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" />
|
||||
</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" />
|
||||
</button>
|
||||
<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 = {
|
||||
song: PropTypes.instanceOf(Immutable.Map).isRequired,
|
||||
isPlaying: PropTypes.bool.isRequired,
|
||||
isRandom: 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
|
||||
};
|
||||
|
||||
|
17
app/models/webplayer.js
Normal file
17
app/models/webplayer.js
Normal 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()
|
||||
});
|
@ -3,6 +3,7 @@ import { combineReducers } from "redux";
|
||||
|
||||
import auth from "./auth";
|
||||
import paginate from "./paginate";
|
||||
import webplayer from "./webplayer";
|
||||
|
||||
import * as ActionTypes from "../actions";
|
||||
|
||||
@ -16,7 +17,8 @@ const api = paginate([
|
||||
const rootReducer = combineReducers({
|
||||
routing,
|
||||
auth,
|
||||
api
|
||||
api,
|
||||
webplayer
|
||||
});
|
||||
|
||||
export default rootReducer;
|
||||
|
51
app/reducers/webplayer.js
Normal file
51
app/reducers/webplayer.js
Normal 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"));
|
||||
},
|
||||
});
|
@ -25,11 +25,14 @@ $controlsMarginTop: 10px;
|
||||
.btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
opacity: 0.8;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
.btn:hover,
|
||||
.btn:active,
|
||||
.btn:focus {
|
||||
opacity: 1;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.prevBtn,
|
||||
|
@ -27,14 +27,14 @@ class ArtistPageIntl extends Component {
|
||||
const {formatMessage} = this.props.intl;
|
||||
const error = handleErrorI18nObject(this.props.error, formatMessage, artistMessages);
|
||||
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 artists = state.api.entities.get("artist");
|
||||
let artist = undefined;
|
||||
let artist = new Immutable.Map();
|
||||
let albums = new Immutable.Map();
|
||||
let songs = new Immutable.Map();
|
||||
if (artists) {
|
||||
|
@ -35,7 +35,7 @@ class SongsPageIntl extends Component {
|
||||
const {formatMessage} = this.props.intl;
|
||||
const error = handleErrorI18nObject(this.props.error, formatMessage, songsMessages);
|
||||
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} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,23 +1,112 @@
|
||||
import React, { Component } from "react";
|
||||
import { bindActionCreators } from "redux";
|
||||
import { connect } from "react-redux";
|
||||
import { Howl } from "howler";
|
||||
import Immutable from "immutable";
|
||||
|
||||
import * as actionCreators from "../actions";
|
||||
|
||||
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 () {
|
||||
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 = {
|
||||
song: new Immutable.Map({
|
||||
art: "http://albumartcollection.com/wp-content/uploads/2011/07/summer-album-art.jpg",
|
||||
title: "Tel-ho",
|
||||
artist: "Lapso Laps",
|
||||
}),
|
||||
isPlaying: false,
|
||||
isRandom: false,
|
||||
isRepeat: true
|
||||
isPlaying: this.props.isPlaying,
|
||||
isRandom: this.props.isRandom,
|
||||
isRepeat: this.props.isRepeat,
|
||||
isMute: this.props.isMute,
|
||||
currentTrack: currentTrack,
|
||||
currentArtist: currentArtist,
|
||||
onPlayPause: (() => this.props.actions.togglePlaying()),
|
||||
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 (
|
||||
<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);
|
||||
|
@ -26,6 +26,7 @@
|
||||
"eslint": "^3.2.2",
|
||||
"font-awesome": "^4.6.3",
|
||||
"fuse.js": "^2.4.1",
|
||||
"howler": "^2.0.0",
|
||||
"html5shiv": "^3.7.3",
|
||||
"humps": "^1.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
@ -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
|
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
Loading…
Reference in New Issue
Block a user