Rework webplayer

Full rework of webplayer. Webplayer is back to its previous working
state, and ready for further improvements.
This commit is contained in:
Lucas Verney 2016-08-10 23:50:23 +02:00
parent fffe9c4cd3
commit d8a7d4f66a
71 changed files with 778 additions and 433 deletions

View File

@ -43,6 +43,14 @@ module.exports = {
"strict": [
"error",
],
"comma-dangle": [
"error",
"always-multiline"
],
"space-before-function-paren": [
"error",
{ "anonymous": "always", "named": "never" }
],
"react/jsx-uses-react": "error",
"react/jsx-uses-vars": "error",

View File

@ -42,7 +42,7 @@ export default function (action, requestType, successType, failureType) {
{
artist: arrayOf(artist),
album: arrayOf(album),
song: arrayOf(song)
song: arrayOf(song),
},
{
// Use custom assignEntity function to delete useless fields
@ -52,7 +52,7 @@ export default function (action, requestType, successType, failureType) {
} else {
output[key] = value;
}
}
},
}
);
};
@ -80,9 +80,9 @@ export default function (action, requestType, successType, failureType) {
type: itemName,
result: jsonData.result[itemName],
nPages: nPages,
currentPage: pageNumber
}
}
currentPage: pageNumber,
},
},
];
};
@ -104,7 +104,7 @@ export default function (action, requestType, successType, failureType) {
return {
type: requestType,
payload: {
}
},
};
};
@ -119,8 +119,8 @@ export default function (action, requestType, successType, failureType) {
return {
type: failureType,
payload: {
error: error
}
error: error,
},
};
};
@ -144,7 +144,7 @@ export default function (action, requestType, successType, failureType) {
// Set extra params for pagination
let extraParams = {
offset: offset,
limit: limit
limit: limit,
};
// Handle filter
@ -165,13 +165,13 @@ export default function (action, requestType, successType, failureType) {
dispatch: [
fetchItemsRequest,
null,
fetchItemsFailure
fetchItemsFailure,
],
action: action,
auth: passphrase,
username: username,
extraParams: extraParams
}
extraParams: extraParams,
},
};
};
@ -186,7 +186,7 @@ export default function (action, requestType, successType, failureType) {
*
* Dispatches the CALL_API action to fetch these items.
*/
const loadPaginatedItems = function({ pageNumber = 1, limit = DEFAULT_LIMIT, filter = null, include = [] } = {}) {
const loadPaginatedItems = function ({ pageNumber = 1, limit = DEFAULT_LIMIT, filter = null, include = [] } = {}) {
return (dispatch, getState) => {
// Get credentials from the state
const { auth } = getState();
@ -222,7 +222,7 @@ export default function (action, requestType, successType, failureType) {
*
* Dispatches the CALL_API action to fetch this item.
*/
const loadItem = function({ filter = null, include = [] } = {}) {
const loadItem = function ({ filter = null, include = [] } = {}) {
return (dispatch, getState) => {
// Get credentials from the state
const { auth } = getState();

View File

@ -41,13 +41,13 @@ export function loginKeepAlive(username, token, endpoint) {
null,
error => dispatch => {
dispatch(loginUserFailure(error || new i18nRecord({ id: "app.login.expired", values: {}})));
}
},
],
action: "ping",
auth: token,
username: username,
extraParams: {}
}
extraParams: {},
},
};
}
@ -72,8 +72,8 @@ export function loginUserSuccess(username, token, endpoint, rememberMe, timerID)
token: token,
endpoint: endpoint,
rememberMe: rememberMe,
timerID: timerID
}
timerID: timerID,
},
};
}
@ -94,8 +94,8 @@ export function loginUserFailure(error) {
return {
type: LOGIN_USER_FAILURE,
payload: {
error: error
}
error: error,
},
};
}
@ -111,8 +111,8 @@ export function loginUserExpired(error) {
return {
type: LOGIN_USER_EXPIRED,
payload: {
error: error
}
error: error,
},
};
}
@ -125,7 +125,7 @@ export const LOGIN_USER_REQUEST = "LOGIN_USER_REQUEST";
*/
export function loginUserRequest() {
return {
type: LOGIN_USER_REQUEST
type: LOGIN_USER_REQUEST,
};
}
@ -152,7 +152,7 @@ export function logout() {
Cookies.remove("token");
Cookies.remove("endpoint");
dispatch({
type: LOGOUT_USER
type: LOGOUT_USER,
});
};
}
@ -223,7 +223,7 @@ export function loginUser(username, passwordOrToken, endpoint, rememberMe, redir
// Get token from the API
const token = {
token: jsonData.auth,
expires: new Date(jsonData.sessionExpire)
expires: new Date(jsonData.sessionExpire),
};
// Handle session keep alive timer
const timerID = setInterval(
@ -242,12 +242,12 @@ export function loginUser(username, passwordOrToken, endpoint, rememberMe, redir
// Redirect
dispatch(push(redirect));
},
loginUserFailure
loginUserFailure,
],
action: "handshake",
auth: passphrase,
username: username,
extraParams: {timestamp: time}
}
extraParams: {timestamp: time},
},
};
}

View File

@ -17,8 +17,8 @@ export function pushEntities(entities, refCountType=["album", "artist", "song"])
type: PUSH_ENTITIES,
payload: {
entities: entities,
refCountType: refCountType
}
refCountType: refCountType,
},
};
}
@ -36,8 +36,8 @@ export function incrementRefCount(entities) {
return {
type: INCREMENT_REFCOUNT,
payload: {
entities: entities
}
entities: entities,
},
};
}
@ -55,7 +55,7 @@ export function decrementRefCount(entities) {
return {
type: DECREMENT_REFCOUNT,
payload: {
entities: entities
}
entities: entities,
},
};
}

View File

@ -7,8 +7,8 @@ import { decrementRefCount } from "./entities";
/** Define an action to invalidate results in paginated store. */
export const CLEAR_RESULTS = "CLEAR_RESULTS";
export function clearResults() {
export const CLEAR_PAGINATED_RESULTS = "CLEAR_PAGINATED_RESULTS";
export function clearPaginatedResults() {
return (dispatch, getState) => {
// Decrement reference counter
const paginatedStore = getState().paginated;
@ -18,7 +18,7 @@ export function clearResults() {
// Clear results in store
dispatch({
type: CLEAR_RESULTS
type: CLEAR_PAGINATED_RESULTS,
});
};
}

View File

@ -7,6 +7,6 @@
export const INVALIDATE_STORE = "INVALIDATE_STORE";
export function invalidateStore() {
return {
type: INVALIDATE_STORE
type: INVALIDATE_STORE,
};
}

View File

@ -1,93 +1,284 @@
// TODO: This file is not finished
/**
* These actions are actions acting on the webplayer.
*/
// Other actions
import { decrementRefCount, incrementRefCount } from "./entities";
export const PLAY_PAUSE = "PLAY_PAUSE";
/**
* true to play, false to pause.
* Toggle play / pause for the webplayer.
*
* @param playPause [Optional] True to play, false to pause. If not given,
* toggle the current state.
*
* @return Dispatch a PLAY_PAUSE action.
*/
export function togglePlaying(playPause) {
return (dispatch, getState) => {
let isPlaying = false;
let newIsPlaying = false;
if (typeof playPause !== "undefined") {
isPlaying = playPause;
// If we want to force a mode
newIsPlaying = playPause;
} else {
isPlaying = !(getState().webplayer.isPlaying);
// Else, just toggle
newIsPlaying = !(getState().webplayer.isPlaying);
}
// Dispatch action
dispatch({
type: PLAY_PAUSE,
payload: {
isPlaying: isPlaying
}
isPlaying: newIsPlaying,
},
});
};
}
export const PUSH_PLAYLIST = "PUSH_PLAYLIST";
export function playTrack(trackID) {
export const STOP_PLAYBACK = "STOP_PLAYBACK";
/**
* Stop the webplayer, clearing the playlist.
*
* Handle the entities store reference counting.
*
* @return Dispatch a STOP_PLAYBACK action.
*/
export function stopPlayback() {
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]
]
}
// Handle reference counting
dispatch(decrementRefCount({
song: getState().webplayer.get("playlist").toArray(),
}));
// Stop playback
dispatch ({
type: STOP_PLAYBACK,
});
};
}
export const SET_PLAYLIST = "SET_PLAYLIST";
/**
* Set a given playlist.
*
* Handle the entities store reference counting.
*
* @param playlist A list of song IDs.
*
* @return Dispatch a SET_PLAYLIST action.
*/
export function setPlaylist(playlist) {
return (dispatch, getState) => {
// Handle reference counting
dispatch(decrementRefCount({
song: getState().webplayer.get("playlist").toArray(),
}));
dispatch(incrementRefCount({
song: playlist,
}));
// Set playlist
dispatch ({
type: SET_PLAYLIST,
payload: {
playlist: playlist,
},
});
};
}
/**
* Play a given song, emptying the current playlist.
*
* Handle the entities store reference counting.
*
* @param songID The id of the song to play.
*
* @return Dispatch a SET_PLAYLIST action to play this song and start playing.
*/
export function playSong(songID) {
return (dispatch, getState) => {
// Handle reference counting
dispatch(decrementRefCount({
song: getState().webplayer.get("playlist").toArray(),
}));
dispatch(incrementRefCount({
song: [songID],
}));
// Set new playlist
dispatch({
type: SET_PLAYLIST,
payload: {
playlist: [songID],
},
});
// Force playing
dispatch(togglePlaying(true));
};
}
export const CHANGE_TRACK = "CHANGE_TRACK";
export const PUSH_SONG = "PUSH_SONG";
/**
* Push a given song in the playlist.
*
* Handle the entities store reference counting.
*
* @param songID The id of the song to push.
* @param index [Optional] The position to insert at in the playlist.
* If negative, counts from the end. Defaults to last.
*
* @return Dispatch a PUSH_SONG action.
*/
export function pushSong(songID, index=-1) {
return (dispatch) => {
// Handle reference counting
dispatch(incrementRefCount({
song: [songID],
}));
// Push song
dispatch({
type: PUSH_SONG,
payload: {
song: songID,
index: index,
},
});
};
}
export const POP_SONG = "POP_SONG";
/**
* Pop a given song from the playlist.
*
* Handle the entities store reference counting.
*
* @param songID The id of the song to pop.
*
* @return Dispatch a POP_SONG action.
*/
export function popSong(songID) {
return (dispatch) => {
// Handle reference counting
dispatch(decrementRefCount({
song: [songID],
}));
// Pop song
dispatch({
type: POP_SONG,
payload: {
song: songID,
},
});
};
}
export const JUMP_TO_SONG = "JUMP_TO_SONG";
/**
* Set current playlist index to specific song.
*
* @param songID The id of the song to play.
*
* @return Dispatch a JUMP_TO_SONG action.
*/
export function jumpToSong(songID) {
return (dispatch) => {
// Push song
dispatch({
type: JUMP_TO_SONG,
payload: {
song: songID,
},
});
};
}
export const PLAY_PREVIOUS = "PLAY_PREVIOUS";
/**
* Move one song backwards in the playlist.
*
* @return Dispatch a PLAY_PREVIOUS action.
*/
export function playPrevious() {
// TODO: Playlist overflow
return (dispatch, getState) => {
let { index } = getState().webplayer;
return (dispatch) => {
dispatch({
type: CHANGE_TRACK,
payload: {
index: index - 1
}
type: PLAY_PREVIOUS,
});
};
}
export const PLAY_NEXT = "PLAY_NEXT";
/**
* Move one song forward in the playlist.
*
* @return Dispatch a PLAY_NEXT action.
*/
export function playNext() {
// TODO: Playlist overflow
return (dispatch, getState) => {
let { index } = getState().webplayer;
return (dispatch) => {
dispatch({
type: CHANGE_TRACK,
payload: {
index: index + 1
}
type: PLAY_NEXT,
});
};
}
export const TOGGLE_RANDOM = "TOGGLE_RANDOM";
/**
* Toggle random mode.
*
* @return Dispatch a TOGGLE_RANDOM action.
*/
export function toggleRandom() {
return {
type: TOGGLE_RANDOM
type: TOGGLE_RANDOM,
};
}
export const TOGGLE_REPEAT = "TOGGLE_REPEAT";
/**
* Toggle repeat mode.
*
* @return Dispatch a TOGGLE_REPEAT action.
*/
export function toggleRepeat() {
return {
type: TOGGLE_REPEAT
type: TOGGLE_REPEAT,
};
}
export const TOGGLE_MUTE = "TOGGLE_MUTE";
/**
* Toggle mute mode.
*
* @return Dispatch a TOGGLE_MUTE action.
*/
export function toggleMute() {
return {
type: TOGGLE_MUTE
type: TOGGLE_MUTE,
};
}
export const SET_VOLUME = "SET_VOLUME";
/**
* Set the volume.
*
* @param volume Volume to set (between 0 and 100)
*
* @return Dispatch a SET_VOLUME action.
*/
export function setVolume(volume) {
return {
type: SET_VOLUME,
payload: {
volume: volume,
},
};
}

View File

@ -12,8 +12,8 @@
*
* @return The element it was applied one, for chaining.
*/
$.fn.shake = function(intShakes, intDistance, intDuration) {
this.each(function() {
$.fn.shake = function (intShakes, intDistance, intDuration) {
this.each(function () {
$(this).css("position","relative");
for (let x=1; x<=intShakes; x++) {
$(this).animate({left:(intDistance*-1)}, (((intDuration/intShakes)/4)))

View File

@ -22,7 +22,7 @@ const albumMessages = defineMessages(messagesMap(Array.concat([], commonMessages
* Track row in an album tracks table.
*/
class AlbumTrackRowCSSIntl extends Component {
render () {
render() {
const { formatMessage } = this.props.intl;
const length = formatLength(this.props.track.get("time"));
return (
@ -45,7 +45,7 @@ class AlbumTrackRowCSSIntl extends Component {
AlbumTrackRowCSSIntl.propTypes = {
playAction: PropTypes.func.isRequired,
track: PropTypes.instanceOf(Immutable.Map).isRequired,
intl: intlShape.isRequired
intl: intlShape.isRequired,
};
export let AlbumTrackRow = injectIntl(CSSModules(AlbumTrackRowCSSIntl, css));
@ -54,7 +54,7 @@ export let AlbumTrackRow = injectIntl(CSSModules(AlbumTrackRowCSSIntl, css));
* Tracks table of an album.
*/
class AlbumTracksTableCSS extends Component {
render () {
render() {
let rows = [];
// Build rows for each track
const playAction = this.props.playAction;
@ -72,7 +72,7 @@ class AlbumTracksTableCSS extends Component {
}
AlbumTracksTableCSS.propTypes = {
playAction: PropTypes.func.isRequired,
tracks: PropTypes.instanceOf(Immutable.List).isRequired
tracks: PropTypes.instanceOf(Immutable.List).isRequired,
};
export let AlbumTracksTable = CSSModules(AlbumTracksTableCSS, css);
@ -81,7 +81,7 @@ export let AlbumTracksTable = CSSModules(AlbumTracksTableCSS, css);
* An entire album row containing art and tracks table.
*/
class AlbumRowCSS extends Component {
render () {
render() {
return (
<div className="row" styleName="row">
<div className="col-sm-offset-2 col-xs-9 col-sm-10" styleName="nameRow">
@ -104,6 +104,6 @@ class AlbumRowCSS extends Component {
AlbumRowCSS.propTypes = {
playAction: PropTypes.func.isRequired,
album: PropTypes.instanceOf(Immutable.Map).isRequired,
songs: PropTypes.instanceOf(Immutable.List).isRequired
songs: PropTypes.instanceOf(Immutable.List).isRequired,
};
export let AlbumRow = CSSModules(AlbumRowCSS, css);

View File

@ -11,7 +11,7 @@ import DismissibleAlert from "./elements/DismissibleAlert";
* Paginated albums grid
*/
export default class Albums extends Component {
render () {
render() {
// Handle error
let error = null;
if (this.props.error) {
@ -25,7 +25,7 @@ export default class Albums extends Component {
itemsType: "album",
itemsLabel: "app.common.album",
subItemsType: "tracks",
subItemsLabel: "app.common.track"
subItemsLabel: "app.common.track",
};
return (

View File

@ -26,7 +26,7 @@ const artistMessages = defineMessages(messagesMap(Array.concat([], commonMessage
* Single artist page
*/
class ArtistCSS extends Component {
render () {
render() {
// Define loading message
let loading = null;
if (this.props.isFetching) {
@ -87,6 +87,6 @@ ArtistCSS.propTypes = {
playAction: PropTypes.func.isRequired,
artist: PropTypes.instanceOf(Immutable.Map),
albums: PropTypes.instanceOf(Immutable.List),
songs: PropTypes.instanceOf(Immutable.Map)
songs: PropTypes.instanceOf(Immutable.Map),
};
export default CSSModules(ArtistCSS, css);

View File

@ -11,7 +11,7 @@ import DismissibleAlert from "./elements/DismissibleAlert";
* Paginated artists grid
*/
export default class Artists extends Component {
render () {
render() {
// Handle error
let error = null;
if (this.props.error) {
@ -25,7 +25,7 @@ export default class Artists extends Component {
itemsType: "artist",
itemsLabel: "app.common.artist",
subItemsType: "albums",
subItemsLabel: "app.common.album"
subItemsLabel: "app.common.album",
};
return (

View File

@ -6,7 +6,7 @@ import FontAwesome from "react-fontawesome";
import css from "../styles/Discover.scss";
export default class DiscoverCSS extends Component {
render () {
render() {
const artistsAlbumsSongsDropdown = (
<div className="btn-group">
<button type="button" className="btn btn-default dropdown-toggle" styleName="h2Title" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">

View File

@ -23,7 +23,7 @@ const loginMessages = defineMessages(messagesMap(Array.concat([], APIMessages, m
* Login form component
*/
class LoginFormCSSIntl extends Component {
constructor (props) {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this); // bind this to handleSubmit
}
@ -36,7 +36,7 @@ class LoginFormCSSIntl extends Component {
*
* @return True if an error is set, false otherwise
*/
setError (formGroup, hasError) {
setError(formGroup, hasError) {
if (hasError) {
// If error is true, then add error class
formGroup.classList.add("has-error");
@ -54,7 +54,7 @@ class LoginFormCSSIntl extends Component {
*
* @param e JS Event.
*/
handleSubmit (e) {
handleSubmit(e) {
e.preventDefault();
// Don't handle submit if already logging in
@ -79,7 +79,7 @@ class LoginFormCSSIntl extends Component {
}
}
componentDidUpdate () {
componentDidUpdate() {
if (this.props.error) {
// On unsuccessful login, set error classes and shake the form
$(this.refs.loginForm).shake(3, 10, 300);
@ -89,7 +89,7 @@ class LoginFormCSSIntl extends Component {
}
}
render () {
render() {
const {formatMessage} = this.props.intl;
// Handle info message
@ -187,7 +187,7 @@ export let LoginForm = injectIntl(CSSModules(LoginFormCSSIntl, css));
* Main login page, including title and login form.
*/
class LoginCSS extends Component {
render () {
render() {
const greeting = (
<p>
<FormattedMessage {...loginMessages["app.login.greeting"]} />
@ -212,6 +212,6 @@ LoginCSS.propTypes = {
onSubmit: PropTypes.func.isRequired,
isAuthenticating: PropTypes.bool,
info: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
error: PropTypes.oneOfType([PropTypes.string, PropTypes.object])
error: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
};
export default CSSModules(LoginCSS, css);

View File

@ -30,7 +30,7 @@ const songsMessages = defineMessages(messagesMap(Array.concat([], commonMessages
* A single row for a single song in the songs table.
*/
class SongsTableRowCSSIntl extends Component {
render () {
render() {
const { formatMessage } = this.props.intl;
const length = formatLength(this.props.song.get("time"));
@ -59,7 +59,7 @@ class SongsTableRowCSSIntl extends Component {
SongsTableRowCSSIntl.propTypes = {
playAction: PropTypes.func.isRequired,
song: PropTypes.instanceOf(Immutable.Map).isRequired,
intl: intlShape.isRequired
intl: intlShape.isRequired,
};
export let SongsTableRow = injectIntl(CSSModules(SongsTableRowCSSIntl, css));
@ -68,7 +68,7 @@ export let SongsTableRow = injectIntl(CSSModules(SongsTableRowCSSIntl, css));
* The songs table.
*/
class SongsTableCSS extends Component {
render () {
render() {
// Handle filtering
let displayedSongs = this.props.songs;
if (this.props.filterText) {
@ -78,7 +78,7 @@ class SongsTableCSS extends Component {
{
"keys": ["name"],
"threshold": 0.4,
"include": ["score"]
"include": ["score"],
}).search(this.props.filterText);
// Keep only items in results
displayedSongs = displayedSongs.map(function (item) { return new Immutable.Map(item.item); });
@ -135,7 +135,7 @@ class SongsTableCSS extends Component {
SongsTableCSS.propTypes = {
playAction: PropTypes.func.isRequired,
songs: PropTypes.instanceOf(Immutable.List).isRequired,
filterText: PropTypes.string
filterText: PropTypes.string,
};
export let SongsTable = CSSModules(SongsTableCSS, css);
@ -144,10 +144,10 @@ export let SongsTable = CSSModules(SongsTableCSS, css);
* Complete songs table view with filter and pagination
*/
export default class FilterablePaginatedSongsTable extends Component {
constructor (props) {
constructor(props) {
super(props);
this.state = {
filterText: "" // Initial state, no filter text
filterText: "", // Initial state, no filter text
};
this.handleUserInput = this.handleUserInput.bind(this); // Bind this on user input handling
@ -160,13 +160,13 @@ export default class FilterablePaginatedSongsTable extends Component {
*
* @param filterText Content of the filter input.
*/
handleUserInput (filterText) {
handleUserInput(filterText) {
this.setState({
filterText: filterText
filterText: filterText,
});
}
render () {
render() {
// Handle error
let error = null;
if (this.props.error) {
@ -176,13 +176,13 @@ export default class FilterablePaginatedSongsTable extends Component {
// Set props
const filterProps = {
filterText: this.state.filterText,
onUserInput: this.handleUserInput
onUserInput: this.handleUserInput,
};
const songsTableProps = {
playAction: this.props.playAction,
isFetching: this.props.isFetching,
songs: this.props.songs,
filterText: this.state.filterText
filterText: this.state.filterText,
};
return (
@ -200,5 +200,5 @@ FilterablePaginatedSongsTable.propTypes = {
isFetching: PropTypes.bool.isRequired,
error: PropTypes.string,
songs: PropTypes.instanceOf(Immutable.List).isRequired,
pagination: PropTypes.object.isRequired
pagination: PropTypes.object.isRequired,
};

View File

@ -6,7 +6,7 @@ import React, { Component, PropTypes } from "react";
* A dismissible Bootstrap alert.
*/
export default class DismissibleAlert extends Component {
render () {
render() {
// Set correct alert type
let alertType = "alert-danger";
if (this.props.type) {
@ -27,5 +27,5 @@ export default class DismissibleAlert extends Component {
}
DismissibleAlert.propTypes = {
type: PropTypes.string,
text: PropTypes.string
text: PropTypes.string,
};

View File

@ -20,7 +20,7 @@ const filterMessages = defineMessages(messagesMap(Array.concat([], messages)));
* Filter bar element with input filter.
*/
class FilterBarCSSIntl extends Component {
constructor (props) {
constructor(props) {
super(props);
// Bind this on methods
this.handleChange = this.handleChange.bind(this);
@ -33,12 +33,12 @@ class FilterBarCSSIntl extends Component {
*
* @param e A JS event.
*/
handleChange (e) {
handleChange(e) {
e.preventDefault();
this.props.onUserInput(this.refs.filterTextInput.value);
}
render () {
render() {
const {formatMessage} = this.props.intl;
return (
@ -60,6 +60,6 @@ class FilterBarCSSIntl extends Component {
FilterBarCSSIntl.propTypes = {
onUserInput: PropTypes.func,
filterText: PropTypes.string,
intl: intlShape.isRequired
intl: intlShape.isRequired,
};
export default injectIntl(CSSModules(FilterBarCSSIntl, css));

View File

@ -31,7 +31,7 @@ const gridMessages = defineMessages(messagesMap(Array.concat([], commonMessages,
const ISOTOPE_OPTIONS = { /** Default options for Isotope grid layout. */
getSortData: {
name: ".name",
nSubitems: ".sub-items .n-sub-items"
nSubitems: ".sub-items .n-sub-items",
},
transitionDuration: 0,
sortBy: "name",
@ -40,8 +40,8 @@ const ISOTOPE_OPTIONS = { /** Default options for Isotope grid layout. */
layoutMode: "fitRows",
filter: "*",
fitRows: {
gutter: 0
}
gutter: 0,
},
};
@ -49,7 +49,7 @@ const ISOTOPE_OPTIONS = { /** Default options for Isotope grid layout. */
* A single item in the grid, art + text under the art.
*/
class GridItemCSSIntl extends Component {
render () {
render() {
const {formatMessage} = this.props.intl;
// Get number of sub-items
@ -85,7 +85,7 @@ GridItemCSSIntl.propTypes = {
itemsLabel: PropTypes.string.isRequired,
subItemsType: PropTypes.string.isRequired,
subItemsLabel: PropTypes.string.isRequired,
intl: intlShape.isRequired
intl: intlShape.isRequired,
};
export let GridItem = injectIntl(CSSModules(GridItemCSSIntl, css));
@ -94,7 +94,7 @@ export let GridItem = injectIntl(CSSModules(GridItemCSSIntl, css));
* A grid, formatted using Isotope.JS
*/
export class Grid extends Component {
constructor (props) {
constructor(props) {
super(props);
// Init grid data member
@ -108,7 +108,7 @@ export class Grid extends Component {
/**
* Create an isotope container if none already exist.
*/
createIsotopeContainer () {
createIsotopeContainer() {
if (this.iso == null) {
this.iso = new Isotope(this.refs.grid, ISOTOPE_OPTIONS);
}
@ -117,7 +117,7 @@ export class Grid extends Component {
/**
* Handle filtering on the grid.
*/
handleFiltering (props) {
handleFiltering(props) {
// If no query provided, drop any filter in use
if (props.filterText == "") {
return this.iso.arrange(ISOTOPE_OPTIONS);
@ -129,7 +129,7 @@ export class Grid extends Component {
{
"keys": ["name"],
"threshold": 0.4,
"include": ["score"]
"include": ["score"],
}
).search(props.filterText);
@ -149,9 +149,9 @@ export class Grid extends Component {
}
return p;
}, 0);
}
},
},
sortBy: "relevance"
sortBy: "relevance",
});
this.iso.updateSortData();
this.iso.arrange();
@ -169,7 +169,7 @@ export class Grid extends Component {
}
}
componentDidMount () {
componentDidMount() {
// Setup grid
this.createIsotopeContainer();
// Only arrange if there are elements to arrange
@ -212,7 +212,7 @@ export class Grid extends Component {
}
// Layout again after images are loaded
imagesLoaded(this.refs.grid).on("progress", function() {
imagesLoaded(this.refs.grid).on("progress", function () {
// Layout after each image load, fix for responsive grid
if (!iso) { // Grid could have been destroyed in the meantime
return;
@ -221,7 +221,7 @@ export class Grid extends Component {
});
}
render () {
render() {
// Handle loading
let loading = null;
if (this.props.isFetching) {
@ -264,7 +264,7 @@ Grid.propTypes = {
itemsLabel: PropTypes.string.isRequired,
subItemsType: PropTypes.string.isRequired,
subItemsLabel: PropTypes.string.isRequired,
filterText: PropTypes.string
filterText: PropTypes.string,
};
@ -272,11 +272,11 @@ Grid.propTypes = {
* Full grid with pagination and filtering input.
*/
export default class FilterablePaginatedGrid extends Component {
constructor (props) {
constructor(props) {
super(props);
this.state = {
filterText: "" // No filterText at init
filterText: "", // No filterText at init
};
// Bind this
@ -290,13 +290,13 @@ export default class FilterablePaginatedGrid extends Component {
*
* @param filterText Content of the filter input.
*/
handleUserInput (filterText) {
handleUserInput(filterText) {
this.setState({
filterText: filterText
filterText: filterText,
});
}
render () {
render() {
return (
<div>
<FilterBar filterText={this.state.filterText} onUserInput={this.handleUserInput} />
@ -309,5 +309,5 @@ export default class FilterablePaginatedGrid extends Component {
FilterablePaginatedGrid.propTypes = {
grid: PropTypes.object.isRequired,
pagination: PropTypes.object.isRequired
pagination: PropTypes.object.isRequired,
};

View File

@ -22,7 +22,7 @@ const paginationMessages = defineMessages(messagesMap(Array.concat([], commonMes
* Pagination button bar
*/
class PaginationCSSIntl extends Component {
constructor (props) {
constructor(props) {
super (props);
// Bind this
@ -74,7 +74,7 @@ class PaginationCSSIntl extends Component {
$(this.refs.paginationModal).modal("hide");
}
render () {
render() {
const { formatMessage } = this.props.intl;
// Get bounds

View File

@ -1,47 +1,66 @@
// TODO: This file is to review
// NPM imports
import React, { Component, PropTypes } from "react";
import CSSModules from "react-css-modules";
import { defineMessages, injectIntl, intlShape, FormattedMessage } from "react-intl";
import FontAwesome from "react-fontawesome";
import Immutable from "immutable";
import FontAwesome from "react-fontawesome";
// Local imports
import { messagesMap } from "../../utils";
// Styles
import css from "../../styles/elements/WebPlayer.scss";
// Translations
import commonMessages from "../../locales/messagesDescriptors/common";
import messages from "../../locales/messagesDescriptors/elements/WebPlayer";
// Define translations
const webplayerMessages = defineMessages(messagesMap(Array.concat([], commonMessages, messages)));
/**
* Webplayer component.
*/
class WebPlayerCSSIntl extends Component {
constructor (props) {
constructor(props) {
super(props);
// Bind this
this.artOpacityHandler = this.artOpacityHandler.bind(this);
}
artOpacityHandler (ev) {
/**
* Handle opacity on album art.
*
* Set opacity on mouseover / mouseout.
*
* @param ev A JS event.
*/
artOpacityHandler(ev) {
if (ev.type == "mouseover") {
// On mouse over, reduce opacity
this.refs.art.style.opacity = "1";
this.refs.artText.style.display = "none";
} else {
// On mouse out, set opacity back
this.refs.art.style.opacity = "0.75";
this.refs.artText.style.display = "block";
}
}
render () {
render() {
const { formatMessage } = this.props.intl;
const song = this.props.currentTrack;
if (!song) {
return (<div></div>);
}
// Get current song (eventually undefined)
const song = this.props.currentSong;
// Current status (play or pause) for localization
const playPause = this.props.isPlaying ? "pause" : "play";
const volumeMute = this.props.isMute ? "volume-off" : "volume-up";
// Volume fontawesome icon
const volumeIcon = this.props.isMute ? "volume-off" : "volume-up";
// Get classes for random and repeat buttons
const randomBtnStyles = ["randomBtn"];
const repeatBtnStyles = ["repeatBtn"];
if (this.props.isRandom) {
@ -51,18 +70,30 @@ class WebPlayerCSSIntl extends Component {
repeatBtnStyles.push("active");
}
// Check if a song is currently playing
let art = null;
let songTitle = null;
let artistName = null;
if (song) {
art = song.get("art");
songTitle = song.get("title");
if (this.props.currentArtist) {
artistName = this.props.currentArtist.get("name");
}
}
return (
<div id="row" styleName="webplayer">
<div className="col-xs-12">
<div className="row" styleName="artRow" onMouseOver={this.artOpacityHandler} onMouseOut={this.artOpacityHandler}>
<div className="col-xs-12">
<img src={song.get("art")} width="200" height="200" alt={formatMessage(webplayerMessages["app.common.art"])} ref="art" styleName="art" />
<img src={art} width="200" height="200" alt={formatMessage(webplayerMessages["app.common.art"])} ref="art" styleName="art" />
<div ref="artText">
<h2>{song.get("title")}</h2>
<h2>{songTitle}</h2>
<h3>
<span className="text-capitalize">
<FormattedMessage {...webplayerMessages["app.webplayer.by"]} />
</span> { this.props.currentArtist.get("name") }
</span> { artistName }
</h3>
</div>
</div>
@ -82,7 +113,7 @@ class WebPlayerCSSIntl extends Component {
</div>
<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}>
<FontAwesome name={volumeMute} />
<FontAwesome name={volumeIcon} />
</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}>
<FontAwesome name="repeat" />
@ -106,7 +137,10 @@ WebPlayerCSSIntl.propTypes = {
isRandom: PropTypes.bool.isRequired,
isRepeat: PropTypes.bool.isRequired,
isMute: PropTypes.bool.isRequired,
currentTrack: PropTypes.instanceOf(Immutable.Map),
volume: PropTypes.number.isRequired,
currentIndex: PropTypes.number.isRequired,
playlist: PropTypes.instanceOf(Immutable.List).isRequired,
currentSong: PropTypes.instanceOf(Immutable.Map),
currentArtist: PropTypes.instanceOf(Immutable.Map),
onPlayPause: PropTypes.func.isRequired,
onPrev: PropTypes.func.isRequired,
@ -114,7 +148,7 @@ WebPlayerCSSIntl.propTypes = {
onRandom: PropTypes.func.isRequired,
onRepeat: PropTypes.func.isRequired,
onMute: PropTypes.func.isRequired,
intl: intlShape.isRequired
intl: intlShape.isRequired,
};
export default injectIntl(CSSModules(WebPlayerCSSIntl, css, { allowMultiple: true }));

View File

@ -8,7 +8,7 @@ import { defineMessages, injectIntl, intlShape, FormattedMessage } from "react-i
import { messagesMap } from "../../utils";
// Other components
/* import WebPlayer from "../../views/WebPlayer"; TODO */
import WebPlayer from "../../views/WebPlayer";
// Translations
import commonMessages from "../../locales/messagesDescriptors/common";
@ -25,7 +25,7 @@ const sidebarLayoutMessages = defineMessages(messagesMap(Array.concat([], common
* Sidebar layout component, putting children next to the sidebar menu.
*/
class SidebarLayoutIntl extends Component {
render () {
render() {
const { formatMessage } = this.props.intl;
// Check active links
@ -35,7 +35,7 @@ class SidebarLayoutIntl extends Component {
artists: (this.props.location.pathname == "/artists") ? "active" : "link",
albums: (this.props.location.pathname == "/albums") ? "active" : "link",
songs: (this.props.location.pathname == "/songs") ? "active" : "link",
search: (this.props.location.pathname == "/search") ? "active" : "link"
search: (this.props.location.pathname == "/search") ? "active" : "link",
};
// Hamburger collapsing function
@ -146,7 +146,7 @@ class SidebarLayoutIntl extends Component {
</li>
</ul>
</nav>
{ /** TODO <WebPlayer /> */ }
<WebPlayer />
</div>
</div>
@ -159,6 +159,6 @@ class SidebarLayoutIntl extends Component {
}
SidebarLayoutIntl.propTypes = {
children: PropTypes.node,
intl: intlShape.isRequired
intl: intlShape.isRequired,
};
export default injectIntl(CSSModules(SidebarLayoutIntl, css));

View File

@ -6,7 +6,7 @@ import React, { Component } from "react";
* Simple layout, meaning just enclosing children in a div.
*/
export default class SimpleLayout extends Component {
render () {
render() {
return (
<div>
{this.props.children}

View File

@ -6,7 +6,7 @@
import React, { Component, PropTypes } from "react";
export default class App extends Component {
render () {
render() {
return (
<div>
{this.props.children}

View File

@ -7,12 +7,12 @@ import { connect } from "react-redux";
export class RequireAuthentication extends Component {
componentWillMount () {
componentWillMount() {
// Check authentication on mount
this.checkAuth(this.props.isAuthenticated);
}
componentWillUpdate (newProps) {
componentWillUpdate(newProps) {
// Check authentication on update
this.checkAuth(newProps.isAuthenticated);
}
@ -23,20 +23,20 @@ export class RequireAuthentication extends Component {
* @param isAuthenticated A boolean stating whether user has a valid
* session or not.
*/
checkAuth (isAuthenticated) {
checkAuth(isAuthenticated) {
if (!isAuthenticated) {
// Redirect to login, redirecting to the actual page after login.
this.context.router.replace({
pathname: "/login",
state: {
nextPathname: this.props.location.pathname,
nextQuery: this.props.location.query
}
nextQuery: this.props.location.query,
},
});
}
}
render () {
render() {
return (
<div>
{this.props.isAuthenticated === true
@ -50,15 +50,15 @@ export class RequireAuthentication extends Component {
RequireAuthentication.propTypes = {
// Injected by React Router
children: PropTypes.node
children: PropTypes.node,
};
RequireAuthentication.contextTypes = {
router: PropTypes.object.isRequired
router: PropTypes.object.isRequired,
};
const mapStateToProps = (state) => ({
isAuthenticated: state.auth.isAuthenticated
isAuthenticated: state.auth.isAuthenticated,
});
export default connect(mapStateToProps)(RequireAuthentication);

View File

@ -27,5 +27,5 @@ Root.propTypes = {
render: PropTypes.func,
locale: PropTypes.string.isRequired,
messages: PropTypes.object.isRequired,
defaultLocale: PropTypes.string.isRequired
defaultLocale: PropTypes.string.isRequired,
};

View File

@ -1,5 +1,5 @@
// Export all the existing locales
module.exports = {
"en-US": require("./en-US"),
"fr-FR": require("./fr-FR")
"fr-FR": require("./fr-FR"),
};

View File

@ -2,55 +2,55 @@ const messages = [
{
id: "app.login.username",
defaultMessage: "Username",
description: "Username input placeholder"
description: "Username input placeholder",
},
{
id: "app.login.password",
defaultMessage: "Password",
description: "Password input placeholder"
description: "Password input placeholder",
},
{
id: "app.login.signIn",
defaultMessage: "Sign in",
description: "Sign in"
description: "Sign in",
},
{
id: "app.login.endpointInputAriaLabel",
defaultMessage: "URL of your Ampache instance (e.g. http://ampache.example.com)",
description: "ARIA label for the endpoint input"
description: "ARIA label for the endpoint input",
},
{
id: "app.login.rememberMe",
description: "Remember me checkbox label",
defaultMessage: "Remember me"
defaultMessage: "Remember me",
},
{
id: "app.login.greeting",
description: "Greeting to welcome the user to the app",
defaultMessage: "Welcome back on Ampache, let's go!"
defaultMessage: "Welcome back on Ampache, let's go!",
},
// From the auth reducer
{
id: "app.login.connecting",
defaultMessage: "Connecting…",
description: "Info message while trying to connect"
description: "Info message while trying to connect",
},
{
id: "app.login.success",
defaultMessage: "Successfully logged in as { username }!",
description: "Info message on successful login."
description: "Info message on successful login.",
},
{
id: "app.login.byebye",
defaultMessage: "See you soon!",
description: "Info message on successful logout"
description: "Info message on successful logout",
},
{
id: "app.login.expired",
defaultMessage: "Your session expired… =(",
description: "Error message on expired session"
}
description: "Error message on expired session",
},
];
export default messages;

View File

@ -2,18 +2,18 @@ const messages = [
{
"id": "app.songs.title",
"description": "Title (song)",
"defaultMessage": "Title"
"defaultMessage": "Title",
},
{
"id": "app.songs.genre",
"description": "Genre (song)",
"defaultMessage": "Genre"
"defaultMessage": "Genre",
},
{
"id": "app.songs.length",
"description": "Length (song)",
"defaultMessage": "Length"
}
"defaultMessage": "Length",
},
];
export default messages;

View File

@ -2,18 +2,18 @@ const messages = [
{
id: "app.api.invalidResponse",
defaultMessage: "Invalid response text.",
description: "Invalid response from the API"
description: "Invalid response from the API",
},
{
id: "app.api.emptyResponse",
defaultMessage: "Empty response text.",
description: "Empty response from the API"
description: "Empty response from the API",
},
{
id: "app.api.error",
defaultMessage: "Unknown API error.",
description: "An unknown error occurred from the API"
}
description: "An unknown error occurred from the API",
},
];
export default messages;

View File

@ -2,52 +2,52 @@ const messages = [
{
id: "app.common.close",
defaultMessage: "Close",
description: "Close"
description: "Close",
},
{
id: "app.common.cancel",
description: "Cancel",
defaultMessage: "Cancel"
defaultMessage: "Cancel",
},
{
id: "app.common.go",
description: "Go",
defaultMessage: "Go"
defaultMessage: "Go",
},
{
id: "app.common.art",
description: "Art",
defaultMessage: "Art"
defaultMessage: "Art",
},
{
id: "app.common.artist",
description: "Artist",
defaultMessage: "{itemCount, plural, one {artist} other {artists}}"
defaultMessage: "{itemCount, plural, one {artist} other {artists}}",
},
{
id: "app.common.album",
description: "Album",
defaultMessage: "{itemCount, plural, one {album} other {albums}}"
defaultMessage: "{itemCount, plural, one {album} other {albums}}",
},
{
id: "app.common.track",
description: "Track",
defaultMessage: "{itemCount, plural, one {track} other {tracks}}"
defaultMessage: "{itemCount, plural, one {track} other {tracks}}",
},
{
id: "app.common.loading",
description: "Loading indicator",
defaultMessage: "Loading…"
defaultMessage: "Loading…",
},
{
id: "app.common.play",
description: "Play icon description",
defaultMessage: "Play"
defaultMessage: "Play",
},
{
id: "app.common.pause",
description: "Pause icon description",
defaultMessage: "Pause"
defaultMessage: "Pause",
},
];

View File

@ -2,12 +2,12 @@ const messages = [
{
id: "app.filter.filter",
defaultMessage: "Filter…",
description: "Filtering input placeholder"
description: "Filtering input placeholder",
},
{
id: "app.filter.whatAreWeListeningToToday",
description: "Description for the filter bar",
defaultMessage: "What are we listening to today?"
defaultMessage: "What are we listening to today?",
},
];

View File

@ -2,28 +2,28 @@ const messages = [
{
id: "app.pagination.goToPage",
defaultMessage: "<span class=\"sr-only\">Go to page </span>{pageNumber}",
description: "Link content to go to page N. span is here for screen-readers"
description: "Link content to go to page N. span is here for screen-readers",
},
{
id: "app.pagination.goToPageWithoutMarkup",
defaultMessage: "Go to page {pageNumber}",
description: "Link title to go to page N"
description: "Link title to go to page N",
},
{
id: "app.pagination.pageNavigation",
defaultMessage: "Page navigation",
description: "ARIA label for the nav block containing pagination"
description: "ARIA label for the nav block containing pagination",
},
{
id: "app.pagination.pageToGoTo",
description: "Title of the pagination modal",
defaultMessage: "Page to go to?"
defaultMessage: "Page to go to?",
},
{
id: "app.pagination.current",
description: "Current (page)",
defaultMessage: "current"
}
defaultMessage: "current",
},
];
export default messages;

View File

@ -2,38 +2,38 @@ const messages = [
{
id: "app.webplayer.by",
defaultMessage: "by",
description: "Artist affiliation of a song"
description: "Artist affiliation of a song",
},
{
id: "app.webplayer.previous",
defaultMessage: "Previous",
description: "Previous button description"
description: "Previous button description",
},
{
id: "app.webplayer.next",
defaultMessage: "Next",
description: "Next button description"
description: "Next button description",
},
{
id: "app.webplayer.volume",
defaultMessage: "Volume",
description: "Volume button description"
description: "Volume button description",
},
{
id: "app.webplayer.repeat",
defaultMessage: "Repeat",
description: "Repeat button description"
description: "Repeat button description",
},
{
id: "app.webplayer.random",
defaultMessage: "Random",
description: "Random button description"
description: "Random button description",
},
{
id: "app.webplayer.playlist",
defaultMessage: "Playlist",
description: "Playlist button description"
}
description: "Playlist button description",
},
];
export default messages;

View File

@ -2,13 +2,13 @@ const messages = [
{
id: "app.grid.goToArtistPage",
defaultMessage: "Go to artist page",
description: "Artist thumbnail link title"
description: "Artist thumbnail link title",
},
{
id: "app.grid.goToAlbumPage",
defaultMessage: "Go to album page",
description: "Album thumbnail link title"
}
description: "Album thumbnail link title",
},
];
export default messages;

View File

@ -2,53 +2,53 @@ const messages = [
{
id: "app.sidebarLayout.mainNavigationMenu",
description: "ARIA label for the main navigation menu",
defaultMessage: "Main navigation menu"
defaultMessage: "Main navigation menu",
},
{
id: "app.sidebarLayout.home",
description: "Home",
defaultMessage: "Home"
defaultMessage: "Home",
},
{
id: "app.sidebarLayout.settings",
description: "Settings",
defaultMessage: "Settings"
defaultMessage: "Settings",
},
{
id: "app.sidebarLayout.logout",
description: "Logout",
defaultMessage: "Logout"
defaultMessage: "Logout",
},
{
id: "app.sidebarLayout.discover",
description: "Discover",
defaultMessage: "Discover"
defaultMessage: "Discover",
},
{
id: "app.sidebarLayout.browse",
description: "Browse",
defaultMessage: "Browse"
defaultMessage: "Browse",
},
{
id: "app.sidebarLayout.browseArtists",
description: "Browse artists",
defaultMessage: "Browse artists"
defaultMessage: "Browse artists",
},
{
id: "app.sidebarLayout.browseAlbums",
description: "Browse albums",
defaultMessage: "Browse albums"
defaultMessage: "Browse albums",
},
{
id: "app.sidebarLayout.browseSongs",
description: "Browse songs",
defaultMessage: "Browse songs"
defaultMessage: "Browse songs",
},
{
id: "app.sidebarLayout.toggleNavigation",
description: "Screen reader description of toggle navigation button",
defaultMessage: "Toggle navigation"
}
defaultMessage: "Toggle navigation",
},
];
export default messages;

View File

@ -29,7 +29,7 @@ class APIError extends Error {}
* @param response A XHR response object.
* @return The response or a rejected Promise if the check failed.
*/
function _checkHTTPStatus (response) {
function _checkHTTPStatus(response) {
if (response.status >= 200 && response.status < 300) {
return response;
} else {
@ -44,17 +44,17 @@ function _checkHTTPStatus (response) {
* @param responseText The text from the API response.
* @return The response as a JS object or a rejected Promise on error.
*/
function _parseToJSON (responseText) {
function _parseToJSON(responseText) {
let x2js = new X2JS({
attributePrefix: "", // No prefix for attributes
keepCData: false // Do not store __cdata and toString functions
keepCData: false, // Do not store __cdata and toString functions
});
if (responseText) {
return x2js.xml_str2json(responseText).root;
}
return Promise.reject(new i18nRecord({
id: "app.api.invalidResponse",
values: {}
values: {},
}));
}
@ -65,14 +65,14 @@ function _parseToJSON (responseText) {
* @param jsonData A JS object representing the API response.
* @return The input data or a rejected Promise if errors are present.
*/
function _checkAPIErrors (jsonData) {
function _checkAPIErrors(jsonData) {
if (jsonData.error) {
return Promise.reject(jsonData.error);
} else if (!jsonData) {
// No data returned
return Promise.reject(new i18nRecord({
id: "app.api.emptyResponse",
values: {}
values: {},
}));
}
return jsonData;
@ -85,7 +85,7 @@ function _checkAPIErrors (jsonData) {
* @param jsonData A JS object representing the API response.
* @return A fixed JS object.
*/
function _uglyFixes (jsonData) {
function _uglyFixes(jsonData) {
// Fix songs array
let _uglyFixesSongs = function (songs) {
return songs.map(function (song) {
@ -201,7 +201,7 @@ function _uglyFixes (jsonData) {
*
* @return A fetching Promise.
*/
function doAPICall (endpoint, action, auth, username, extraParams) {
function doAPICall(endpoint, action, auth, username, extraParams) {
// Translate the API action to real API action
const APIAction = extraParams.filter ? action.rstrip("s") : action;
// Set base params
@ -209,7 +209,7 @@ function doAPICall (endpoint, action, auth, username, extraParams) {
version: API_VERSION,
action: APIAction,
auth: auth,
user: username
user: username,
};
// Extend with extraParams
const params = Object.assign({}, baseParams, extraParams);

View File

@ -14,15 +14,15 @@ export const song = new Schema("song"); /** Song schema */
// Explicit relations between them
artist.define({ // Artist has albums and songs (tracks)
albums: arrayOf(album),
songs: arrayOf(song)
songs: arrayOf(song),
});
album.define({ // Album has artist, tracks and tags
artist: artist,
tracks: arrayOf(song)
tracks: arrayOf(song),
});
song.define({ // Track has artist and album
artist: artist,
album: album
album: album,
});

View File

@ -9,7 +9,7 @@ import Immutable from "immutable";
/** Record to store token parameters */
export const tokenRecord = Immutable.Record({
token: null, /** Token string */
expires: null /** Token expiration date */
expires: null, /** Token expiration date */
});
@ -23,5 +23,5 @@ export const stateRecord = new Immutable.Record({
isAuthenticating: false, /** Whether authentication is in progress or not */
error: null, /** An error string */
info: null, /** An info string */
timerID: null /** Timer ID for setInterval calls to revive API session */
timerID: null, /** Timer ID for setInterval calls to revive API session */
});

View File

@ -12,11 +12,11 @@ export const stateRecord = new Immutable.Record({
refCounts: new Immutable.Map({
album: new Immutable.Map(),
artist: new Immutable.Map(),
song: new Immutable.Map()
song: new Immutable.Map(),
}), /** Map of id => reference count for each object type (garbage collection) */
entities: new Immutable.Map({
album: new Immutable.Map(),
artist: new Immutable.Map(),
song: new Immutable.Map()
}) /** Map of id => entity for each object type */
song: new Immutable.Map(),
}), /** Map of id => entity for each object type */
});

View File

@ -8,5 +8,5 @@ import Immutable from "immutable";
/** i18n record for passing errors to be localized from actions to components */
export const i18nRecord = new Immutable.Record({
id: null, /** Translation message id */
values: new Immutable.Map() /** Values to pass to formatMessage */
values: new Immutable.Map(), /** Values to pass to formatMessage */
});

View File

@ -6,5 +6,5 @@ export const stateRecord = new Immutable.Record({
type: null, /** Type of the paginated entries */
result: new Immutable.List(), /** List of IDs of the resulting entries, maps to the entities store */
currentPage: 1, /** Number of current page */
nPages: 1 /** Total number of page in this batch */
nPages: 1, /** Total number of page in this batch */
});

View File

@ -14,5 +14,5 @@ export const stateRecord = new Immutable.Record({
isMute: false, /** Whether sound is muted or not */
volume: 100, /** Current volume, between 0 and 100 */
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 */
});

View File

@ -68,8 +68,8 @@ export default createReducer(initialState, {
isAuthenticating: true,
info: new i18nRecord({
id: "app.login.connecting",
values: {}
})
values: {},
}),
});
},
[LOGIN_USER_SUCCESS]: (state, payload) => {
@ -81,28 +81,28 @@ export default createReducer(initialState, {
"rememberMe": payload.rememberMe,
"info": new i18nRecord({
id: "app.login.success",
values: {username: payload.username}
values: {username: payload.username},
}),
"timerID": payload.timerID
"timerID": payload.timerID,
});
},
[LOGIN_USER_FAILURE]: (state, payload) => {
return new stateRecord({
"error": payload.error
"error": payload.error,
});
},
[LOGIN_USER_EXPIRED]: (state, payload) => {
return new stateRecord({
"isAuthenticated": false,
"error": payload.error
"error": payload.error,
});
},
[LOGOUT_USER]: () => {
return new stateRecord({
info: new i18nRecord({
id: "app.login.byebye",
values: {}
})
values: {},
}),
});
}
},
});

View File

@ -18,7 +18,7 @@ import {
PUSH_ENTITIES,
INCREMENT_REFCOUNT,
DECREMENT_REFCOUNT,
INVALIDATE_STORE
INVALIDATE_STORE,
} from "../actions";
@ -171,9 +171,11 @@ export default createReducer(initialState, {
// Increment reference counter
payload.refCountType.forEach(function (itemName) {
newState.getIn(["entities", itemName]).forEach(function (entity, id) {
const entities = payload.entities[itemName];
for (let id in entities) {
const entity = newState.getIn(["entities", itemName, id]);
newState = updateEntityRefCount(newState, itemName, id, entity, 1);
});
}
});
return newState;
@ -183,7 +185,9 @@ export default createReducer(initialState, {
// Increment reference counter
for (let itemName in payload.entities) {
newState.getIn(["entities", itemName]).forEach(function (entity, id) {
const entities = payload.entities[itemName];
entities.forEach(function (id) {
const entity = newState.getIn(["entities", itemName, id]);
newState = updateEntityRefCount(newState, itemName, id, entity, 1);
});
}
@ -195,7 +199,9 @@ export default createReducer(initialState, {
// Decrement reference counter
for (let itemName in payload.entities) {
newState.getIn(["entities", itemName]).forEach(function (entity, id) {
const entities = payload.entities[itemName];
entities.forEach(function (id) {
const entity = newState.getIn(["entities", itemName, id]);
newState = updateEntityRefCount(newState, itemName, id, entity, -1);
});
}
@ -207,5 +213,5 @@ export default createReducer(initialState, {
},
[INVALIDATE_STORE]: () => {
return new stateRecord();
}
},
});

View File

@ -19,7 +19,7 @@ import * as ActionTypes from "../actions";
const paginated = paginatedMaker([
ActionTypes.API_REQUEST,
ActionTypes.API_SUCCESS,
ActionTypes.API_FAILURE
ActionTypes.API_FAILURE,
]);
// Export the combined reducers
@ -28,5 +28,5 @@ export default combineReducers({
auth,
entities,
paginated,
webplayer
webplayer,
});

View File

@ -12,7 +12,7 @@ import { createReducer } from "../utils";
import { stateRecord } from "../models/paginated";
// Actions
import { CLEAR_RESULTS, INVALIDATE_STORE } from "../actions";
import { CLEAR_PAGINATED_RESULTS, INVALIDATE_STORE } from "../actions";
/** Initial state of the reducer */
@ -50,12 +50,12 @@ export default function paginated(types) {
[failureType]: (state) => {
return state;
},
[CLEAR_RESULTS]: (state) => {
[CLEAR_PAGINATED_RESULTS]: (state) => {
return state.set("result", new Immutable.List());
},
[INVALIDATE_STORE]: () => {
// Reset state on invalidation
return new stateRecord();
}
},
});
}

View File

@ -1,16 +1,32 @@
// TODO: This is a WIP
/**
* This implements the webplayer reducers.
*/
// NPM imports
import Immutable from "immutable";
// Local imports
import { createReducer } from "../utils";
// Models
import { stateRecord } from "../models/webplayer";
// Actions
import {
PUSH_PLAYLIST,
CHANGE_TRACK,
PLAY_PAUSE,
STOP_PLAYBACK,
SET_PLAYLIST,
PUSH_SONG,
POP_SONG,
JUMP_TO_SONG,
PLAY_PREVIOUS,
PLAY_NEXT,
TOGGLE_RANDOM,
TOGGLE_REPEAT,
TOGGLE_MUTE,
SET_VOLUME,
INVALIDATE_STORE } from "../actions";
import { createReducer } from "../utils";
import { stateRecord } from "../models/webplayer";
/**
* Initial state
@ -19,28 +35,80 @@ import { stateRecord } from "../models/webplayer";
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
*/
export default createReducer(initialState, {
[PLAY_PAUSE]: (state, payload) => {
// Force play or pause
return state.set("isPlaying", payload.isPlaying);
},
[CHANGE_TRACK]: (state, payload) => {
return state.set("currentIndex", payload.index);
[STOP_PLAYBACK]: (state) => {
// Clear the playlist
return stopPlayback(state);
},
[PUSH_PLAYLIST]: (state, payload) => {
[SET_PLAYLIST]: (state, payload) => {
// Set current playlist, reset playlist index
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)
);
},
[PUSH_SONG]: (state, payload) => {
// Push song to playlist
const newPlaylist = state.get("playlist").insert(payload.index, payload.song);
return state.set("playlist", newPlaylist);
},
[POP_SONG]: (state, payload) => {
// Pop song from playlist
return state.deleteIn(["playlist", payload.index]);
},
[JUMP_TO_SONG]: (state, payload) => {
// Set current index
const newCurrentIndex = state.get("playlist").findKey(x => x == payload.song);
return state.set("currentIndex", newCurrentIndex);
},
[PLAY_PREVIOUS]: (state) => {
const newIndex = state.get("currentIndex") - 1;
if (newIndex < 0) {
// If there is an overlow on the left of the playlist, just stop
// playback
return stopPlayback(state);
} else {
return state.set("currentIndex", newIndex);
}
},
[PLAY_NEXT]: (state) => {
const newIndex = state.get("currentIndex") + 1;
if (newIndex > state.get("playlist").size) {
// If there is an overflow, just stop playback
return stopPlayback(state);
} else {
// Else, play next item
return state.set("currentIndex", newIndex);
}
},
[TOGGLE_RANDOM]: (state) => {
return state.set("isRandom", !state.get("isRandom"));
},
@ -50,7 +118,10 @@ export default createReducer(initialState, {
[TOGGLE_MUTE]: (state) => {
return state.set("isMute", !state.get("isMute"));
},
[SET_VOLUME]: (state, payload) => {
return state.set("volume", payload.volume);
},
[INVALIDATE_STORE]: () => {
return new stateRecord();
}
},
});

View File

@ -15,7 +15,7 @@ import jsSHA from "jssha";
* @remark This builds an HMAC as expected by Ampache API, which is not a
* standard HMAC.
*/
export function buildHMAC (password) {
export function buildHMAC(password) {
const time = Math.floor(Date.now() / 1000);
let shaObj = new jsSHA("SHA-256", "TEXT");
@ -27,6 +27,6 @@ export function buildHMAC (password) {
return {
time: time,
passphrase: shaObj.getHash("HEX")
passphrase: shaObj.getHash("HEX"),
};
}

View File

@ -10,7 +10,7 @@
* @param b Second Immutable object.
* @returns An Immutable object equal to a except for the items in b.
*/
export function immutableDiff (a, b) {
export function immutableDiff(a, b) {
return a.filter(function (i) {
return b.indexOf(i) < 0;
});

View File

@ -6,7 +6,7 @@ import { i18nRecord } from "../models/i18n";
/**
* Get the preferred locales from the browser, as an array sorted by preferences.
*/
export function getBrowserLocales () {
export function getBrowserLocales() {
let langs = [];
if (navigator.languages) {

View File

@ -10,7 +10,7 @@
* @return Either NaN if the string was not a valid int representation, or the
* int.
*/
export function filterInt (value) {
export function filterInt(value) {
if (/^(\-|\+)?([0-9]+|Infinity)$/.test(value)) {
return Number(value);
}
@ -24,7 +24,7 @@ export function filterInt (value) {
* @param time Length of the song in seconds.
* @return Formatted length as MM:SS.
*/
export function formatLength (time) {
export function formatLength(time) {
const min = Math.floor(time / 60);
let sec = (time - 60 * min);
if (sec < 10) {

View File

@ -17,14 +17,14 @@ export function buildPaginationObject(location, currentPage, nPages, goToPageAct
const buildLinkToPage = function (pageNumber) {
return {
pathname: location.pathname,
query: Object.assign({}, location.query, { page: pageNumber })
query: Object.assign({}, location.query, { page: pageNumber }),
};
};
return {
currentPage: currentPage,
nPages: nPages,
goToPage: pageNumber => goToPageAction(buildLinkToPage(pageNumber)),
buildLinkToPage: buildLinkToPage
buildLinkToPage: buildLinkToPage,
};
}
@ -57,6 +57,6 @@ export function computePaginationBounds(currentPage, nPages, maxNumberPagesShown
return {
lowerLimit: lowerLimit,
upperLimit: upperLimit + 1 // +1 to ease iteration in for with <
upperLimit: upperLimit + 1, // +1 to ease iteration in for with <
};
}

View File

@ -11,7 +11,7 @@
*
* @return A string with the full URL with GET params.
*/
export function assembleURLAndParams (endpoint, params) {
export function assembleURLAndParams(endpoint, params) {
let url = endpoint + "?";
Object.keys(params).forEach(
key => {
@ -34,7 +34,7 @@ export function assembleURLAndParams (endpoint, params) {
* @param An URL
* @return The cleaned URL
*/
export function cleanURL (endpoint) {
export function cleanURL(endpoint) {
if (
!endpoint.startsWith("//") &&
!endpoint.startsWith("http://") &&

View File

@ -10,16 +10,16 @@ import Album from "../components/Album";
// TODO: AlbumPage should be scrolled ArtistPage
export class AlbumPage extends Component {
componentWillMount () {
componentWillMount() {
// Load the data
this.props.actions.loadAlbums({
pageNumber: 1,
filter: this.props.params.id,
include: ["songs"]
include: ["songs"],
});
}
render () {
render() {
if (this.props.album) {
return (
<Album album={this.props.album} songs={this.props.songs} />
@ -52,12 +52,12 @@ const mapStateToProps = (state, ownProps) => {
}
return {
album: album,
songs: songs
songs: songs,
};
};
const mapDispatchToProps = (dispatch) => ({
actions: bindActionCreators(actionCreators, dispatch)
actions: bindActionCreators(actionCreators, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(AlbumPage);

View File

@ -25,13 +25,13 @@ const albumsMessages = defineMessages(messagesMap(Array.concat([], APIMessages))
* Albums page, grid layout of albums arts.
*/
class AlbumsPageIntl extends Component {
componentWillMount () {
componentWillMount() {
// Load the data for current page
const currentPage = parseInt(this.props.location.query.page) || 1;
this.props.actions.loadPaginatedAlbums({ pageNumber: currentPage });
}
componentWillReceiveProps (nextProps) {
componentWillReceiveProps(nextProps) {
// Load the data if page has changed
const currentPage = parseInt(this.props.location.query.page) || 1;
const nextPage = parseInt(nextProps.location.query.page) || 1;
@ -40,12 +40,12 @@ class AlbumsPageIntl extends Component {
}
}
componentWillUnmount () {
componentWillUnmount() {
// Unload data on page change
this.props.actions.clearResults();
this.props.actions.clearPaginatedResults();
}
render () {
render() {
const {formatMessage} = this.props.intl;
const pagination = buildPaginationObject(this.props.location, this.props.currentPage, this.props.nPages, this.props.actions.goToPage);
@ -74,12 +74,12 @@ const mapStateToProps = (state) => {
error: state.entities.error,
albumsList: albumsList,
currentPage: state.paginated.currentPage,
nPages: state.paginated.nPages
nPages: state.paginated.nPages,
};
};
const mapDispatchToProps = (dispatch) => ({
actions: bindActionCreators(actionCreators, dispatch)
actions: bindActionCreators(actionCreators, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(AlbumsPageIntl));

View File

@ -25,27 +25,27 @@ const artistMessages = defineMessages(messagesMap(Array.concat([], APIMessages))
* Single artist page.
*/
class ArtistPageIntl extends Component {
componentWillMount () {
componentWillMount() {
// Load the data
this.props.actions.loadArtist({
filter: this.props.params.id,
include: ["albums", "songs"]
include: ["albums", "songs"],
});
}
componentWillUnmount () {
componentWillUnmount() {
this.props.actions.decrementRefCount({
"artist": [this.props.artist.get("id")]
"artist": [this.props.artist.get("id")],
});
}
render () {
render() {
const {formatMessage} = this.props.intl;
const error = handleErrorI18nObject(this.props.error, formatMessage, artistMessages);
return (
<Artist playAction={this.props.actions.playTrack} isFetching={this.props.isFetching} error={error} artist={this.props.artist} albums={this.props.albums} songs={this.props.songs} />
<Artist playAction={this.props.actions.playSong} isFetching={this.props.isFetching} error={error} artist={this.props.artist} albums={this.props.albums} songs={this.props.songs} />
);
}
}
@ -82,12 +82,12 @@ const mapStateToProps = (state, ownProps) => {
error: state.entities.error,
artist: artist,
albums: albums,
songs: songs
songs: songs,
};
};
const mapDispatchToProps = (dispatch) => ({
actions: bindActionCreators(actionCreators, dispatch)
actions: bindActionCreators(actionCreators, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(ArtistPageIntl));

View File

@ -25,13 +25,13 @@ const artistsMessages = defineMessages(messagesMap(Array.concat([], APIMessages)
* Grid of artists arts.
*/
class ArtistsPageIntl extends Component {
componentWillMount () {
componentWillMount() {
// Load the data for the current page
const currentPage = parseInt(this.props.location.query.page) || 1;
this.props.actions.loadPaginatedArtists({pageNumber: currentPage});
}
componentWillReceiveProps (nextProps) {
componentWillReceiveProps(nextProps) {
// Load the data if page has changed
const currentPage = parseInt(this.props.location.query.page) || 1;
const nextPage = parseInt(nextProps.location.query.page) || 1;
@ -40,12 +40,12 @@ class ArtistsPageIntl extends Component {
}
}
componentWillUnmount () {
componentWillUnmount() {
// Unload data on page change
this.props.actions.clearResults();
this.props.actions.clearPaginatedResults();
}
render () {
render() {
const {formatMessage} = this.props.intl;
const pagination = buildPaginationObject(this.props.location, this.props.currentPage, this.props.nPages, this.props.actions.goToPage);
@ -79,7 +79,7 @@ const mapStateToProps = (state) => {
};
const mapDispatchToProps = (dispatch) => ({
actions: bindActionCreators(actionCreators, dispatch)
actions: bindActionCreators(actionCreators, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(ArtistsPageIntl));

View File

@ -9,7 +9,7 @@ import ArtistsPage from "./ArtistsPage";
* Browse page is an alias for artists page at the moment.
*/
export default class BrowsePage extends Component {
render () {
render() {
return (
<ArtistsPage {...this.props} />
);

View File

@ -8,7 +8,7 @@ import Discover from "../components/Discover";
* Discover page
*/
export default class DiscoverPage extends Component {
render () {
render() {
return (
<Discover />
);

View File

@ -8,7 +8,7 @@ import ArtistsPage from "./ArtistsPage";
* Homepage is an alias for Artists page at the moment.
*/
export default class HomePage extends Component {
render () {
render() {
return (
<ArtistsPage {...this.props} />
);

View File

@ -14,7 +14,7 @@ import Login from "../components/Login";
* Login page
*/
export class LoginPage extends Component {
constructor (props) {
constructor(props) {
super(props);
// Bind this
@ -37,11 +37,11 @@ export class LoginPage extends Component {
}
return {
pathname: redirectPathname,
query: redirectQuery
query: redirectQuery,
};
}
componentWillMount () {
componentWillMount() {
// This checks if the user is already connected or not and redirects
// them if it is the case.
@ -67,14 +67,14 @@ export class LoginPage extends Component {
/**
* Handle click on submit button.
*/
handleSubmit (username, password, endpoint, rememberMe) {
handleSubmit(username, password, endpoint, rememberMe) {
// Get page to redirect to
const redirectTo = this._getRedirectTo();
// Trigger login action
this.props.actions.loginUser(username, password, endpoint, rememberMe, redirectTo);
}
render () {
render() {
return (
<Login onSubmit={this.handleSubmit} username={this.props.username} endpoint={this.props.endpoint} rememberMe={this.props.rememberMe} isAuthenticating={this.props.isAuthenticating} error={this.props.error} info={this.props.info} />
);
@ -82,7 +82,7 @@ export class LoginPage extends Component {
}
LoginPage.contextTypes = {
router: PropTypes.object.isRequired
router: PropTypes.object.isRequired,
};
const mapStateToProps = (state) => ({
@ -93,11 +93,11 @@ const mapStateToProps = (state) => ({
isAuthenticated: state.auth.isAuthenticated,
token: state.auth.token,
error: state.auth.error,
info: state.auth.info
info: state.auth.info,
});
const mapDispatchToProps = (dispatch) => ({
actions: bindActionCreators(actionCreators, dispatch)
actions: bindActionCreators(actionCreators, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(LoginPage);

View File

@ -11,12 +11,12 @@ import * as actionCreators from "../actions";
* Logout page
*/
export class LogoutPage extends Component {
componentWillMount () {
componentWillMount() {
// Logout when component is mounted
this.props.actions.logoutAndRedirect();
}
render () {
render() {
return (
<div></div>
);
@ -24,7 +24,7 @@ export class LogoutPage extends Component {
}
const mapDispatchToProps = (dispatch) => ({
actions: bindActionCreators(actionCreators, dispatch)
actions: bindActionCreators(actionCreators, dispatch),
});
export default connect(null, mapDispatchToProps)(LogoutPage);

View File

@ -25,13 +25,13 @@ const songsMessages = defineMessages(messagesMap(Array.concat([], APIMessages)))
* Paginated table of available songs
*/
class SongsPageIntl extends Component {
componentWillMount () {
componentWillMount() {
// Load the data for current page
const currentPage = parseInt(this.props.location.query.page) || 1;
this.props.actions.loadPaginatedSongs({pageNumber: currentPage});
}
componentWillReceiveProps (nextProps) {
componentWillReceiveProps(nextProps) {
// Load the data if page has changed
const currentPage = parseInt(this.props.location.query.page) || 1;
const nextPage = parseInt(nextProps.location.query.page) || 1;
@ -40,12 +40,12 @@ class SongsPageIntl extends Component {
}
}
componentWillUnmount () {
componentWillUnmount() {
// Unload data on page change
this.props.actions.clearResults();
this.props.actions.clearPaginatedResults();
}
render () {
render() {
const {formatMessage} = this.props.intl;
const pagination = buildPaginationObject(this.props.location, this.props.currentPage, this.props.nPages, this.props.actions.goToPage);
@ -53,7 +53,7 @@ class SongsPageIntl extends Component {
const error = handleErrorI18nObject(this.props.error, formatMessage, songsMessages);
return (
<Songs playAction={this.props.actions.playTrack} isFetching={this.props.isFetching} error={error} songs={this.props.songsList} pagination={pagination} />
<Songs playAction={this.props.actions.playSong} isFetching={this.props.isFetching} error={error} songs={this.props.songsList} pagination={pagination} />
);
}
}
@ -82,12 +82,12 @@ const mapStateToProps = (state) => {
error: state.entities.error,
songsList: songsList,
currentPage: state.paginated.currentPage,
nPages: state.paginated.nPages
nPages: state.paginated.nPages,
};
};
const mapDispatchToProps = (dispatch) => ({
actions: bindActionCreators(actionCreators, dispatch)
actions: bindActionCreators(actionCreators, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(SongsPageIntl));

View File

@ -1,112 +1,146 @@
// TODO: This file is not finished
// NPM imports
import React, { Component } from "react";
import { bindActionCreators } from "redux";
import { connect } from "react-redux";
import { Howl } from "howler";
import Immutable from "immutable";
// Actions
import * as actionCreators from "../actions";
// Components
import WebPlayerComponent from "../components/elements/WebPlayer";
/**
* Webplayer container.
*/
class WebPlayer extends Component {
constructor (props) {
constructor(props) {
super(props);
this.play = this.play.bind(this);
// Data attributes
this.howl = null;
// Bind this
this.startPlaying = this.startPlaying.bind(this);
}
componentDidMount () {
this.play(this.props.isPlaying);
componentDidMount() {
// Start playback upon component mount
this.startPlaying(this.props);
}
componentWillUpdate (nextProps) {
componentWillUpdate(nextProps) {
// Handle stop
if (!nextProps.currentSong || nextProps.playlist.size < 1) {
if (this.howl) {
this.howl.stop();
}
}
// Toggle play / pause
if (nextProps.isPlaying != this.props.isPlaying) {
// This check ensure we do not start multiple times the same music.
this.play(nextProps);
// This check ensure we do not start playing multiple times the
// same song
this.startPlaying(nextProps);
}
// Toggle mute / unmute
// If something is playing back
if (this.howl) {
// Set mute / unmute
this.howl.mute(nextProps.isMute);
// Set volume
this.howl.volume(nextProps.volume / 100);
}
}
getCurrentTrackPath (props) {
return [
"tracks",
props.playlist.get(props.currentIndex)
];
}
play (props) {
if (props.isPlaying) {
/**
* Handle playback through Howler and Web Audio API.
*
* @params props A set of props to use for setting play parameters.
*/
startPlaying(props) {
if (props.isPlaying && props.currentSong) {
// If it should be playing any song
if (!this.howl) {
const url = props.entities.getIn(
Array.concat([], this.getCurrentTrackPath(props), ["url"])
);
// Build a new Howler object with current song to play
const url = props.currentSong.get("url");
if (!url) {
// TODO: Error handling
console.error("URL not found.");
return;
}
this.howl = new Howl({
src: [url],
html5: true,
loop: false,
html5: true, // Use HTML5 by default to allow streaming
mute: props.isMute,
autoplay: false,
volume: props.volume / 100, // Set current volume
autoplay: false, // No autoplay, we handle it manually
});
} else {
// Else, something is playing
// TODO If it is not the expected song, change it
}
// Start playing
this.howl.play();
}
else {
// If it should not be playing
if (this.howl) {
// Pause any running music
this.howl.pause();
}
}
}
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")]);
}
render() {
const webplayerProps = {
isPlaying: this.props.isPlaying,
isRandom: this.props.isRandom,
isRepeat: this.props.isRepeat,
isMute: this.props.isMute,
currentTrack: currentTrack,
currentArtist: currentArtist,
volume: this.props.volume,
currentIndex: this.props.currentIndex,
playlist: this.props.playlist,
currentSong: this.props.currentSong,
currentArtist: this.props.currentArtist,
// Use a lambda to ensure no first argument is passed to
// togglePlaying
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
onMute: this.props.actions.toggleMute,
};
return (
<WebPlayerComponent {...webplayerProps} />
);
}
}
const mapStateToProps = (state) => {
const currentIndex = state.webplayer.currentIndex;
const playlist = state.webplayer.playlist;
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
});
// Get current song and artist from entities store
const currentSong = state.entities.getIn(["entities", "song", playlist.get(currentIndex)]);
let currentArtist = undefined;
if (currentSong) {
currentArtist = state.entities.getIn(["entities", "artist", currentSong.get("artist")]);
}
return {
isPlaying: state.webplayer.isPlaying,
isRandom: state.webplayer.isRandom,
isRepeat: state.webplayer.isRepeat,
isMute: state.webplayer.isMute,
volume: state.webplayer.volume,
currentIndex: currentIndex,
playlist: playlist,
currentSong: currentSong,
currentArtist: currentArtist,
};
};
const mapDispatchToProps = (dispatch) => ({
actions: bindActionCreators(actionCreators, dispatch)
actions: bindActionCreators(actionCreators, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(WebPlayer);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -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(629);Object.keys(r).forEach(function(e){"default"!==e&&"__esModule"!==e&&Object.defineProperty(t,e,{enumerable:!0,get:function(){return r[e]}})})},629: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(634);Object.keys(r).forEach(function(e){"default"!==e&&"__esModule"!==e&&Object.defineProperty(t,e,{enumerable:!0,get:function(){return r[e]}})})},634: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

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