Basic error mechanism in webplayer
This commit is contained in:
parent
92f909a668
commit
23aa8b52ab
@ -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: {},
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
};
|
};
|
||||||
|
@ -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
|
||||||
};
|
};
|
||||||
|
@ -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;
|
||||||
|
@ -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 */
|
||||||
});
|
});
|
||||||
|
@ -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();
|
||||||
},
|
},
|
||||||
|
@ -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));
|
||||||
|
@ -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';
|
||||||
|
Loading…
Reference in New Issue
Block a user