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.
这个提交包含在:
父节点
4cef2c2014
当前提交
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";
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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",
|
||||
|
|
文件差异因一行或多行过长而隐藏
文件差异因一行或多行过长而隐藏
|
@ -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
|
文件差异因一行或多行过长而隐藏
文件差异因一行或多行过长而隐藏
文件差异因一行或多行过长而隐藏
文件差异因一行或多行过长而隐藏
正在加载...
在新工单中引用