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,
} },
}; };
}; };
@ -186,7 +186,7 @@ export default function (action, requestType, successType, failureType) {
* *
* Dispatches the CALL_API action to fetch these items. * 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) => { return (dispatch, getState) => {
// Get credentials from the state // Get credentials from the state
const { auth } = getState(); const { auth } = getState();
@ -222,7 +222,7 @@ export default function (action, requestType, successType, failureType) {
* *
* Dispatches the CALL_API action to fetch this item. * Dispatches the CALL_API action to fetch this item.
*/ */
const loadItem = function({ filter = null, include = [] } = {}) { const loadItem = function ({ filter = null, include = [] } = {}) {
return (dispatch, getState) => { return (dispatch, getState) => {
// Get credentials from the state // Get credentials from the state
const { auth } = getState(); const { auth } = getState();

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(),
dispatch({ }));
type: PUSH_PLAYLIST, // Stop playback
payload: { dispatch ({
playlist: [trackID], type: STOP_PLAYBACK,
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

@ -12,8 +12,8 @@
* *
* @return The element it was applied one, for chaining. * @return The element it was applied one, for chaining.
*/ */
$.fn.shake = function(intShakes, intDistance, intDuration) { $.fn.shake = function (intShakes, intDistance, intDuration) {
this.each(function() { this.each(function () {
$(this).css("position","relative"); $(this).css("position","relative");
for (let x=1; x<=intShakes; x++) { for (let x=1; x<=intShakes; x++) {
$(this).animate({left:(intDistance*-1)}, (((intDuration/intShakes)/4))) $(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. * Track row in an album tracks table.
*/ */
class AlbumTrackRowCSSIntl extends Component { class AlbumTrackRowCSSIntl extends Component {
render () { render() {
const { formatMessage } = this.props.intl; const { formatMessage } = this.props.intl;
const length = formatLength(this.props.track.get("time")); const length = formatLength(this.props.track.get("time"));
return ( return (
@ -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));
@ -54,7 +54,7 @@ export let AlbumTrackRow = injectIntl(CSSModules(AlbumTrackRowCSSIntl, css));
* Tracks table of an album. * Tracks table of an album.
*/ */
class AlbumTracksTableCSS extends Component { class AlbumTracksTableCSS extends Component {
render () { render() {
let rows = []; let rows = [];
// Build rows for each track // Build rows for each track
const playAction = this.props.playAction; const playAction = this.props.playAction;
@ -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);
@ -81,7 +81,7 @@ export let AlbumTracksTable = CSSModules(AlbumTracksTableCSS, css);
* An entire album row containing art and tracks table. * An entire album row containing art and tracks table.
*/ */
class AlbumRowCSS extends Component { class AlbumRowCSS extends Component {
render () { render() {
return ( return (
<div className="row" styleName="row"> <div className="row" styleName="row">
<div className="col-sm-offset-2 col-xs-9 col-sm-10" styleName="nameRow"> <div className="col-sm-offset-2 col-xs-9 col-sm-10" styleName="nameRow">
@ -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

@ -11,7 +11,7 @@ import DismissibleAlert from "./elements/DismissibleAlert";
* Paginated albums grid * Paginated albums grid
*/ */
export default class Albums extends Component { export default class Albums extends Component {
render () { render() {
// Handle error // Handle error
let error = null; let error = null;
if (this.props.error) { if (this.props.error) {
@ -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

@ -26,7 +26,7 @@ const artistMessages = defineMessages(messagesMap(Array.concat([], commonMessage
* Single artist page * Single artist page
*/ */
class ArtistCSS extends Component { class ArtistCSS extends Component {
render () { render() {
// Define loading message // Define loading message
let loading = null; let loading = null;
if (this.props.isFetching) { if (this.props.isFetching) {
@ -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

@ -11,7 +11,7 @@ import DismissibleAlert from "./elements/DismissibleAlert";
* Paginated artists grid * Paginated artists grid
*/ */
export default class Artists extends Component { export default class Artists extends Component {
render () { render() {
// Handle error // Handle error
let error = null; let error = null;
if (this.props.error) { if (this.props.error) {
@ -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

@ -6,7 +6,7 @@ import FontAwesome from "react-fontawesome";
import css from "../styles/Discover.scss"; import css from "../styles/Discover.scss";
export default class DiscoverCSS extends Component { export default class DiscoverCSS extends Component {
render () { render() {
const artistsAlbumsSongsDropdown = ( const artistsAlbumsSongsDropdown = (
<div className="btn-group"> <div className="btn-group">
<button type="button" className="btn btn-default dropdown-toggle" styleName="h2Title" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> <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 * Login form component
*/ */
class LoginFormCSSIntl extends Component { class LoginFormCSSIntl extends Component {
constructor (props) { constructor(props) {
super(props); super(props);
this.handleSubmit = this.handleSubmit.bind(this); // bind this to handleSubmit 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 * @return True if an error is set, false otherwise
*/ */
setError (formGroup, hasError) { setError(formGroup, hasError) {
if (hasError) { if (hasError) {
// If error is true, then add error class // If error is true, then add error class
formGroup.classList.add("has-error"); formGroup.classList.add("has-error");
@ -54,7 +54,7 @@ class LoginFormCSSIntl extends Component {
* *
* @param e JS Event. * @param e JS Event.
*/ */
handleSubmit (e) { handleSubmit(e) {
e.preventDefault(); e.preventDefault();
// Don't handle submit if already logging in // Don't handle submit if already logging in
@ -79,7 +79,7 @@ class LoginFormCSSIntl extends Component {
} }
} }
componentDidUpdate () { componentDidUpdate() {
if (this.props.error) { if (this.props.error) {
// On unsuccessful login, set error classes and shake the form // On unsuccessful login, set error classes and shake the form
$(this.refs.loginForm).shake(3, 10, 300); $(this.refs.loginForm).shake(3, 10, 300);
@ -89,7 +89,7 @@ class LoginFormCSSIntl extends Component {
} }
} }
render () { render() {
const {formatMessage} = this.props.intl; const {formatMessage} = this.props.intl;
// Handle info message // Handle info message
@ -187,7 +187,7 @@ export let LoginForm = injectIntl(CSSModules(LoginFormCSSIntl, css));
* Main login page, including title and login form. * Main login page, including title and login form.
*/ */
class LoginCSS extends Component { class LoginCSS extends Component {
render () { render() {
const greeting = ( const greeting = (
<p> <p>
<FormattedMessage {...loginMessages["app.login.greeting"]} /> <FormattedMessage {...loginMessages["app.login.greeting"]} />
@ -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

@ -30,7 +30,7 @@ const songsMessages = defineMessages(messagesMap(Array.concat([], commonMessages
* A single row for a single song in the songs table. * A single row for a single song in the songs table.
*/ */
class SongsTableRowCSSIntl extends Component { class SongsTableRowCSSIntl extends Component {
render () { render() {
const { formatMessage } = this.props.intl; const { formatMessage } = this.props.intl;
const length = formatLength(this.props.song.get("time")); const length = formatLength(this.props.song.get("time"));
@ -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));
@ -68,7 +68,7 @@ export let SongsTableRow = injectIntl(CSSModules(SongsTableRowCSSIntl, css));
* The songs table. * The songs table.
*/ */
class SongsTableCSS extends Component { class SongsTableCSS extends Component {
render () { render() {
// Handle filtering // Handle filtering
let displayedSongs = this.props.songs; let displayedSongs = this.props.songs;
if (this.props.filterText) { if (this.props.filterText) {
@ -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);
@ -144,10 +144,10 @@ export let SongsTable = CSSModules(SongsTableCSS, css);
* Complete songs table view with filter and pagination * Complete songs table view with filter and pagination
*/ */
export default class FilterablePaginatedSongsTable extends Component { 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
@ -160,13 +160,13 @@ export default class FilterablePaginatedSongsTable extends Component {
* *
* @param filterText Content of the filter input. * @param filterText Content of the filter input.
*/ */
handleUserInput (filterText) { handleUserInput(filterText) {
this.setState({ this.setState({
filterText: filterText filterText: filterText,
}); });
} }
render () { render() {
// Handle error // Handle error
let error = null; let error = null;
if (this.props.error) { if (this.props.error) {
@ -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

@ -6,7 +6,7 @@ import React, { Component, PropTypes } from "react";
* A dismissible Bootstrap alert. * A dismissible Bootstrap alert.
*/ */
export default class DismissibleAlert extends Component { export default class DismissibleAlert extends Component {
render () { render() {
// Set correct alert type // Set correct alert type
let alertType = "alert-danger"; let alertType = "alert-danger";
if (this.props.type) { if (this.props.type) {
@ -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

@ -20,7 +20,7 @@ const filterMessages = defineMessages(messagesMap(Array.concat([], messages)));
* Filter bar element with input filter. * Filter bar element with input filter.
*/ */
class FilterBarCSSIntl extends Component { class FilterBarCSSIntl extends Component {
constructor (props) { constructor(props) {
super(props); super(props);
// Bind this on methods // Bind this on methods
this.handleChange = this.handleChange.bind(this); this.handleChange = this.handleChange.bind(this);
@ -33,12 +33,12 @@ class FilterBarCSSIntl extends Component {
* *
* @param e A JS event. * @param e A JS event.
*/ */
handleChange (e) { handleChange(e) {
e.preventDefault(); e.preventDefault();
this.props.onUserInput(this.refs.filterTextInput.value); this.props.onUserInput(this.refs.filterTextInput.value);
} }
render () { render() {
const {formatMessage} = this.props.intl; const {formatMessage} = this.props.intl;
return ( return (
@ -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,
} },
}; };
@ -49,7 +49,7 @@ const ISOTOPE_OPTIONS = { /** Default options for Isotope grid layout. */
* A single item in the grid, art + text under the art. * A single item in the grid, art + text under the art.
*/ */
class GridItemCSSIntl extends Component { class GridItemCSSIntl extends Component {
render () { render() {
const {formatMessage} = this.props.intl; const {formatMessage} = this.props.intl;
// Get number of sub-items // Get number of sub-items
@ -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));
@ -94,7 +94,7 @@ export let GridItem = injectIntl(CSSModules(GridItemCSSIntl, css));
* A grid, formatted using Isotope.JS * A grid, formatted using Isotope.JS
*/ */
export class Grid extends Component { export class Grid extends Component {
constructor (props) { constructor(props) {
super(props); super(props);
// Init grid data member // Init grid data member
@ -108,7 +108,7 @@ export class Grid extends Component {
/** /**
* Create an isotope container if none already exist. * Create an isotope container if none already exist.
*/ */
createIsotopeContainer () { createIsotopeContainer() {
if (this.iso == null) { if (this.iso == null) {
this.iso = new Isotope(this.refs.grid, ISOTOPE_OPTIONS); this.iso = new Isotope(this.refs.grid, ISOTOPE_OPTIONS);
} }
@ -117,7 +117,7 @@ export class Grid extends Component {
/** /**
* Handle filtering on the grid. * Handle filtering on the grid.
*/ */
handleFiltering (props) { handleFiltering(props) {
// If no query provided, drop any filter in use // If no query provided, drop any filter in use
if (props.filterText == "") { if (props.filterText == "") {
return this.iso.arrange(ISOTOPE_OPTIONS); return this.iso.arrange(ISOTOPE_OPTIONS);
@ -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();
@ -169,7 +169,7 @@ export class Grid extends Component {
} }
} }
componentDidMount () { componentDidMount() {
// Setup grid // Setup grid
this.createIsotopeContainer(); this.createIsotopeContainer();
// Only arrange if there are elements to arrange // Only arrange if there are elements to arrange
@ -212,7 +212,7 @@ export class Grid extends Component {
} }
// Layout again after images are loaded // 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 // Layout after each image load, fix for responsive grid
if (!iso) { // Grid could have been destroyed in the meantime if (!iso) { // Grid could have been destroyed in the meantime
return; return;
@ -221,7 +221,7 @@ export class Grid extends Component {
}); });
} }
render () { render() {
// Handle loading // Handle loading
let loading = null; let loading = null;
if (this.props.isFetching) { if (this.props.isFetching) {
@ -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,
}; };
@ -272,11 +272,11 @@ Grid.propTypes = {
* Full grid with pagination and filtering input. * Full grid with pagination and filtering input.
*/ */
export default class FilterablePaginatedGrid extends Component { export default class FilterablePaginatedGrid extends Component {
constructor (props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
filterText: "" // No filterText at init filterText: "", // No filterText at init
}; };
// Bind this // Bind this
@ -290,13 +290,13 @@ export default class FilterablePaginatedGrid extends Component {
* *
* @param filterText Content of the filter input. * @param filterText Content of the filter input.
*/ */
handleUserInput (filterText) { handleUserInput(filterText) {
this.setState({ this.setState({
filterText: filterText filterText: filterText,
}); });
} }
render () { render() {
return ( return (
<div> <div>
<FilterBar filterText={this.state.filterText} onUserInput={this.handleUserInput} /> <FilterBar filterText={this.state.filterText} onUserInput={this.handleUserInput} />
@ -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

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

View File

@ -1,47 +1,66 @@
// 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);
} }
artOpacityHandler (ev) { /**
* Handle opacity on album art.
*
* Set opacity on mouseover / mouseout.
*
* @param ev A JS event.
*/
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";
} }
} }
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";
@ -25,7 +25,7 @@ const sidebarLayoutMessages = defineMessages(messagesMap(Array.concat([], common
* Sidebar layout component, putting children next to the sidebar menu. * Sidebar layout component, putting children next to the sidebar menu.
*/ */
class SidebarLayoutIntl extends Component { class SidebarLayoutIntl extends Component {
render () { render() {
const { formatMessage } = this.props.intl; const { formatMessage } = this.props.intl;
// Check active links // Check active links
@ -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

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

View File

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

View File

@ -7,12 +7,12 @@ import { connect } from "react-redux";
export class RequireAuthentication extends Component { export class RequireAuthentication extends Component {
componentWillMount () { componentWillMount() {
// Check authentication on mount // Check authentication on mount
this.checkAuth(this.props.isAuthenticated); this.checkAuth(this.props.isAuthenticated);
} }
componentWillUpdate (newProps) { componentWillUpdate(newProps) {
// Check authentication on update // Check authentication on update
this.checkAuth(newProps.isAuthenticated); this.checkAuth(newProps.isAuthenticated);
} }
@ -23,20 +23,20 @@ export class RequireAuthentication extends Component {
* @param isAuthenticated A boolean stating whether user has a valid * @param isAuthenticated A boolean stating whether user has a valid
* session or not. * session or not.
*/ */
checkAuth (isAuthenticated) { checkAuth(isAuthenticated) {
if (!isAuthenticated) { if (!isAuthenticated) {
// Redirect to login, redirecting to the actual page after login. // Redirect to login, redirecting to the actual page after login.
this.context.router.replace({ this.context.router.replace({
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,
} },
}); });
} }
} }
render () { render() {
return ( return (
<div> <div>
{this.props.isAuthenticated === true {this.props.isAuthenticated === true
@ -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

@ -29,7 +29,7 @@ class APIError extends Error {}
* @param response A XHR response object. * @param response A XHR response object.
* @return The response or a rejected Promise if the check failed. * @return The response or a rejected Promise if the check failed.
*/ */
function _checkHTTPStatus (response) { function _checkHTTPStatus(response) {
if (response.status >= 200 && response.status < 300) { if (response.status >= 200 && response.status < 300) {
return response; return response;
} else { } else {
@ -44,17 +44,17 @@ function _checkHTTPStatus (response) {
* @param responseText The text from the API response. * @param responseText The text from the API response.
* @return The response as a JS object or a rejected Promise on error. * @return The response as a JS object or a rejected Promise on error.
*/ */
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: {},
})); }));
} }
@ -65,14 +65,14 @@ function _parseToJSON (responseText) {
* @param jsonData A JS object representing the API response. * @param jsonData A JS object representing the API response.
* @return The input data or a rejected Promise if errors are present. * @return The input data or a rejected Promise if errors are present.
*/ */
function _checkAPIErrors (jsonData) { function _checkAPIErrors(jsonData) {
if (jsonData.error) { if (jsonData.error) {
return Promise.reject(jsonData.error); return Promise.reject(jsonData.error);
} else if (!jsonData) { } else if (!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;
@ -85,7 +85,7 @@ function _checkAPIErrors (jsonData) {
* @param jsonData A JS object representing the API response. * @param jsonData A JS object representing the API response.
* @return A fixed JS object. * @return A fixed JS object.
*/ */
function _uglyFixes (jsonData) { function _uglyFixes(jsonData) {
// Fix songs array // Fix songs array
let _uglyFixesSongs = function (songs) { let _uglyFixesSongs = function (songs) {
return songs.map(function (song) { return songs.map(function (song) {
@ -201,7 +201,7 @@ function _uglyFixes (jsonData) {
* *
* @return A fetching Promise. * @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 // Translate the API action to real API action
const APIAction = extraParams.filter ? action.rstrip("s") : action; const APIAction = extraParams.filter ? action.rstrip("s") : action;
// Set base params // Set base params
@ -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

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

View File

@ -10,7 +10,7 @@
* @param b Second Immutable object. * @param b Second Immutable object.
* @returns An Immutable object equal to a except for the items in b. * @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 a.filter(function (i) {
return b.indexOf(i) < 0; 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. * Get the preferred locales from the browser, as an array sorted by preferences.
*/ */
export function getBrowserLocales () { export function getBrowserLocales() {
let langs = []; let langs = [];
if (navigator.languages) { if (navigator.languages) {

View File

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

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

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

View File

@ -10,16 +10,16 @@ import Album from "../components/Album";
// TODO: AlbumPage should be scrolled ArtistPage // TODO: AlbumPage should be scrolled ArtistPage
export class AlbumPage extends Component { export class AlbumPage extends Component {
componentWillMount () { componentWillMount() {
// Load the data // Load the data
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"],
}); });
} }
render () { render() {
if (this.props.album) { if (this.props.album) {
return ( return (
<Album album={this.props.album} songs={this.props.songs} /> <Album album={this.props.album} songs={this.props.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

@ -25,13 +25,13 @@ const albumsMessages = defineMessages(messagesMap(Array.concat([], APIMessages))
* Albums page, grid layout of albums arts. * Albums page, grid layout of albums arts.
*/ */
class AlbumsPageIntl extends Component { class AlbumsPageIntl extends Component {
componentWillMount () { componentWillMount() {
// Load the data for current page // Load the data for current page
const currentPage = parseInt(this.props.location.query.page) || 1; const currentPage = parseInt(this.props.location.query.page) || 1;
this.props.actions.loadPaginatedAlbums({ pageNumber: currentPage }); this.props.actions.loadPaginatedAlbums({ pageNumber: currentPage });
} }
componentWillReceiveProps (nextProps) { componentWillReceiveProps(nextProps) {
// Load the data if page has changed // Load the data if page has changed
const currentPage = parseInt(this.props.location.query.page) || 1; const currentPage = parseInt(this.props.location.query.page) || 1;
const nextPage = parseInt(nextProps.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 // Unload data on page change
this.props.actions.clearResults(); this.props.actions.clearPaginatedResults();
} }
render () { render() {
const {formatMessage} = this.props.intl; const {formatMessage} = this.props.intl;
const pagination = buildPaginationObject(this.props.location, this.props.currentPage, this.props.nPages, this.props.actions.goToPage); 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, 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

@ -25,27 +25,27 @@ const artistMessages = defineMessages(messagesMap(Array.concat([], APIMessages))
* Single artist page. * Single artist page.
*/ */
class ArtistPageIntl extends Component { class ArtistPageIntl extends Component {
componentWillMount () { componentWillMount() {
// 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")],
}); });
} }
render () { render() {
const {formatMessage} = this.props.intl; const {formatMessage} = this.props.intl;
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

@ -25,13 +25,13 @@ const artistsMessages = defineMessages(messagesMap(Array.concat([], APIMessages)
* Grid of artists arts. * Grid of artists arts.
*/ */
class ArtistsPageIntl extends Component { class ArtistsPageIntl extends Component {
componentWillMount () { componentWillMount() {
// Load the data for the current page // Load the data for the current page
const currentPage = parseInt(this.props.location.query.page) || 1; const currentPage = parseInt(this.props.location.query.page) || 1;
this.props.actions.loadPaginatedArtists({pageNumber: currentPage}); this.props.actions.loadPaginatedArtists({pageNumber: currentPage});
} }
componentWillReceiveProps (nextProps) { componentWillReceiveProps(nextProps) {
// Load the data if page has changed // Load the data if page has changed
const currentPage = parseInt(this.props.location.query.page) || 1; const currentPage = parseInt(this.props.location.query.page) || 1;
const nextPage = parseInt(nextProps.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 // Unload data on page change
this.props.actions.clearResults(); this.props.actions.clearPaginatedResults();
} }
render () { render() {
const {formatMessage} = this.props.intl; const {formatMessage} = this.props.intl;
const pagination = buildPaginationObject(this.props.location, this.props.currentPage, this.props.nPages, this.props.actions.goToPage); 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) => ({ 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

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

View File

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

View File

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

View File

@ -14,7 +14,7 @@ import Login from "../components/Login";
* Login page * Login page
*/ */
export class LoginPage extends Component { export class LoginPage extends Component {
constructor (props) { constructor(props) {
super(props); super(props);
// Bind this // Bind this
@ -37,11 +37,11 @@ export class LoginPage extends Component {
} }
return { return {
pathname: redirectPathname, pathname: redirectPathname,
query: redirectQuery query: redirectQuery,
}; };
} }
componentWillMount () { componentWillMount() {
// This checks if the user is already connected or not and redirects // This checks if the user is already connected or not and redirects
// them if it is the case. // them if it is the case.
@ -67,14 +67,14 @@ export class LoginPage extends Component {
/** /**
* Handle click on submit button. * Handle click on submit button.
*/ */
handleSubmit (username, password, endpoint, rememberMe) { handleSubmit(username, password, endpoint, rememberMe) {
// Get page to redirect to // Get page to redirect to
const redirectTo = this._getRedirectTo(); const redirectTo = this._getRedirectTo();
// Trigger login action // Trigger login action
this.props.actions.loginUser(username, password, endpoint, rememberMe, redirectTo); this.props.actions.loginUser(username, password, endpoint, rememberMe, redirectTo);
} }
render () { render() {
return ( 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} /> <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 = { 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

@ -11,12 +11,12 @@ import * as actionCreators from "../actions";
* Logout page * Logout page
*/ */
export class LogoutPage extends Component { export class LogoutPage extends Component {
componentWillMount () { componentWillMount() {
// Logout when component is mounted // Logout when component is mounted
this.props.actions.logoutAndRedirect(); this.props.actions.logoutAndRedirect();
} }
render () { render() {
return ( return (
<div></div> <div></div>
); );
@ -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

@ -25,13 +25,13 @@ const songsMessages = defineMessages(messagesMap(Array.concat([], APIMessages)))
* Paginated table of available songs * Paginated table of available songs
*/ */
class SongsPageIntl extends Component { class SongsPageIntl extends Component {
componentWillMount () { componentWillMount() {
// Load the data for current page // Load the data for current page
const currentPage = parseInt(this.props.location.query.page) || 1; const currentPage = parseInt(this.props.location.query.page) || 1;
this.props.actions.loadPaginatedSongs({pageNumber: currentPage}); this.props.actions.loadPaginatedSongs({pageNumber: currentPage});
} }
componentWillReceiveProps (nextProps) { componentWillReceiveProps(nextProps) {
// Load the data if page has changed // Load the data if page has changed
const currentPage = parseInt(this.props.location.query.page) || 1; const currentPage = parseInt(this.props.location.query.page) || 1;
const nextPage = parseInt(nextProps.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 // Unload data on page change
this.props.actions.clearResults(); this.props.actions.clearPaginatedResults();
} }
render () { render() {
const {formatMessage} = this.props.intl; const {formatMessage} = this.props.intl;
const pagination = buildPaginationObject(this.props.location, this.props.currentPage, this.props.nPages, this.props.actions.goToPage); 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); 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
isPlaying: state.webplayer.isPlaying, const currentSong = state.entities.getIn(["entities", "song", playlist.get(currentIndex)]);
isRandom: state.webplayer.isRandom, let currentArtist = undefined;
isRepeat: state.webplayer.isRepeat, if (currentSong) {
isMute: state.webplayer.isMute, currentArtist = state.entities.getIn(["entities", "artist", currentSong.get("artist")]);
currentIndex: state.webplayer.currentIndex, }
playlist: state.webplayer.playlist 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) => ({ 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