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:
parent
b7f2f32e1d
commit
ab7470d618
@ -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,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
<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,
|
||||||
};
|
};
|
||||||
|
@ -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),
|
||||||
|
@ -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>
|
||||||
|
<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,
|
||||||
|
@ -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"])}>
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
|
@ -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) {
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
Loading…
Reference in New Issue
Block a user