Basic error mechanism in webplayer

This commit is contained in:
Lucas Verney 2016-08-12 16:30:17 +02:00
parent 92f909a668
commit 23aa8b52ab
9 changed files with 133 additions and 52 deletions

View File

@ -4,6 +4,7 @@
// Other actions // Other actions
import { decrementRefCount, incrementRefCount } from "./entities"; import { decrementRefCount, incrementRefCount } from "./entities";
import { i18nRecord } from "../models/i18n";
export const PLAY_PAUSE = "PLAY_PAUSE"; export const PLAY_PAUSE = "PLAY_PAUSE";
@ -251,8 +252,10 @@ export const TOGGLE_RANDOM = "TOGGLE_RANDOM";
* @return Dispatch a TOGGLE_RANDOM action. * @return Dispatch a TOGGLE_RANDOM action.
*/ */
export function toggleRandom() { export function toggleRandom() {
return { return (dispatch) => {
type: TOGGLE_RANDOM, dispatch({
type: TOGGLE_RANDOM,
});
}; };
} }
@ -264,8 +267,10 @@ export const TOGGLE_REPEAT = "TOGGLE_REPEAT";
* @return Dispatch a TOGGLE_REPEAT action. * @return Dispatch a TOGGLE_REPEAT action.
*/ */
export function toggleRepeat() { export function toggleRepeat() {
return { return (dispatch) => {
type: TOGGLE_REPEAT, dispatch({
type: TOGGLE_REPEAT,
});
}; };
} }
@ -277,8 +282,10 @@ export const TOGGLE_MUTE = "TOGGLE_MUTE";
* @return Dispatch a TOGGLE_MUTE action. * @return Dispatch a TOGGLE_MUTE action.
*/ */
export function toggleMute() { export function toggleMute() {
return { return (dispatch) => {
type: TOGGLE_MUTE, dispatch({
type: TOGGLE_MUTE,
});
}; };
} }
@ -292,10 +299,33 @@ export const SET_VOLUME = "SET_VOLUME";
* @return Dispatch a SET_VOLUME action. * @return Dispatch a SET_VOLUME action.
*/ */
export function setVolume(volume) { export function setVolume(volume) {
return { return (dispatch) => {
type: SET_VOLUME, dispatch({
payload: { type: SET_VOLUME,
volume: volume, payload: {
}, volume: volume,
},
});
};
}
export const SET_ERROR = "SET_ERROR";
/**
* Set an error in case a song is not in a supported format.
*
* @return Dispatch a SET_ERROR action.
*/
export function unsupportedMediaType() {
return (dispatch) => {
dispatch({
type: SET_ERROR,
payload: {
error: new i18nRecord({
id: "app.webplayer.unsupported",
values: {},
}),
},
});
}; };
} }

View File

@ -138,6 +138,12 @@ class WebPlayerCSSIntl extends Component {
</div> </div>
</div> </div>
{
this.props.error
? <div className="row text-center"><p>{this.props.error}</p></div>
: null
}
<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={onPrev} ref="prevBtn"> <button styleName="prevBtn" aria-label={formatMessage(webplayerMessages["app.webplayer.previous"])} title={formatMessage(webplayerMessages["app.webplayer.previous"])} onClick={onPrev} ref="prevBtn">
@ -179,6 +185,7 @@ WebPlayerCSSIntl.propTypes = {
volume: PropTypes.number.isRequired, volume: PropTypes.number.isRequired,
currentIndex: PropTypes.number.isRequired, currentIndex: PropTypes.number.isRequired,
playlist: PropTypes.instanceOf(Immutable.List).isRequired, playlist: PropTypes.instanceOf(Immutable.List).isRequired,
error: PropTypes.string,
currentSong: PropTypes.instanceOf(Immutable.Map), currentSong: PropTypes.instanceOf(Immutable.Map),
currentArtist: PropTypes.instanceOf(Immutable.Map), currentArtist: PropTypes.instanceOf(Immutable.Map),
onPlayPause: PropTypes.func.isRequired, onPlayPause: PropTypes.func.isRequired,

View File

@ -51,5 +51,6 @@ module.exports = {
"app.webplayer.previous": "Previous", // Previous button description "app.webplayer.previous": "Previous", // Previous button description
"app.webplayer.random": "Random", // Random button description "app.webplayer.random": "Random", // Random button description
"app.webplayer.repeat": "Repeat", // Repeat button description "app.webplayer.repeat": "Repeat", // Repeat button description
"app.webplayer.unsupported": "Unsupported media type", // "Unsupported media type",
"app.webplayer.volume": "Volume", // Volume button description "app.webplayer.volume": "Volume", // Volume button description
}; };

View File

@ -51,5 +51,6 @@ module.exports = {
"app.webplayer.previous": "Précédent", // Previous button description "app.webplayer.previous": "Précédent", // Previous button description
"app.webplayer.random": "Aléatoire", // Random button description "app.webplayer.random": "Aléatoire", // Random button description
"app.webplayer.repeat": "Répéter", // Repeat button description "app.webplayer.repeat": "Répéter", // Repeat button description
"app.webplayer.unsupported": "Format non supporté", // "Unsupported media type",
"app.webplayer.volume": "Volume", // Volume button description "app.webplayer.volume": "Volume", // Volume button description
}; };

View File

@ -34,6 +34,11 @@ const messages = [
defaultMessage: "Playlist", defaultMessage: "Playlist",
description: "Playlist button description", description: "Playlist button description",
}, },
{
"id": "app.webplayer.unsupported",
"description": "Unsupported media type",
"defaultMessage": "Unsupported media type",
},
]; ];
export default messages; export default messages;

View File

@ -15,4 +15,5 @@ export const stateRecord = new Immutable.Record({
volume: 100, /** Current volume, between 0 and 100 */ volume: 100, /** Current volume, between 0 and 100 */
currentIndex: 0, /** Current index in the playlist */ currentIndex: 0, /** Current index in the playlist */
playlist: new Immutable.List(), /** List of songs IDs, references songs in the entities store */ playlist: new Immutable.List(), /** List of songs IDs, references songs in the entities store */
error: null, /** An error string */
}); });

View File

@ -25,6 +25,7 @@ import {
TOGGLE_REPEAT, TOGGLE_REPEAT,
TOGGLE_MUTE, TOGGLE_MUTE,
SET_VOLUME, SET_VOLUME,
SET_ERROR,
INVALIDATE_STORE } from "../actions"; INVALIDATE_STORE } from "../actions";
@ -35,25 +36,6 @@ import {
var initialState = new stateRecord(); var initialState = new stateRecord();
/**
* Helper functions
*/
/**
* Stop playback in reducer helper.
*
* @param state Current state to update.
*/
function stopPlayback(state) {
return (
state
.set("isPlaying", false)
.set("currentIndex", 0)
.set("playlist", new Immutable.List())
);
}
/** /**
* Reducers * Reducers
*/ */
@ -61,11 +43,21 @@ function stopPlayback(state) {
export default createReducer(initialState, { export default createReducer(initialState, {
[PLAY_PAUSE]: (state, payload) => { [PLAY_PAUSE]: (state, payload) => {
// Force play or pause // Force play or pause
return state.set("isPlaying", payload.isPlaying); return (
state
.set("isPlaying", payload.isPlaying)
.set("error", null)
);
}, },
[STOP_PLAYBACK]: (state) => { [STOP_PLAYBACK]: (state) => {
// Clear the playlist // Clear the playlist
return stopPlayback(state); return (
state
.set("isPlaying", false)
.set("currentIndex", 0)
.set("playlist", new Immutable.List())
.set("error", null)
);
}, },
[SET_PLAYLIST]: (state, payload) => { [SET_PLAYLIST]: (state, payload) => {
// Set current playlist, reset playlist index // Set current playlist, reset playlist index
@ -73,6 +65,7 @@ export default createReducer(initialState, {
state state
.set("playlist", new Immutable.List(payload.playlist)) .set("playlist", new Immutable.List(payload.playlist))
.set("currentIndex", 0) .set("currentIndex", 0)
.set("error", null)
); );
}, },
[PUSH_SONG]: (state, payload) => { [PUSH_SONG]: (state, payload) => {
@ -113,6 +106,9 @@ export default createReducer(initialState, {
"currentIndex", "currentIndex",
Math.max(newState.get("currentIndex") - 1, 0) Math.max(newState.get("currentIndex") - 1, 0)
); );
} else if (payload.index == state.get("currentIndex")) {
// If we remove current song, clear the error as well
newState = newState.set("error", null);
} }
return newState; return newState;
}, },
@ -127,9 +123,13 @@ export default createReducer(initialState, {
// If there is an overlow on the left of the playlist, just play // If there is an overlow on the left of the playlist, just play
// first music again // first music again
// TODO: Should seek to beginning of music // TODO: Should seek to beginning of music
return state; return state.set("error", null);
} else { } else {
return state.set("currentIndex", newIndex); return (
state
.set("currentIndex", newIndex)
.set("error", null)
);
} }
}, },
[PLAY_NEXT_SONG]: (state) => { [PLAY_NEXT_SONG]: (state) => {
@ -138,14 +138,22 @@ export default createReducer(initialState, {
// If there is an overflow // If there is an overflow
if (state.get("isRepeat")) { if (state.get("isRepeat")) {
// TODO: Handle repeat // TODO: Handle repeat
return state; return state.set("error", null);
} else { } else {
// Just stop playback // Just stop playback
return state.set("isPlaying", false); return (
state
.set("isPlaying", false)
.set("error", null)
);
} }
} else { } else {
// Else, play next item // Else, play next item
return state.set("currentIndex", newIndex); return (
state
.set("currentIndex", newIndex)
.set("error", null)
);
} }
}, },
[TOGGLE_RANDOM]: (state) => { [TOGGLE_RANDOM]: (state) => {
@ -160,6 +168,13 @@ export default createReducer(initialState, {
[SET_VOLUME]: (state, payload) => { [SET_VOLUME]: (state, payload) => {
return state.set("volume", payload.volume); return state.set("volume", payload.volume);
}, },
[SET_ERROR]: (state, payload) => {
return (
state
.set("isPlaying", false)
.set("error", payload.error)
);
},
[INVALIDATE_STORE]: () => { [INVALIDATE_STORE]: () => {
return new stateRecord(); return new stateRecord();
}, },

View File

@ -2,7 +2,11 @@
import React, { Component, PropTypes } from "react"; import React, { Component, PropTypes } from "react";
import { bindActionCreators } from "redux"; import { bindActionCreators } from "redux";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Howl } from "howler"; import { defineMessages, injectIntl, intlShape } from "react-intl";
import { Howler, Howl } from "howler";
// Local imports
import { messagesMap, handleErrorI18nObject } from "../utils";
// Actions // Actions
import * as actionCreators from "../actions"; import * as actionCreators from "../actions";
@ -10,11 +14,17 @@ import * as actionCreators from "../actions";
// Components // Components
import WebPlayerComponent from "../components/elements/WebPlayer"; import WebPlayerComponent from "../components/elements/WebPlayer";
// Translations
import messages from "../locales/messagesDescriptors/elements/WebPlayer";
// Define translations
const webplayerMessages = defineMessages(messagesMap(Array.concat([], messages)));
/** /**
* Webplayer container. * Webplayer container.
*/ */
class WebPlayer extends Component { class WebPlayerIntl extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
@ -75,18 +85,22 @@ class WebPlayer extends Component {
startPlaying(props) { startPlaying(props) {
if (props.isPlaying && props.currentSong) { if (props.isPlaying && props.currentSong) {
// If it should be playing any song // If it should be playing any song
// Build a new Howler object with current song to play
const url = props.currentSong.get("url"); const url = props.currentSong.get("url");
this.howl = new Howl({ if (Howler.codecs(url.split(".").pop())) {
src: [url], // Build a new Howler object with current song to play
html5: true, // Use HTML5 by default to allow streaming this.howl = new Howl({
mute: props.isMute, src: [url],
volume: props.volume / 100, // Set current volume html5: true, // Use HTML5 by default to allow streaming
autoplay: false, // No autoplay, we handle it manually mute: props.isMute,
onend: () => props.actions.playNextSong(), // Play next song at the end volume: props.volume / 100, // Set current volume
}); autoplay: false, // No autoplay, we handle it manually
// Start playing onend: () => props.actions.playNextSong(), // Play next song at the end
this.howl.play(); });
// Start playing
this.howl.play();
} else {
this.props.actions.unsupportedMediaType();
}
} }
else { else {
// If it should not be playing // If it should not be playing
@ -120,6 +134,8 @@ class WebPlayer extends Component {
} }
render() { render() {
const { formatMessage } = this.props.intl;
const webplayerProps = { const webplayerProps = {
isPlaying: this.props.isPlaying, isPlaying: this.props.isPlaying,
isRandom: this.props.isRandom, isRandom: this.props.isRandom,
@ -128,6 +144,7 @@ class WebPlayer extends Component {
volume: this.props.volume, volume: this.props.volume,
currentIndex: this.props.currentIndex, currentIndex: this.props.currentIndex,
playlist: this.props.playlist, playlist: this.props.playlist,
error: handleErrorI18nObject(this.props.error, formatMessage, webplayerMessages),
currentSong: this.props.currentSong, currentSong: this.props.currentSong,
currentArtist: this.props.currentArtist, currentArtist: this.props.currentArtist,
// Use a lambda to ensure no first argument is passed to // Use a lambda to ensure no first argument is passed to
@ -151,8 +168,9 @@ class WebPlayer extends Component {
); );
} }
} }
WebPlayer.propTypes = { WebPlayerIntl.propTypes = {
location: PropTypes.object, location: PropTypes.object,
intl: intlShape.isRequired,
}; };
const mapStateToProps = (state) => { const mapStateToProps = (state) => {
const currentIndex = state.webplayer.currentIndex; const currentIndex = state.webplayer.currentIndex;
@ -172,6 +190,7 @@ const mapStateToProps = (state) => {
volume: state.webplayer.volume, volume: state.webplayer.volume,
currentIndex: currentIndex, currentIndex: currentIndex,
playlist: playlist, playlist: playlist,
error: state.webplayer.error,
currentSong: currentSong, currentSong: currentSong,
currentArtist: currentArtist, currentArtist: currentArtist,
}; };
@ -179,4 +198,4 @@ const mapStateToProps = (state) => {
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
actions: bindActionCreators(actionCreators, dispatch), actions: bindActionCreators(actionCreators, dispatch),
}); });
export default connect(mapStateToProps, mapDispatchToProps)(WebPlayer); export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(WebPlayerIntl));

View File

@ -3,6 +3,8 @@
* in the app, and generates a complete locale file for English. * in the app, and generates a complete locale file for English.
* *
* This script is meant to be run through `npm run extractTranslations`. * This script is meant to be run through `npm run extractTranslations`.
*
* TODO: Check that every identifier is actually used in the code.
*/ */
import * as fs from 'fs'; import * as fs from 'fs';
import {sync as globSync} from 'glob'; import {sync as globSync} from 'glob';