Basic playlist support for webplayer

Webplayer can now handle a basic playlist, pushing multiple songs in the
playlist and passing from one song to another.

Some things that are not yet working:
* Using previous and next buttons and going outside of the playlist
breaks things.
* Random / repeat modes are not yet implemented.
* Playlist is not exposed in the UI at the moment.
* Seeking in a song is not exposed in the UI.
* When playing a song, webplayer does not automatically play the next
one when reaching the end of the song.
This commit is contained in:
Lucas Verney 2016-08-11 22:01:47 +02:00
parent b7f2f32e1d
commit ab7470d618
19 changed files with 206 additions and 64 deletions

View File

@ -127,11 +127,12 @@ export const PUSH_SONG = "PUSH_SONG";
* *
* @param songID The id of the song to push. * @param songID The id of the song to push.
* @param index [Optional] The position to insert at in the playlist. * @param index [Optional] The position to insert at in the playlist.
* If negative, counts from the end. Defaults to last. * If negative, counts from the end. Undefined (default)
* is last position.
* *
* @return Dispatch a PUSH_SONG action. * @return Dispatch a PUSH_SONG action.
*/ */
export function pushSong(songID, index=-1) { export function pushSong(songID, index) {
return (dispatch) => { return (dispatch) => {
// Handle reference counting // Handle reference counting
dispatch(incrementRefCount({ dispatch(incrementRefCount({
@ -197,31 +198,31 @@ export function jumpToSong(songID) {
} }
export const PLAY_PREVIOUS = "PLAY_PREVIOUS"; export const PLAY_PREVIOUS_SONG = "PLAY_PREVIOUS_SONG";
/** /**
* Move one song backwards in the playlist. * Move one song backwards in the playlist.
* *
* @return Dispatch a PLAY_PREVIOUS action. * @return Dispatch a PLAY_PREVIOUS_SONG action.
*/ */
export function playPrevious() { export function playPreviousSong() {
return (dispatch) => { return (dispatch) => {
dispatch({ dispatch({
type: PLAY_PREVIOUS, type: PLAY_PREVIOUS_SONG,
}); });
}; };
} }
export const PLAY_NEXT = "PLAY_NEXT"; export const PLAY_NEXT_SONG = "PLAY_NEXT_SONG";
/** /**
* Move one song forward in the playlist. * Move one song forward in the playlist.
* *
* @return Dispatch a PLAY_NEXT action. * @return Dispatch a PLAY_NEXT_SONG action.
*/ */
export function playNext() { export function playNextSong() {
return (dispatch) => { return (dispatch) => {
dispatch({ dispatch({
type: PLAY_NEXT, type: PLAY_NEXT_SONG,
}); });
}; };
} }

View File

@ -22,17 +22,47 @@ const albumMessages = defineMessages(messagesMap(Array.concat([], commonMessages
* Track row in an album tracks table. * Track row in an album tracks table.
*/ */
class AlbumTrackRowCSSIntl extends Component { class AlbumTrackRowCSSIntl extends Component {
constructor(props) {
super(props);
// Bind this
this.onPlayClick = this.onPlayClick.bind(this);
this.onPlayNextClick = this.onPlayNextClick.bind(this);
}
/**
* Handle click on play button.
*/
onPlayClick() {
$(this.refs.play).blur();
this.props.playAction(this.props.song.get("id"));
}
/**
* Handle click on play next button.
*/
onPlayNextClick() {
$(this.refs.playNext).blur();
this.props.playNextAction(this.props.song.get("id"));
}
render() { render() {
const { formatMessage } = this.props.intl; 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" title={formatMessage(albumMessages["app.common.play"])} onClick={() => this.props.playAction(this.props.track.get("id"))}> <button styleName="play" title={formatMessage(albumMessages["app.common.play"])} onClick={this.onPlayClick}>
<span className="sr-only"> <span className="sr-only">
<FormattedMessage {...albumMessages["app.common.play"]} /> <FormattedMessage {...albumMessages["app.common.play"]} />
</span> </span>
<FontAwesome name="play-circle-o" aria-hidden="true" /> <FontAwesome name="play-circle-o" aria-hidden="true" />
</button>&nbsp;
<button styleName="playNext" title={formatMessage(albumMessages["app.common.playNext"])} onClick={this.onPlayNextClick} ref="playNext">
<span className="sr-only">
<FormattedMessage {...albumMessages["app.common.playNext"]} />
</span>
<FontAwesome name="plus-circle" aria-hidden="true" />
</button> </button>
</td> </td>
<td>{this.props.track.get("track")}</td> <td>{this.props.track.get("track")}</td>
@ -44,6 +74,7 @@ class AlbumTrackRowCSSIntl extends Component {
} }
AlbumTrackRowCSSIntl.propTypes = { AlbumTrackRowCSSIntl.propTypes = {
playAction: PropTypes.func.isRequired, playAction: PropTypes.func.isRequired,
playNextAction: PropTypes.func.isRequired,
track: PropTypes.instanceOf(Immutable.Map).isRequired, track: PropTypes.instanceOf(Immutable.Map).isRequired,
intl: intlShape.isRequired, intl: intlShape.isRequired,
}; };
@ -57,9 +88,9 @@ class AlbumTracksTableCSS extends Component {
render() { render() {
let rows = []; let rows = [];
// Build rows for each track // Build rows for each track
const playAction = this.props.playAction; const { playAction, playNextAction } = this.props;
this.props.tracks.forEach(function (item) { this.props.tracks.forEach(function (item) {
rows.push(<AlbumTrackRow playAction={playAction} track={item} key={item.get("id")} />); rows.push(<AlbumTrackRow playAction={playAction} playNextAction={playNextAction} track={item} key={item.get("id")} />);
}); });
return ( return (
<table className="table table-hover" styleName="songs"> <table className="table table-hover" styleName="songs">
@ -72,6 +103,7 @@ class AlbumTracksTableCSS extends Component {
} }
AlbumTracksTableCSS.propTypes = { AlbumTracksTableCSS.propTypes = {
playAction: PropTypes.func.isRequired, playAction: PropTypes.func.isRequired,
playNextAction: PropTypes.func.isRequired,
tracks: PropTypes.instanceOf(Immutable.List).isRequired, tracks: PropTypes.instanceOf(Immutable.List).isRequired,
}; };
export let AlbumTracksTable = CSSModules(AlbumTracksTableCSS, css); export let AlbumTracksTable = CSSModules(AlbumTracksTableCSS, css);
@ -93,7 +125,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 playAction={this.props.playAction} tracks={this.props.songs} /> : <AlbumTracksTable playAction={this.props.playAction} playNextAction={this.props.playNextAction} tracks={this.props.songs} /> :
null null
} }
</div> </div>
@ -103,6 +135,7 @@ class AlbumRowCSS extends Component {
} }
AlbumRowCSS.propTypes = { AlbumRowCSS.propTypes = {
playAction: PropTypes.func.isRequired, playAction: PropTypes.func.isRequired,
playNextAction: 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

@ -48,13 +48,13 @@ class ArtistCSS extends Component {
// Build album rows // Build album rows
let albumsRows = []; let albumsRows = [];
const { albums, songs, playAction } = this.props; const { albums, songs, playAction, playNextAction } = this.props;
if (albums && songs) { if (albums && songs) {
albums.forEach(function (album) { albums.forEach(function (album) {
const albumSongs = album.get("tracks").map( const albumSongs = album.get("tracks").map(
id => songs.get(id) id => songs.get(id)
); );
albumsRows.push(<AlbumRow playAction={playAction} album={album} songs={albumSongs} key={album.get("id")} />); albumsRows.push(<AlbumRow playAction={playAction} playNextAction={playNextAction} album={album} songs={albumSongs} key={album.get("id")} />);
}); });
} }
@ -85,6 +85,7 @@ ArtistCSS.propTypes = {
error: PropTypes.string, error: PropTypes.string,
isFetching: PropTypes.bool.isRequired, isFetching: PropTypes.bool.isRequired,
playAction: PropTypes.func.isRequired, playAction: PropTypes.func.isRequired,
playNextAction: PropTypes.func.isRequired,
artist: PropTypes.instanceOf(Immutable.Map), artist: PropTypes.instanceOf(Immutable.Map),
albums: PropTypes.instanceOf(Immutable.List), albums: PropTypes.instanceOf(Immutable.List),
songs: PropTypes.instanceOf(Immutable.Map), songs: PropTypes.instanceOf(Immutable.Map),

View File

@ -30,6 +30,30 @@ const songsMessages = defineMessages(messagesMap(Array.concat([], commonMessages
* A single row for a single song in the songs table. * A single row for a single song in the songs table.
*/ */
class SongsTableRowCSSIntl extends Component { class SongsTableRowCSSIntl extends Component {
constructor(props) {
super(props);
// Bind this
this.onPlayClick = this.onPlayClick.bind(this);
this.onPlayNextClick = this.onPlayNextClick.bind(this);
}
/**
* Handle click on play button.
*/
onPlayClick() {
$(this.refs.play).blur();
this.props.playAction(this.props.song.get("id"));
}
/**
* Handle click on play next button.
*/
onPlayNextClick() {
$(this.refs.playNext).blur();
this.props.playNextAction(this.props.song.get("id"));
}
render() { render() {
const { formatMessage } = this.props.intl; const { formatMessage } = this.props.intl;
@ -40,11 +64,17 @@ class SongsTableRowCSSIntl extends Component {
return ( return (
<tr> <tr>
<td> <td>
<button styleName="play" title={formatMessage(songsMessages["app.common.play"])} onClick={() => this.props.playAction(this.props.song.get("id"))}> <button styleName="play" title={formatMessage(songsMessages["app.common.play"])} onClick={this.onPlayClick} ref="play">
<span className="sr-only"> <span className="sr-only">
<FormattedMessage {...songsMessages["app.common.play"]} /> <FormattedMessage {...songsMessages["app.common.play"]} />
</span> </span>
<FontAwesome name="play-circle-o" aria-hidden="true" /> <FontAwesome name="play-circle-o" aria-hidden="true" />
</button>&nbsp;
<button styleName="playNext" title={formatMessage(songsMessages["app.common.playNext"])} onClick={this.onPlayNextClick} ref="playNext">
<span className="sr-only">
<FormattedMessage {...songsMessages["app.common.playNext"]} />
</span>
<FontAwesome name="plus-circle" aria-hidden="true" />
</button> </button>
</td> </td>
<td className="title">{this.props.song.get("name")}</td> <td className="title">{this.props.song.get("name")}</td>
@ -58,6 +88,7 @@ class SongsTableRowCSSIntl extends Component {
} }
SongsTableRowCSSIntl.propTypes = { SongsTableRowCSSIntl.propTypes = {
playAction: PropTypes.func.isRequired, playAction: PropTypes.func.isRequired,
playNextAction: PropTypes.func.isRequired,
song: PropTypes.instanceOf(Immutable.Map).isRequired, song: PropTypes.instanceOf(Immutable.Map).isRequired,
intl: intlShape.isRequired, intl: intlShape.isRequired,
}; };
@ -86,9 +117,9 @@ class SongsTableCSS extends Component {
// Build song rows // Build song rows
let rows = []; let rows = [];
const { playAction } = this.props; const { playAction, playNextAction } = this.props;
displayedSongs.forEach(function (song) { displayedSongs.forEach(function (song) {
rows.push(<SongsTableRow playAction={playAction} song={song} key={song.get("id")} />); rows.push(<SongsTableRow playAction={playAction} playNextAction={playNextAction} song={song} key={song.get("id")} />);
}); });
// Handle login icon // Handle login icon
@ -134,6 +165,7 @@ class SongsTableCSS extends Component {
} }
SongsTableCSS.propTypes = { SongsTableCSS.propTypes = {
playAction: PropTypes.func.isRequired, playAction: PropTypes.func.isRequired,
playNextAction: PropTypes.func.isRequired,
songs: PropTypes.instanceOf(Immutable.List).isRequired, songs: PropTypes.instanceOf(Immutable.List).isRequired,
filterText: PropTypes.string, filterText: PropTypes.string,
}; };
@ -180,6 +212,7 @@ export default class FilterablePaginatedSongsTable extends Component {
}; };
const songsTableProps = { const songsTableProps = {
playAction: this.props.playAction, playAction: this.props.playAction,
playNextAction: this.props.playNextAction,
isFetching: this.props.isFetching, isFetching: this.props.isFetching,
songs: this.props.songs, songs: this.props.songs,
filterText: this.state.filterText, filterText: this.state.filterText,
@ -197,6 +230,7 @@ export default class FilterablePaginatedSongsTable extends Component {
} }
FilterablePaginatedSongsTable.propTypes = { FilterablePaginatedSongsTable.propTypes = {
playAction: PropTypes.func.isRequired, playAction: PropTypes.func.isRequired,
playNextAction: 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

@ -82,6 +82,32 @@ class WebPlayerCSSIntl extends Component {
} }
} }
// Click handlers
const onPrev = (function () {
$(this.refs.prevBtn).blur();
this.props.onPrev();
}).bind(this);
const onPlayPause = (function () {
$(this.refs.playPauseBtn).blur();
this.props.onPlayPause();
}).bind(this);
const onSkip = (function () {
$(this.refs.nextBtn).blur();
this.props.onSkip();
}).bind(this);
const onMute = (function () {
$(this.refs.volumeBtn).blur();
this.props.onMute();
}).bind(this);
const onRepeat = (function () {
$(this.refs.repeatBtn).blur();
this.props.onRepeat();
}).bind(this);
const onRandom = (function () {
$(this.refs.randomBtn).blur();
this.props.onRandom();
}).bind(this);
return ( return (
<div id="row" styleName="webplayer"> <div id="row" styleName="webplayer">
<div className="col-xs-12"> <div className="col-xs-12">
@ -109,24 +135,24 @@ class WebPlayerCSSIntl extends Component {
<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"])} onClick={this.props.onPrev}> <button styleName="prevBtn" aria-label={formatMessage(webplayerMessages["app.webplayer.previous"])} title={formatMessage(webplayerMessages["app.webplayer.previous"])} onClick={onPrev} ref="prevBtn">
<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])} onClick={this.props.onPlayPause}> <button className="play" styleName="playPauseBtn" aria-label={formatMessage(webplayerMessages["app.common." + playPause])} title={formatMessage(webplayerMessages["app.common." + playPause])} onClick={onPlayPause.bind(this)} ref="playPauseBtn">
<FontAwesome name={playPause} /> <FontAwesome name={playPause} />
</button> </button>
<button styleName="nextBtn" aria-label={formatMessage(webplayerMessages["app.webplayer.next"])} title={formatMessage(webplayerMessages["app.webplayer.next"])} onClick={this.props.onSkip}> <button styleName="nextBtn" aria-label={formatMessage(webplayerMessages["app.webplayer.next"])} title={formatMessage(webplayerMessages["app.webplayer.next"])} onClick={onSkip} ref="nextBtn">
<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"])} onClick={this.props.onMute}> <button styleName="volumeBtn" aria-label={formatMessage(webplayerMessages["app.webplayer.volume"])} title={formatMessage(webplayerMessages["app.webplayer.volume"])} onClick={onMute} ref="volumeBtn">
<FontAwesome name={volumeIcon} /> <FontAwesome name={volumeIcon} />
</button> </button>
<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}> <button styleName={repeatBtnStyles.join(" ")} aria-label={formatMessage(webplayerMessages["app.webplayer.repeat"])} title={formatMessage(webplayerMessages["app.webplayer.repeat"])} aria-pressed={this.props.isRepeat} onClick={onRepeat} ref="repeatBtn">
<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} onClick={this.props.onRandom}> <button styleName={randomBtnStyles.join(" ")} aria-label={formatMessage(webplayerMessages["app.webplayer.random"])} title={formatMessage(webplayerMessages["app.webplayer.random"])} aria-pressed={this.props.isRandom} onClick={onRandom} ref="randomBtn">
<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"])}>

View File

@ -11,6 +11,7 @@ module.exports = {
"app.common.loading": "Loading…", // Loading indicator "app.common.loading": "Loading…", // Loading indicator
"app.common.pause": "Pause", // Pause icon description "app.common.pause": "Pause", // Pause icon description
"app.common.play": "Play", // Play icon description "app.common.play": "Play", // Play icon description
"app.common.playNext": "Play next", // "Play next icon descripton"
"app.common.track": "{itemCount, plural, one {track} other {tracks}}", // Track "app.common.track": "{itemCount, plural, one {track} other {tracks}}", // Track
"app.filter.filter": "Filter…", // Filtering input placeholder "app.filter.filter": "Filter…", // Filtering input placeholder
"app.filter.whatAreWeListeningToToday": "What are we listening to today?", // Description for the filter bar "app.filter.whatAreWeListeningToToday": "What are we listening to today?", // Description for the filter bar

View File

@ -11,6 +11,7 @@ module.exports = {
"app.common.loading": "Chargement…", // Loading indicator "app.common.loading": "Chargement…", // Loading indicator
"app.common.pause": "Pause", // Pause icon description "app.common.pause": "Pause", // Pause icon description
"app.common.play": "Jouer", // PLay icon description "app.common.play": "Jouer", // PLay icon description
"app.common.playNext": "Jouer après", // "Play next icon descripton"
"app.common.track": "{itemCount, plural, one {piste} other {pistes}}", // Track "app.common.track": "{itemCount, plural, one {piste} other {pistes}}", // Track
"app.filter.filter": "Filtrer…", // Filtering input placeholder "app.filter.filter": "Filtrer…", // Filtering input placeholder
"app.filter.whatAreWeListeningToToday": "Que voulez-vous écouter aujourd'hui\u00a0?", // Description for the filter bar "app.filter.whatAreWeListeningToToday": "Que voulez-vous écouter aujourd'hui\u00a0?", // Description for the filter bar

View File

@ -49,6 +49,11 @@ const messages = [
description: "Pause icon description", description: "Pause icon description",
defaultMessage: "Pause", defaultMessage: "Pause",
}, },
{
id: "app.common.playNext",
defaultMessage: "Play next",
description: "Play next icon descripton",
},
]; ];
export default messages; export default messages;

View File

@ -167,7 +167,7 @@ export default createReducer(initialState, {
newState = state.set("isFetching", false).set("error", payload.error); newState = state.set("isFetching", false).set("error", payload.error);
// Merge entities // Merge entities
newState = newState.mergeIn(["entities"], payload.entities); newState = newState.mergeDeepIn(["entities"], payload.entities);
// Increment reference counter // Increment reference counter
payload.refCountType.forEach(function (itemName) { payload.refCountType.forEach(function (itemName) {

View File

@ -19,8 +19,8 @@ import {
PUSH_SONG, PUSH_SONG,
POP_SONG, POP_SONG,
JUMP_TO_SONG, JUMP_TO_SONG,
PLAY_PREVIOUS, PLAY_PREVIOUS_SONG,
PLAY_NEXT, PLAY_NEXT_SONG,
TOGGLE_RANDOM, TOGGLE_RANDOM,
TOGGLE_REPEAT, TOGGLE_REPEAT,
TOGGLE_MUTE, TOGGLE_MUTE,
@ -77,32 +77,66 @@ export default createReducer(initialState, {
}, },
[PUSH_SONG]: (state, payload) => { [PUSH_SONG]: (state, payload) => {
// Push song to playlist // Push song to playlist
const newPlaylist = state.get("playlist").insert(payload.index, payload.song); let newState = state;
return state.set("playlist", newPlaylist); if (payload.index) {
// If index is specified, insert it at this position
newState = newState.set(
"playlist",
newState.get("playlist").insert(payload.index, payload.song)
);
if (payload.index <= newState.get("currentIndex")) { // "<=" because insertion is made before
// If we insert before the current position in the playlist, we
// update the current position to keep the currently played
// music
newState = newState.set(
"currentIndex",
Math.min(newState.get("currentIndex") + 1, newState.get("playlist").size)
);
}
} else {
// Else, push at the end of the playlist
newState = newState.set(
"playlist",
newState.get("playlist").push(payload.song)
);
}
return newState;
}, },
[POP_SONG]: (state, payload) => { [POP_SONG]: (state, payload) => {
// Pop song from playlist // Pop song from playlist
return state.deleteIn(["playlist", payload.index]); let newState = state.deleteIn(["playlist", payload.index]);
if (payload.index < state.get("currentIndex")) {
// If we delete before the current position in the playlist, we
// update the current position to keep the currently played
// music
newState = newState.set(
"currentIndex",
Math.max(newState.get("currentIndex") - 1, 0)
);
}
return newState;
}, },
[JUMP_TO_SONG]: (state, payload) => { [JUMP_TO_SONG]: (state, payload) => {
// Set current index // Set current index
const newCurrentIndex = state.get("playlist").findKey(x => x == payload.song); const newCurrentIndex = state.get("playlist").findKey(x => x == payload.song);
return state.set("currentIndex", newCurrentIndex); return state.set("currentIndex", newCurrentIndex);
}, },
[PLAY_PREVIOUS]: (state) => { [PLAY_PREVIOUS_SONG]: (state) => {
const newIndex = state.get("currentIndex") - 1; const newIndex = state.get("currentIndex") - 1;
if (newIndex < 0) { if (newIndex < 0) {
// If there is an overlow on the left of the playlist, just stop // If there is an overlow on the left of the playlist, just stop
// playback // playback
// TODO: Behavior is not correct
return stopPlayback(state); return stopPlayback(state);
} else { } else {
return state.set("currentIndex", newIndex); return state.set("currentIndex", newIndex);
} }
}, },
[PLAY_NEXT]: (state) => { [PLAY_NEXT_SONG]: (state) => {
const newIndex = state.get("currentIndex") + 1; const newIndex = state.get("currentIndex") + 1;
if (newIndex > state.get("playlist").size) { if (newIndex > state.get("playlist").size) {
// If there is an overflow, just stop playback // If there is an overflow, just stop playback
// TODO: Behavior is not correct
return stopPlayback(state); return stopPlayback(state);
} else { } else {
// Else, play next item // Else, play next item

View File

@ -35,6 +35,11 @@ $artMarginBottom: 10px;
composes: play from "./Songs.scss"; composes: play from "./Songs.scss";
} }
/* Play next button is based on the one in Songs list. */
.playNext {
composes: playNext from "./Songs.scss";
}
@media (max-width: 767px) { @media (max-width: 767px) {
.nameRow h2 { .nameRow h2 {
margin-top: 0; margin-top: 0;

View File

@ -1,7 +1,8 @@
/** /**
* Style for Songs component. * Style for Songs component.
*/ */
.play { .play,
.playNext {
background-color: transparent; background-color: transparent;
border: none; border: none;
text-align: center; text-align: center;

View File

@ -45,7 +45,7 @@ class ArtistPageIntl extends Component {
const error = handleErrorI18nObject(this.props.error, formatMessage, artistMessages); const error = handleErrorI18nObject(this.props.error, formatMessage, artistMessages);
return ( return (
<Artist playAction={this.props.actions.playSong} isFetching={this.props.isFetching} error={error} artist={this.props.artist} albums={this.props.albums} songs={this.props.songs} /> <Artist playAction={this.props.actions.playSong} playNextAction={this.props.actions.pushSong} isFetching={this.props.isFetching} error={error} artist={this.props.artist} albums={this.props.albums} songs={this.props.songs} />
); );
} }
} }

View File

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

View File

@ -102,8 +102,8 @@ class WebPlayer extends Component {
// Use a lambda to ensure no first argument is passed to // Use a lambda to ensure no first argument is passed to
// togglePlaying // togglePlaying
onPlayPause: (() => this.props.actions.togglePlaying()), onPlayPause: (() => this.props.actions.togglePlaying()),
onPrev: this.props.actions.playPrevious, onPrev: this.props.actions.playPreviousSong,
onSkip: this.props.actions.playNext, onSkip: this.props.actions.playNextSong,
onRandom: this.props.actions.toggleRandom, onRandom: this.props.actions.toggleRandom,
onRepeat: this.props.actions.toggleRepeat, onRepeat: this.props.actions.toggleRepeat,
onMute: this.props.actions.toggleMute, onMute: this.props.actions.toggleMute,

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