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": [ "strict": [
"error", "error",
], ],
"comma-dangle": [
"error",
"always-multiline"
],
"space-before-function-paren": [
"error",
{ "anonymous": "always", "named": "never" }
],
"react/jsx-uses-react": "error", "react/jsx-uses-react": "error",
"react/jsx-uses-vars": "error", "react/jsx-uses-vars": "error",

View File

@ -42,7 +42,7 @@ export default function (action, requestType, successType, failureType) {
{ {
artist: arrayOf(artist), artist: arrayOf(artist),
album: arrayOf(album), album: arrayOf(album),
song: arrayOf(song) song: arrayOf(song),
}, },
{ {
// Use custom assignEntity function to delete useless fields // Use custom assignEntity function to delete useless fields
@ -52,7 +52,7 @@ export default function (action, requestType, successType, failureType) {
} else { } else {
output[key] = value; output[key] = value;
} }
} },
} }
); );
}; };
@ -80,9 +80,9 @@ export default function (action, requestType, successType, failureType) {
type: itemName, type: itemName,
result: jsonData.result[itemName], result: jsonData.result[itemName],
nPages: nPages, nPages: nPages,
currentPage: pageNumber currentPage: pageNumber,
} },
} },
]; ];
}; };
@ -104,7 +104,7 @@ export default function (action, requestType, successType, failureType) {
return { return {
type: requestType, type: requestType,
payload: { payload: {
} },
}; };
}; };
@ -119,8 +119,8 @@ export default function (action, requestType, successType, failureType) {
return { return {
type: failureType, type: failureType,
payload: { payload: {
error: error error: error,
} },
}; };
}; };
@ -144,7 +144,7 @@ export default function (action, requestType, successType, failureType) {
// Set extra params for pagination // Set extra params for pagination
let extraParams = { let extraParams = {
offset: offset, offset: offset,
limit: limit limit: limit,
}; };
// Handle filter // Handle filter
@ -165,13 +165,13 @@ export default function (action, requestType, successType, failureType) {
dispatch: [ dispatch: [
fetchItemsRequest, fetchItemsRequest,
null, null,
fetchItemsFailure fetchItemsFailure,
], ],
action: action, action: action,
auth: passphrase, auth: passphrase,
username: username, username: username,
extraParams: extraParams extraParams: extraParams,
} },
}; };
}; };

View File

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

View File

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

View File

@ -7,6 +7,6 @@
export const INVALIDATE_STORE = "INVALIDATE_STORE"; export const INVALIDATE_STORE = "INVALIDATE_STORE";
export function invalidateStore() { export function invalidateStore() {
return { 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"; 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) { export function togglePlaying(playPause) {
return (dispatch, getState) => { return (dispatch, getState) => {
let isPlaying = false; let newIsPlaying = false;
if (typeof playPause !== "undefined") { if (typeof playPause !== "undefined") {
isPlaying = playPause; // If we want to force a mode
newIsPlaying = playPause;
} else { } else {
isPlaying = !(getState().webplayer.isPlaying); // Else, just toggle
newIsPlaying = !(getState().webplayer.isPlaying);
} }
// Dispatch action
dispatch({ dispatch({
type: PLAY_PAUSE, type: PLAY_PAUSE,
payload: { 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) => { return (dispatch, getState) => {
const track = getState().api.entities.getIn(["track", trackID]); // Handle reference counting
const album = getState().api.entities.getIn(["album", track.get("album")]); dispatch(decrementRefCount({
const artist = getState().api.entities.getIn(["artist", track.get("artist")]); song: getState().webplayer.get("playlist").toArray(),
}));
// Stop playback
dispatch ({ dispatch ({
type: PUSH_PLAYLIST, type: STOP_PLAYBACK,
payload: {
playlist: [trackID],
tracks: [
[trackID, track]
],
albums: [
[album.get("id"), album]
],
artists: [
[artist.get("id"), artist]
]
}
}); });
};
}
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)); 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() { export function playPrevious() {
// TODO: Playlist overflow return (dispatch) => {
return (dispatch, getState) => {
let { index } = getState().webplayer;
dispatch({ dispatch({
type: CHANGE_TRACK, type: PLAY_PREVIOUS,
payload: {
index: index - 1
}
}); });
}; };
} }
export const PLAY_NEXT = "PLAY_NEXT";
/**
* Move one song forward in the playlist.
*
* @return Dispatch a PLAY_NEXT action.
*/
export function playNext() { export function playNext() {
// TODO: Playlist overflow return (dispatch) => {
return (dispatch, getState) => {
let { index } = getState().webplayer;
dispatch({ dispatch({
type: CHANGE_TRACK, type: PLAY_NEXT,
payload: {
index: index + 1
}
}); });
}; };
} }
export const TOGGLE_RANDOM = "TOGGLE_RANDOM"; export const TOGGLE_RANDOM = "TOGGLE_RANDOM";
/**
* Toggle random mode.
*
* @return Dispatch a TOGGLE_RANDOM action.
*/
export function toggleRandom() { export function toggleRandom() {
return { return {
type: TOGGLE_RANDOM type: TOGGLE_RANDOM,
}; };
} }
export const TOGGLE_REPEAT = "TOGGLE_REPEAT"; export const TOGGLE_REPEAT = "TOGGLE_REPEAT";
/**
* Toggle repeat mode.
*
* @return Dispatch a TOGGLE_REPEAT action.
*/
export function toggleRepeat() { export function toggleRepeat() {
return { return {
type: TOGGLE_REPEAT type: TOGGLE_REPEAT,
}; };
} }
export const TOGGLE_MUTE = "TOGGLE_MUTE"; export const TOGGLE_MUTE = "TOGGLE_MUTE";
/**
* Toggle mute mode.
*
* @return Dispatch a TOGGLE_MUTE action.
*/
export function toggleMute() { export function toggleMute() {
return { 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

@ -45,7 +45,7 @@ class AlbumTrackRowCSSIntl extends Component {
AlbumTrackRowCSSIntl.propTypes = { AlbumTrackRowCSSIntl.propTypes = {
playAction: PropTypes.func.isRequired, playAction: PropTypes.func.isRequired,
track: PropTypes.instanceOf(Immutable.Map).isRequired, track: PropTypes.instanceOf(Immutable.Map).isRequired,
intl: intlShape.isRequired intl: intlShape.isRequired,
}; };
export let AlbumTrackRow = injectIntl(CSSModules(AlbumTrackRowCSSIntl, css)); export let AlbumTrackRow = injectIntl(CSSModules(AlbumTrackRowCSSIntl, css));
@ -72,7 +72,7 @@ class AlbumTracksTableCSS extends Component {
} }
AlbumTracksTableCSS.propTypes = { AlbumTracksTableCSS.propTypes = {
playAction: PropTypes.func.isRequired, playAction: PropTypes.func.isRequired,
tracks: PropTypes.instanceOf(Immutable.List).isRequired tracks: PropTypes.instanceOf(Immutable.List).isRequired,
}; };
export let AlbumTracksTable = CSSModules(AlbumTracksTableCSS, css); export let AlbumTracksTable = CSSModules(AlbumTracksTableCSS, css);
@ -104,6 +104,6 @@ class AlbumRowCSS extends Component {
AlbumRowCSS.propTypes = { AlbumRowCSS.propTypes = {
playAction: PropTypes.func.isRequired, playAction: PropTypes.func.isRequired,
album: PropTypes.instanceOf(Immutable.Map).isRequired, album: PropTypes.instanceOf(Immutable.Map).isRequired,
songs: PropTypes.instanceOf(Immutable.List).isRequired songs: PropTypes.instanceOf(Immutable.List).isRequired,
}; };
export let AlbumRow = CSSModules(AlbumRowCSS, css); export let AlbumRow = CSSModules(AlbumRowCSS, css);

View File

@ -25,7 +25,7 @@ export default class Albums extends Component {
itemsType: "album", itemsType: "album",
itemsLabel: "app.common.album", itemsLabel: "app.common.album",
subItemsType: "tracks", subItemsType: "tracks",
subItemsLabel: "app.common.track" subItemsLabel: "app.common.track",
}; };
return ( return (

View File

@ -87,6 +87,6 @@ ArtistCSS.propTypes = {
playAction: PropTypes.func.isRequired, playAction: PropTypes.func.isRequired,
artist: PropTypes.instanceOf(Immutable.Map), artist: PropTypes.instanceOf(Immutable.Map),
albums: PropTypes.instanceOf(Immutable.List), albums: PropTypes.instanceOf(Immutable.List),
songs: PropTypes.instanceOf(Immutable.Map) songs: PropTypes.instanceOf(Immutable.Map),
}; };
export default CSSModules(ArtistCSS, css); export default CSSModules(ArtistCSS, css);

View File

@ -25,7 +25,7 @@ export default class Artists extends Component {
itemsType: "artist", itemsType: "artist",
itemsLabel: "app.common.artist", itemsLabel: "app.common.artist",
subItemsType: "albums", subItemsType: "albums",
subItemsLabel: "app.common.album" subItemsLabel: "app.common.album",
}; };
return ( return (

View File

@ -212,6 +212,6 @@ LoginCSS.propTypes = {
onSubmit: PropTypes.func.isRequired, onSubmit: PropTypes.func.isRequired,
isAuthenticating: PropTypes.bool, isAuthenticating: PropTypes.bool,
info: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), 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); export default CSSModules(LoginCSS, css);

View File

@ -59,7 +59,7 @@ class SongsTableRowCSSIntl extends Component {
SongsTableRowCSSIntl.propTypes = { SongsTableRowCSSIntl.propTypes = {
playAction: PropTypes.func.isRequired, playAction: PropTypes.func.isRequired,
song: PropTypes.instanceOf(Immutable.Map).isRequired, song: PropTypes.instanceOf(Immutable.Map).isRequired,
intl: intlShape.isRequired intl: intlShape.isRequired,
}; };
export let SongsTableRow = injectIntl(CSSModules(SongsTableRowCSSIntl, css)); export let SongsTableRow = injectIntl(CSSModules(SongsTableRowCSSIntl, css));
@ -78,7 +78,7 @@ class SongsTableCSS extends Component {
{ {
"keys": ["name"], "keys": ["name"],
"threshold": 0.4, "threshold": 0.4,
"include": ["score"] "include": ["score"],
}).search(this.props.filterText); }).search(this.props.filterText);
// Keep only items in results // Keep only items in results
displayedSongs = displayedSongs.map(function (item) { return new Immutable.Map(item.item); }); displayedSongs = displayedSongs.map(function (item) { return new Immutable.Map(item.item); });
@ -135,7 +135,7 @@ class SongsTableCSS extends Component {
SongsTableCSS.propTypes = { SongsTableCSS.propTypes = {
playAction: PropTypes.func.isRequired, playAction: PropTypes.func.isRequired,
songs: PropTypes.instanceOf(Immutable.List).isRequired, songs: PropTypes.instanceOf(Immutable.List).isRequired,
filterText: PropTypes.string filterText: PropTypes.string,
}; };
export let SongsTable = CSSModules(SongsTableCSS, css); export let SongsTable = CSSModules(SongsTableCSS, css);
@ -147,7 +147,7 @@ export default class FilterablePaginatedSongsTable extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { 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 this.handleUserInput = this.handleUserInput.bind(this); // Bind this on user input handling
@ -162,7 +162,7 @@ export default class FilterablePaginatedSongsTable extends Component {
*/ */
handleUserInput(filterText) { handleUserInput(filterText) {
this.setState({ this.setState({
filterText: filterText filterText: filterText,
}); });
} }
@ -176,13 +176,13 @@ export default class FilterablePaginatedSongsTable extends Component {
// Set props // Set props
const filterProps = { const filterProps = {
filterText: this.state.filterText, filterText: this.state.filterText,
onUserInput: this.handleUserInput onUserInput: this.handleUserInput,
}; };
const songsTableProps = { const songsTableProps = {
playAction: this.props.playAction, playAction: this.props.playAction,
isFetching: this.props.isFetching, isFetching: this.props.isFetching,
songs: this.props.songs, songs: this.props.songs,
filterText: this.state.filterText filterText: this.state.filterText,
}; };
return ( return (
@ -200,5 +200,5 @@ FilterablePaginatedSongsTable.propTypes = {
isFetching: PropTypes.bool.isRequired, isFetching: PropTypes.bool.isRequired,
error: PropTypes.string, error: PropTypes.string,
songs: PropTypes.instanceOf(Immutable.List).isRequired, songs: PropTypes.instanceOf(Immutable.List).isRequired,
pagination: PropTypes.object.isRequired pagination: PropTypes.object.isRequired,
}; };

View File

@ -27,5 +27,5 @@ export default class DismissibleAlert extends Component {
} }
DismissibleAlert.propTypes = { DismissibleAlert.propTypes = {
type: PropTypes.string, type: PropTypes.string,
text: PropTypes.string text: PropTypes.string,
}; };

View File

@ -60,6 +60,6 @@ class FilterBarCSSIntl extends Component {
FilterBarCSSIntl.propTypes = { FilterBarCSSIntl.propTypes = {
onUserInput: PropTypes.func, onUserInput: PropTypes.func,
filterText: PropTypes.string, filterText: PropTypes.string,
intl: intlShape.isRequired intl: intlShape.isRequired,
}; };
export default injectIntl(CSSModules(FilterBarCSSIntl, css)); 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. */ const ISOTOPE_OPTIONS = { /** Default options for Isotope grid layout. */
getSortData: { getSortData: {
name: ".name", name: ".name",
nSubitems: ".sub-items .n-sub-items" nSubitems: ".sub-items .n-sub-items",
}, },
transitionDuration: 0, transitionDuration: 0,
sortBy: "name", sortBy: "name",
@ -40,8 +40,8 @@ const ISOTOPE_OPTIONS = { /** Default options for Isotope grid layout. */
layoutMode: "fitRows", layoutMode: "fitRows",
filter: "*", filter: "*",
fitRows: { fitRows: {
gutter: 0 gutter: 0,
} },
}; };
@ -85,7 +85,7 @@ GridItemCSSIntl.propTypes = {
itemsLabel: PropTypes.string.isRequired, itemsLabel: PropTypes.string.isRequired,
subItemsType: PropTypes.string.isRequired, subItemsType: PropTypes.string.isRequired,
subItemsLabel: PropTypes.string.isRequired, subItemsLabel: PropTypes.string.isRequired,
intl: intlShape.isRequired intl: intlShape.isRequired,
}; };
export let GridItem = injectIntl(CSSModules(GridItemCSSIntl, css)); export let GridItem = injectIntl(CSSModules(GridItemCSSIntl, css));
@ -129,7 +129,7 @@ export class Grid extends Component {
{ {
"keys": ["name"], "keys": ["name"],
"threshold": 0.4, "threshold": 0.4,
"include": ["score"] "include": ["score"],
} }
).search(props.filterText); ).search(props.filterText);
@ -149,9 +149,9 @@ export class Grid extends Component {
} }
return p; return p;
}, 0); }, 0);
}
}, },
sortBy: "relevance" },
sortBy: "relevance",
}); });
this.iso.updateSortData(); this.iso.updateSortData();
this.iso.arrange(); this.iso.arrange();
@ -264,7 +264,7 @@ Grid.propTypes = {
itemsLabel: PropTypes.string.isRequired, itemsLabel: PropTypes.string.isRequired,
subItemsType: PropTypes.string.isRequired, subItemsType: PropTypes.string.isRequired,
subItemsLabel: PropTypes.string.isRequired, subItemsLabel: PropTypes.string.isRequired,
filterText: PropTypes.string filterText: PropTypes.string,
}; };
@ -276,7 +276,7 @@ export default class FilterablePaginatedGrid extends Component {
super(props); super(props);
this.state = { this.state = {
filterText: "" // No filterText at init filterText: "", // No filterText at init
}; };
// Bind this // Bind this
@ -292,7 +292,7 @@ export default class FilterablePaginatedGrid extends Component {
*/ */
handleUserInput(filterText) { handleUserInput(filterText) {
this.setState({ this.setState({
filterText: filterText filterText: filterText,
}); });
} }
@ -309,5 +309,5 @@ export default class FilterablePaginatedGrid extends Component {
FilterablePaginatedGrid.propTypes = { FilterablePaginatedGrid.propTypes = {
grid: PropTypes.object.isRequired, grid: PropTypes.object.isRequired,
pagination: PropTypes.object.isRequired pagination: PropTypes.object.isRequired,
}; };

View File

@ -1,31 +1,49 @@
// TODO: This file is to review // NPM imports
import React, { Component, PropTypes } from "react"; import React, { Component, PropTypes } from "react";
import CSSModules from "react-css-modules"; import CSSModules from "react-css-modules";
import { defineMessages, injectIntl, intlShape, FormattedMessage } from "react-intl"; import { defineMessages, injectIntl, intlShape, FormattedMessage } from "react-intl";
import FontAwesome from "react-fontawesome";
import Immutable from "immutable"; import Immutable from "immutable";
import FontAwesome from "react-fontawesome";
// Local imports
import { messagesMap } from "../../utils"; import { messagesMap } from "../../utils";
// Styles
import css from "../../styles/elements/WebPlayer.scss"; import css from "../../styles/elements/WebPlayer.scss";
// Translations
import commonMessages from "../../locales/messagesDescriptors/common"; import commonMessages from "../../locales/messagesDescriptors/common";
import messages from "../../locales/messagesDescriptors/elements/WebPlayer"; import messages from "../../locales/messagesDescriptors/elements/WebPlayer";
// Define translations
const webplayerMessages = defineMessages(messagesMap(Array.concat([], commonMessages, messages))); const webplayerMessages = defineMessages(messagesMap(Array.concat([], commonMessages, messages)));
/**
* Webplayer component.
*/
class WebPlayerCSSIntl extends Component { class WebPlayerCSSIntl extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
// Bind this
this.artOpacityHandler = this.artOpacityHandler.bind(this); this.artOpacityHandler = this.artOpacityHandler.bind(this);
} }
/**
* Handle opacity on album art.
*
* Set opacity on mouseover / mouseout.
*
* @param ev A JS event.
*/
artOpacityHandler(ev) { artOpacityHandler(ev) {
if (ev.type == "mouseover") { if (ev.type == "mouseover") {
// On mouse over, reduce opacity
this.refs.art.style.opacity = "1"; this.refs.art.style.opacity = "1";
this.refs.artText.style.display = "none"; this.refs.artText.style.display = "none";
} else { } else {
// On mouse out, set opacity back
this.refs.art.style.opacity = "0.75"; this.refs.art.style.opacity = "0.75";
this.refs.artText.style.display = "block"; this.refs.artText.style.display = "block";
} }
@ -34,14 +52,15 @@ class WebPlayerCSSIntl extends Component {
render() { render() {
const { formatMessage } = this.props.intl; const { formatMessage } = this.props.intl;
const song = this.props.currentTrack; // Get current song (eventually undefined)
if (!song) { const song = this.props.currentSong;
return (<div></div>);
}
// Current status (play or pause) for localization
const playPause = this.props.isPlaying ? "pause" : "play"; 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 randomBtnStyles = ["randomBtn"];
const repeatBtnStyles = ["repeatBtn"]; const repeatBtnStyles = ["repeatBtn"];
if (this.props.isRandom) { if (this.props.isRandom) {
@ -51,18 +70,30 @@ class WebPlayerCSSIntl extends Component {
repeatBtnStyles.push("active"); 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 ( return (
<div id="row" styleName="webplayer"> <div id="row" styleName="webplayer">
<div className="col-xs-12"> <div className="col-xs-12">
<div className="row" styleName="artRow" onMouseOver={this.artOpacityHandler} onMouseOut={this.artOpacityHandler}> <div className="row" styleName="artRow" onMouseOver={this.artOpacityHandler} onMouseOut={this.artOpacityHandler}>
<div className="col-xs-12"> <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"> <div ref="artText">
<h2>{song.get("title")}</h2> <h2>{songTitle}</h2>
<h3> <h3>
<span className="text-capitalize"> <span className="text-capitalize">
<FormattedMessage {...webplayerMessages["app.webplayer.by"]} /> <FormattedMessage {...webplayerMessages["app.webplayer.by"]} />
</span> { this.props.currentArtist.get("name") } </span> { artistName }
</h3> </h3>
</div> </div>
</div> </div>
@ -82,7 +113,7 @@ class WebPlayerCSSIntl extends Component {
</div> </div>
<div className="col-xs-12"> <div className="col-xs-12">
<button styleName="volumeBtn" aria-label={formatMessage(webplayerMessages["app.webplayer.volume"])} title={formatMessage(webplayerMessages["app.webplayer.volume"])} onClick={this.props.onMute}> <button styleName="volumeBtn" aria-label={formatMessage(webplayerMessages["app.webplayer.volume"])} title={formatMessage(webplayerMessages["app.webplayer.volume"])} onClick={this.props.onMute}>
<FontAwesome name={volumeMute} /> <FontAwesome name={volumeIcon} />
</button> </button>
<button styleName={repeatBtnStyles.join(" ")} aria-label={formatMessage(webplayerMessages["app.webplayer.repeat"])} title={formatMessage(webplayerMessages["app.webplayer.repeat"])} aria-pressed={this.props.isRepeat} onClick={this.props.onRepeat}> <button styleName={repeatBtnStyles.join(" ")} aria-label={formatMessage(webplayerMessages["app.webplayer.repeat"])} title={formatMessage(webplayerMessages["app.webplayer.repeat"])} aria-pressed={this.props.isRepeat} onClick={this.props.onRepeat}>
<FontAwesome name="repeat" /> <FontAwesome name="repeat" />
@ -106,7 +137,10 @@ WebPlayerCSSIntl.propTypes = {
isRandom: PropTypes.bool.isRequired, isRandom: PropTypes.bool.isRequired,
isRepeat: PropTypes.bool.isRequired, isRepeat: PropTypes.bool.isRequired,
isMute: 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), currentArtist: PropTypes.instanceOf(Immutable.Map),
onPlayPause: PropTypes.func.isRequired, onPlayPause: PropTypes.func.isRequired,
onPrev: PropTypes.func.isRequired, onPrev: PropTypes.func.isRequired,
@ -114,7 +148,7 @@ WebPlayerCSSIntl.propTypes = {
onRandom: PropTypes.func.isRequired, onRandom: PropTypes.func.isRequired,
onRepeat: PropTypes.func.isRequired, onRepeat: PropTypes.func.isRequired,
onMute: PropTypes.func.isRequired, onMute: PropTypes.func.isRequired,
intl: intlShape.isRequired intl: intlShape.isRequired,
}; };
export default injectIntl(CSSModules(WebPlayerCSSIntl, css, { allowMultiple: true })); 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"; import { messagesMap } from "../../utils";
// Other components // Other components
/* import WebPlayer from "../../views/WebPlayer"; TODO */ import WebPlayer from "../../views/WebPlayer";
// Translations // Translations
import commonMessages from "../../locales/messagesDescriptors/common"; import commonMessages from "../../locales/messagesDescriptors/common";
@ -35,7 +35,7 @@ class SidebarLayoutIntl extends Component {
artists: (this.props.location.pathname == "/artists") ? "active" : "link", artists: (this.props.location.pathname == "/artists") ? "active" : "link",
albums: (this.props.location.pathname == "/albums") ? "active" : "link", albums: (this.props.location.pathname == "/albums") ? "active" : "link",
songs: (this.props.location.pathname == "/songs") ? "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 // Hamburger collapsing function
@ -146,7 +146,7 @@ class SidebarLayoutIntl extends Component {
</li> </li>
</ul> </ul>
</nav> </nav>
{ /** TODO <WebPlayer /> */ } <WebPlayer />
</div> </div>
</div> </div>
@ -159,6 +159,6 @@ class SidebarLayoutIntl extends Component {
} }
SidebarLayoutIntl.propTypes = { SidebarLayoutIntl.propTypes = {
children: PropTypes.node, children: PropTypes.node,
intl: intlShape.isRequired intl: intlShape.isRequired,
}; };
export default injectIntl(CSSModules(SidebarLayoutIntl, css)); export default injectIntl(CSSModules(SidebarLayoutIntl, css));

View File

@ -30,8 +30,8 @@ export class RequireAuthentication extends Component {
pathname: "/login", pathname: "/login",
state: { state: {
nextPathname: this.props.location.pathname, nextPathname: this.props.location.pathname,
nextQuery: this.props.location.query nextQuery: this.props.location.query,
} },
}); });
} }
} }
@ -50,15 +50,15 @@ export class RequireAuthentication extends Component {
RequireAuthentication.propTypes = { RequireAuthentication.propTypes = {
// Injected by React Router // Injected by React Router
children: PropTypes.node children: PropTypes.node,
}; };
RequireAuthentication.contextTypes = { RequireAuthentication.contextTypes = {
router: PropTypes.object.isRequired router: PropTypes.object.isRequired,
}; };
const mapStateToProps = (state) => ({ const mapStateToProps = (state) => ({
isAuthenticated: state.auth.isAuthenticated isAuthenticated: state.auth.isAuthenticated,
}); });
export default connect(mapStateToProps)(RequireAuthentication); export default connect(mapStateToProps)(RequireAuthentication);

View File

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

View File

@ -1,5 +1,5 @@
// Export all the existing locales // Export all the existing locales
module.exports = { module.exports = {
"en-US": require("./en-US"), "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", id: "app.login.username",
defaultMessage: "Username", defaultMessage: "Username",
description: "Username input placeholder" description: "Username input placeholder",
}, },
{ {
id: "app.login.password", id: "app.login.password",
defaultMessage: "Password", defaultMessage: "Password",
description: "Password input placeholder" description: "Password input placeholder",
}, },
{ {
id: "app.login.signIn", id: "app.login.signIn",
defaultMessage: "Sign in", defaultMessage: "Sign in",
description: "Sign in" description: "Sign in",
}, },
{ {
id: "app.login.endpointInputAriaLabel", id: "app.login.endpointInputAriaLabel",
defaultMessage: "URL of your Ampache instance (e.g. http://ampache.example.com)", 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", id: "app.login.rememberMe",
description: "Remember me checkbox label", description: "Remember me checkbox label",
defaultMessage: "Remember me" defaultMessage: "Remember me",
}, },
{ {
id: "app.login.greeting", id: "app.login.greeting",
description: "Greeting to welcome the user to the app", 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 // From the auth reducer
{ {
id: "app.login.connecting", id: "app.login.connecting",
defaultMessage: "Connecting…", defaultMessage: "Connecting…",
description: "Info message while trying to connect" description: "Info message while trying to connect",
}, },
{ {
id: "app.login.success", id: "app.login.success",
defaultMessage: "Successfully logged in as { username }!", defaultMessage: "Successfully logged in as { username }!",
description: "Info message on successful login." description: "Info message on successful login.",
}, },
{ {
id: "app.login.byebye", id: "app.login.byebye",
defaultMessage: "See you soon!", defaultMessage: "See you soon!",
description: "Info message on successful logout" description: "Info message on successful logout",
}, },
{ {
id: "app.login.expired", id: "app.login.expired",
defaultMessage: "Your session expired… =(", defaultMessage: "Your session expired… =(",
description: "Error message on expired session" description: "Error message on expired session",
} },
]; ];
export default messages; export default messages;

View File

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

View File

@ -2,18 +2,18 @@ const messages = [
{ {
id: "app.api.invalidResponse", id: "app.api.invalidResponse",
defaultMessage: "Invalid response text.", defaultMessage: "Invalid response text.",
description: "Invalid response from the API" description: "Invalid response from the API",
}, },
{ {
id: "app.api.emptyResponse", id: "app.api.emptyResponse",
defaultMessage: "Empty response text.", defaultMessage: "Empty response text.",
description: "Empty response from the API" description: "Empty response from the API",
}, },
{ {
id: "app.api.error", id: "app.api.error",
defaultMessage: "Unknown 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; export default messages;

View File

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

View File

@ -2,12 +2,12 @@ const messages = [
{ {
id: "app.filter.filter", id: "app.filter.filter",
defaultMessage: "Filter…", defaultMessage: "Filter…",
description: "Filtering input placeholder" description: "Filtering input placeholder",
}, },
{ {
id: "app.filter.whatAreWeListeningToToday", id: "app.filter.whatAreWeListeningToToday",
description: "Description for the filter bar", 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", id: "app.pagination.goToPage",
defaultMessage: "<span class=\"sr-only\">Go to page </span>{pageNumber}", 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", id: "app.pagination.goToPageWithoutMarkup",
defaultMessage: "Go to page {pageNumber}", 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", id: "app.pagination.pageNavigation",
defaultMessage: "Page navigation", 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", id: "app.pagination.pageToGoTo",
description: "Title of the pagination modal", description: "Title of the pagination modal",
defaultMessage: "Page to go to?" defaultMessage: "Page to go to?",
}, },
{ {
id: "app.pagination.current", id: "app.pagination.current",
description: "Current (page)", description: "Current (page)",
defaultMessage: "current" defaultMessage: "current",
} },
]; ];
export default messages; export default messages;

View File

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

View File

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

View File

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

View File

@ -47,14 +47,14 @@ function _checkHTTPStatus (response) {
function _parseToJSON(responseText) { function _parseToJSON(responseText) {
let x2js = new X2JS({ let x2js = new X2JS({
attributePrefix: "", // No prefix for attributes 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) { if (responseText) {
return x2js.xml_str2json(responseText).root; return x2js.xml_str2json(responseText).root;
} }
return Promise.reject(new i18nRecord({ return Promise.reject(new i18nRecord({
id: "app.api.invalidResponse", id: "app.api.invalidResponse",
values: {} values: {},
})); }));
} }
@ -72,7 +72,7 @@ function _checkAPIErrors (jsonData) {
// No data returned // No data returned
return Promise.reject(new i18nRecord({ return Promise.reject(new i18nRecord({
id: "app.api.emptyResponse", id: "app.api.emptyResponse",
values: {} values: {},
})); }));
} }
return jsonData; return jsonData;
@ -209,7 +209,7 @@ function doAPICall (endpoint, action, auth, username, extraParams) {
version: API_VERSION, version: API_VERSION,
action: APIAction, action: APIAction,
auth: auth, auth: auth,
user: username user: username,
}; };
// Extend with extraParams // Extend with extraParams
const params = Object.assign({}, baseParams, 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 // Explicit relations between them
artist.define({ // Artist has albums and songs (tracks) artist.define({ // Artist has albums and songs (tracks)
albums: arrayOf(album), albums: arrayOf(album),
songs: arrayOf(song) songs: arrayOf(song),
}); });
album.define({ // Album has artist, tracks and tags album.define({ // Album has artist, tracks and tags
artist: artist, artist: artist,
tracks: arrayOf(song) tracks: arrayOf(song),
}); });
song.define({ // Track has artist and album song.define({ // Track has artist and album
artist: artist, artist: artist,
album: album album: album,
}); });

View File

@ -9,7 +9,7 @@ import Immutable from "immutable";
/** Record to store token parameters */ /** Record to store token parameters */
export const tokenRecord = Immutable.Record({ export const tokenRecord = Immutable.Record({
token: null, /** Token string */ 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 */ isAuthenticating: false, /** Whether authentication is in progress or not */
error: null, /** An error string */ error: null, /** An error string */
info: null, /** An info 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({ refCounts: new Immutable.Map({
album: new Immutable.Map(), album: new Immutable.Map(),
artist: 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) */ }), /** Map of id => reference count for each object type (garbage collection) */
entities: new Immutable.Map({ entities: new Immutable.Map({
album: new Immutable.Map(), album: new Immutable.Map(),
artist: new Immutable.Map(), artist: new Immutable.Map(),
song: new Immutable.Map() song: new Immutable.Map(),
}) /** Map of id => entity for each object type */ }), /** 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 */ /** i18n record for passing errors to be localized from actions to components */
export const i18nRecord = new Immutable.Record({ export const i18nRecord = new Immutable.Record({
id: null, /** Translation message id */ 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 */ type: null, /** Type of the paginated entries */
result: new Immutable.List(), /** List of IDs of the resulting entries, maps to the entities store */ result: new Immutable.List(), /** List of IDs of the resulting entries, maps to the entities store */
currentPage: 1, /** Number of current page */ 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 */ isMute: false, /** Whether sound is muted or not */
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 */
}); });

View File

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

View File

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

View File

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

View File

@ -12,7 +12,7 @@ import { createReducer } from "../utils";
import { stateRecord } from "../models/paginated"; import { stateRecord } from "../models/paginated";
// Actions // Actions
import { CLEAR_RESULTS, INVALIDATE_STORE } from "../actions"; import { CLEAR_PAGINATED_RESULTS, INVALIDATE_STORE } from "../actions";
/** Initial state of the reducer */ /** Initial state of the reducer */
@ -50,12 +50,12 @@ export default function paginated(types) {
[failureType]: (state) => { [failureType]: (state) => {
return state; return state;
}, },
[CLEAR_RESULTS]: (state) => { [CLEAR_PAGINATED_RESULTS]: (state) => {
return state.set("result", new Immutable.List()); return state.set("result", new Immutable.List());
}, },
[INVALIDATE_STORE]: () => { [INVALIDATE_STORE]: () => {
// Reset state on invalidation // Reset state on invalidation
return new stateRecord(); 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"; import Immutable from "immutable";
// Local imports
import { createReducer } from "../utils";
// Models
import { stateRecord } from "../models/webplayer";
// Actions
import { import {
PUSH_PLAYLIST,
CHANGE_TRACK,
PLAY_PAUSE, PLAY_PAUSE,
STOP_PLAYBACK,
SET_PLAYLIST,
PUSH_SONG,
POP_SONG,
JUMP_TO_SONG,
PLAY_PREVIOUS,
PLAY_NEXT,
TOGGLE_RANDOM, TOGGLE_RANDOM,
TOGGLE_REPEAT, TOGGLE_REPEAT,
TOGGLE_MUTE, TOGGLE_MUTE,
SET_VOLUME,
INVALIDATE_STORE } from "../actions"; INVALIDATE_STORE } from "../actions";
import { createReducer } from "../utils";
import { stateRecord } from "../models/webplayer";
/** /**
* Initial state * Initial state
@ -19,28 +35,80 @@ import { stateRecord } from "../models/webplayer";
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
*/ */
export default createReducer(initialState, { export default createReducer(initialState, {
[PLAY_PAUSE]: (state, payload) => { [PLAY_PAUSE]: (state, payload) => {
// Force play or pause
return state.set("isPlaying", payload.isPlaying); return state.set("isPlaying", payload.isPlaying);
}, },
[CHANGE_TRACK]: (state, payload) => { [STOP_PLAYBACK]: (state) => {
return state.set("currentIndex", payload.index); // Clear the playlist
return stopPlayback(state);
}, },
[PUSH_PLAYLIST]: (state, payload) => { [SET_PLAYLIST]: (state, payload) => {
// Set current playlist, reset playlist index
return ( return (
state state
.set("playlist", new Immutable.List(payload.playlist)) .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("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) => { [TOGGLE_RANDOM]: (state) => {
return state.set("isRandom", !state.get("isRandom")); return state.set("isRandom", !state.get("isRandom"));
}, },
@ -50,7 +118,10 @@ export default createReducer(initialState, {
[TOGGLE_MUTE]: (state) => { [TOGGLE_MUTE]: (state) => {
return state.set("isMute", !state.get("isMute")); return state.set("isMute", !state.get("isMute"));
}, },
[SET_VOLUME]: (state, payload) => {
return state.set("volume", payload.volume);
},
[INVALIDATE_STORE]: () => { [INVALIDATE_STORE]: () => {
return new stateRecord(); return new stateRecord();
} },
}); });

View File

@ -27,6 +27,6 @@ export function buildHMAC (password) {
return { return {
time: time, time: time,
passphrase: shaObj.getHash("HEX") passphrase: shaObj.getHash("HEX"),
}; };
} }

View File

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

View File

@ -15,7 +15,7 @@ export class AlbumPage extends Component {
this.props.actions.loadAlbums({ this.props.actions.loadAlbums({
pageNumber: 1, pageNumber: 1,
filter: this.props.params.id, filter: this.props.params.id,
include: ["songs"] include: ["songs"],
}); });
} }
@ -52,12 +52,12 @@ const mapStateToProps = (state, ownProps) => {
} }
return { return {
album: album, album: album,
songs: songs songs: songs,
}; };
}; };
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
actions: bindActionCreators(actionCreators, dispatch) actions: bindActionCreators(actionCreators, dispatch),
}); });
export default connect(mapStateToProps, mapDispatchToProps)(AlbumPage); export default connect(mapStateToProps, mapDispatchToProps)(AlbumPage);

View File

@ -42,7 +42,7 @@ class AlbumsPageIntl extends Component {
componentWillUnmount() { componentWillUnmount() {
// Unload data on page change // Unload data on page change
this.props.actions.clearResults(); this.props.actions.clearPaginatedResults();
} }
render() { render() {
@ -74,12 +74,12 @@ const mapStateToProps = (state) => {
error: state.entities.error, error: state.entities.error,
albumsList: albumsList, albumsList: albumsList,
currentPage: state.paginated.currentPage, currentPage: state.paginated.currentPage,
nPages: state.paginated.nPages nPages: state.paginated.nPages,
}; };
}; };
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
actions: bindActionCreators(actionCreators, dispatch) actions: bindActionCreators(actionCreators, dispatch),
}); });
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(AlbumsPageIntl)); export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(AlbumsPageIntl));

View File

@ -29,13 +29,13 @@ class ArtistPageIntl extends Component {
// Load the data // Load the data
this.props.actions.loadArtist({ this.props.actions.loadArtist({
filter: this.props.params.id, filter: this.props.params.id,
include: ["albums", "songs"] include: ["albums", "songs"],
}); });
} }
componentWillUnmount() { componentWillUnmount() {
this.props.actions.decrementRefCount({ this.props.actions.decrementRefCount({
"artist": [this.props.artist.get("id")] "artist": [this.props.artist.get("id")],
}); });
} }
@ -45,7 +45,7 @@ class ArtistPageIntl extends Component {
const error = handleErrorI18nObject(this.props.error, formatMessage, artistMessages); const error = handleErrorI18nObject(this.props.error, formatMessage, artistMessages);
return ( return (
<Artist playAction={this.props.actions.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, error: state.entities.error,
artist: artist, artist: artist,
albums: albums, albums: albums,
songs: songs songs: songs,
}; };
}; };
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
actions: bindActionCreators(actionCreators, dispatch) actions: bindActionCreators(actionCreators, dispatch),
}); });
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(ArtistPageIntl)); export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(ArtistPageIntl));

View File

@ -42,7 +42,7 @@ class ArtistsPageIntl extends Component {
componentWillUnmount() { componentWillUnmount() {
// Unload data on page change // Unload data on page change
this.props.actions.clearResults(); this.props.actions.clearPaginatedResults();
} }
render() { render() {
@ -79,7 +79,7 @@ const mapStateToProps = (state) => {
}; };
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
actions: bindActionCreators(actionCreators, dispatch) actions: bindActionCreators(actionCreators, dispatch),
}); });
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(ArtistsPageIntl)); export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(ArtistsPageIntl));

View File

@ -37,7 +37,7 @@ export class LoginPage extends Component {
} }
return { return {
pathname: redirectPathname, pathname: redirectPathname,
query: redirectQuery query: redirectQuery,
}; };
} }
@ -82,7 +82,7 @@ export class LoginPage extends Component {
} }
LoginPage.contextTypes = { LoginPage.contextTypes = {
router: PropTypes.object.isRequired router: PropTypes.object.isRequired,
}; };
const mapStateToProps = (state) => ({ const mapStateToProps = (state) => ({
@ -93,11 +93,11 @@ const mapStateToProps = (state) => ({
isAuthenticated: state.auth.isAuthenticated, isAuthenticated: state.auth.isAuthenticated,
token: state.auth.token, token: state.auth.token,
error: state.auth.error, error: state.auth.error,
info: state.auth.info info: state.auth.info,
}); });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
actions: bindActionCreators(actionCreators, dispatch) actions: bindActionCreators(actionCreators, dispatch),
}); });
export default connect(mapStateToProps, mapDispatchToProps)(LoginPage); export default connect(mapStateToProps, mapDispatchToProps)(LoginPage);

View File

@ -24,7 +24,7 @@ export class LogoutPage extends Component {
} }
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
actions: bindActionCreators(actionCreators, dispatch) actions: bindActionCreators(actionCreators, dispatch),
}); });
export default connect(null, mapDispatchToProps)(LogoutPage); export default connect(null, mapDispatchToProps)(LogoutPage);

View File

@ -42,7 +42,7 @@ class SongsPageIntl extends Component {
componentWillUnmount() { componentWillUnmount() {
// Unload data on page change // Unload data on page change
this.props.actions.clearResults(); this.props.actions.clearPaginatedResults();
} }
render() { render() {
@ -53,7 +53,7 @@ class SongsPageIntl extends Component {
const error = handleErrorI18nObject(this.props.error, formatMessage, songsMessages); const error = handleErrorI18nObject(this.props.error, formatMessage, songsMessages);
return ( return (
<Songs playAction={this.props.actions.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, error: state.entities.error,
songsList: songsList, songsList: songsList,
currentPage: state.paginated.currentPage, currentPage: state.paginated.currentPage,
nPages: state.paginated.nPages nPages: state.paginated.nPages,
}; };
}; };
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
actions: bindActionCreators(actionCreators, dispatch) actions: bindActionCreators(actionCreators, dispatch),
}); });
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(SongsPageIntl)); 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 React, { Component } 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 { Howl } from "howler";
import Immutable from "immutable";
// Actions
import * as actionCreators from "../actions"; import * as actionCreators from "../actions";
// Components
import WebPlayerComponent from "../components/elements/WebPlayer"; import WebPlayerComponent from "../components/elements/WebPlayer";
/**
* Webplayer container.
*/
class WebPlayer extends Component { class WebPlayer extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.play = this.play.bind(this); // Data attributes
this.howl = null; this.howl = null;
// Bind this
this.startPlaying = this.startPlaying.bind(this);
} }
componentDidMount() { componentDidMount() {
this.play(this.props.isPlaying); // 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 // Toggle play / pause
if (nextProps.isPlaying != this.props.isPlaying) { if (nextProps.isPlaying != this.props.isPlaying) {
// This check ensure we do not start multiple times the same music. // This check ensure we do not start playing multiple times the
this.play(nextProps); // same song
this.startPlaying(nextProps);
} }
// Toggle mute / unmute // If something is playing back
if (this.howl) { if (this.howl) {
// Set mute / unmute
this.howl.mute(nextProps.isMute); this.howl.mute(nextProps.isMute);
// Set volume
this.howl.volume(nextProps.volume / 100);
} }
} }
getCurrentTrackPath (props) { /**
return [ * Handle playback through Howler and Web Audio API.
"tracks", *
props.playlist.get(props.currentIndex) * @params props A set of props to use for setting play parameters.
]; */
} startPlaying(props) {
if (props.isPlaying && props.currentSong) {
play (props) { // If it should be playing any song
if (props.isPlaying) {
if (!this.howl) { if (!this.howl) {
const url = props.entities.getIn( // Build a new Howler object with current song to play
Array.concat([], this.getCurrentTrackPath(props), ["url"]) const url = props.currentSong.get("url");
);
if (!url) { if (!url) {
// TODO: Error handling // TODO: Error handling
console.error("URL not found.");
return; return;
} }
this.howl = new Howl({ this.howl = new Howl({
src: [url], src: [url],
html5: true, html5: true, // Use HTML5 by default to allow streaming
loop: false,
mute: props.isMute, 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(); this.howl.play();
} }
else { else {
// If it should not be playing
if (this.howl) { if (this.howl) {
// Pause any running music
this.howl.pause(); this.howl.pause();
} }
} }
} }
render() { 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")]);
}
const webplayerProps = { const webplayerProps = {
isPlaying: this.props.isPlaying, isPlaying: this.props.isPlaying,
isRandom: this.props.isRandom, isRandom: this.props.isRandom,
isRepeat: this.props.isRepeat, isRepeat: this.props.isRepeat,
isMute: this.props.isMute, isMute: this.props.isMute,
currentTrack: currentTrack, volume: this.props.volume,
currentArtist: currentArtist, 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()), onPlayPause: (() => this.props.actions.togglePlaying()),
onPrev: this.props.actions.playPrevious, onPrev: this.props.actions.playPrevious,
onSkip: this.props.actions.playNext, onSkip: this.props.actions.playNext,
onRandom: this.props.actions.toggleRandom, onRandom: this.props.actions.toggleRandom,
onRepeat: this.props.actions.toggleRepeat, onRepeat: this.props.actions.toggleRepeat,
onMute: this.props.actions.toggleMute onMute: this.props.actions.toggleMute,
}; };
return ( return (
<WebPlayerComponent {...webplayerProps} /> <WebPlayerComponent {...webplayerProps} />
); );
} }
} }
const mapStateToProps = (state) => {
const currentIndex = state.webplayer.currentIndex;
const playlist = state.webplayer.playlist;
const mapStateToProps = (state) => ({ // 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, isPlaying: state.webplayer.isPlaying,
isRandom: state.webplayer.isRandom, isRandom: state.webplayer.isRandom,
isRepeat: state.webplayer.isRepeat, isRepeat: state.webplayer.isRepeat,
isMute: state.webplayer.isMute, isMute: state.webplayer.isMute,
currentIndex: state.webplayer.currentIndex, volume: state.webplayer.volume,
playlist: state.webplayer.playlist currentIndex: currentIndex,
}); playlist: playlist,
currentSong: currentSong,
currentArtist: currentArtist,
};
};
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)(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 //# 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