Rework webplayer
Full rework of webplayer. Webplayer is back to its previous working state, and ready for further improvements.
This commit is contained in:
parent
fffe9c4cd3
commit
d8a7d4f66a
@ -43,6 +43,14 @@ module.exports = {
|
||||
"strict": [
|
||||
"error",
|
||||
],
|
||||
"comma-dangle": [
|
||||
"error",
|
||||
"always-multiline"
|
||||
],
|
||||
"space-before-function-paren": [
|
||||
"error",
|
||||
{ "anonymous": "always", "named": "never" }
|
||||
],
|
||||
"react/jsx-uses-react": "error",
|
||||
"react/jsx-uses-vars": "error",
|
||||
|
||||
|
@ -42,7 +42,7 @@ export default function (action, requestType, successType, failureType) {
|
||||
{
|
||||
artist: arrayOf(artist),
|
||||
album: arrayOf(album),
|
||||
song: arrayOf(song)
|
||||
song: arrayOf(song),
|
||||
},
|
||||
{
|
||||
// Use custom assignEntity function to delete useless fields
|
||||
@ -52,7 +52,7 @@ export default function (action, requestType, successType, failureType) {
|
||||
} else {
|
||||
output[key] = value;
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
};
|
||||
@ -80,9 +80,9 @@ export default function (action, requestType, successType, failureType) {
|
||||
type: itemName,
|
||||
result: jsonData.result[itemName],
|
||||
nPages: nPages,
|
||||
currentPage: pageNumber
|
||||
}
|
||||
}
|
||||
currentPage: pageNumber,
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
@ -104,7 +104,7 @@ export default function (action, requestType, successType, failureType) {
|
||||
return {
|
||||
type: requestType,
|
||||
payload: {
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@ -119,8 +119,8 @@ export default function (action, requestType, successType, failureType) {
|
||||
return {
|
||||
type: failureType,
|
||||
payload: {
|
||||
error: error
|
||||
}
|
||||
error: error,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@ -144,7 +144,7 @@ export default function (action, requestType, successType, failureType) {
|
||||
// Set extra params for pagination
|
||||
let extraParams = {
|
||||
offset: offset,
|
||||
limit: limit
|
||||
limit: limit,
|
||||
};
|
||||
|
||||
// Handle filter
|
||||
@ -165,13 +165,13 @@ export default function (action, requestType, successType, failureType) {
|
||||
dispatch: [
|
||||
fetchItemsRequest,
|
||||
null,
|
||||
fetchItemsFailure
|
||||
fetchItemsFailure,
|
||||
],
|
||||
action: action,
|
||||
auth: passphrase,
|
||||
username: username,
|
||||
extraParams: extraParams
|
||||
}
|
||||
extraParams: extraParams,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@ -186,7 +186,7 @@ export default function (action, requestType, successType, failureType) {
|
||||
*
|
||||
* Dispatches the CALL_API action to fetch these items.
|
||||
*/
|
||||
const loadPaginatedItems = function({ pageNumber = 1, limit = DEFAULT_LIMIT, filter = null, include = [] } = {}) {
|
||||
const loadPaginatedItems = function ({ pageNumber = 1, limit = DEFAULT_LIMIT, filter = null, include = [] } = {}) {
|
||||
return (dispatch, getState) => {
|
||||
// Get credentials from the state
|
||||
const { auth } = getState();
|
||||
@ -222,7 +222,7 @@ export default function (action, requestType, successType, failureType) {
|
||||
*
|
||||
* Dispatches the CALL_API action to fetch this item.
|
||||
*/
|
||||
const loadItem = function({ filter = null, include = [] } = {}) {
|
||||
const loadItem = function ({ filter = null, include = [] } = {}) {
|
||||
return (dispatch, getState) => {
|
||||
// Get credentials from the state
|
||||
const { auth } = getState();
|
||||
|
@ -41,13 +41,13 @@ export function loginKeepAlive(username, token, endpoint) {
|
||||
null,
|
||||
error => dispatch => {
|
||||
dispatch(loginUserFailure(error || new i18nRecord({ id: "app.login.expired", values: {}})));
|
||||
}
|
||||
},
|
||||
],
|
||||
action: "ping",
|
||||
auth: token,
|
||||
username: username,
|
||||
extraParams: {}
|
||||
}
|
||||
extraParams: {},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -72,8 +72,8 @@ export function loginUserSuccess(username, token, endpoint, rememberMe, timerID)
|
||||
token: token,
|
||||
endpoint: endpoint,
|
||||
rememberMe: rememberMe,
|
||||
timerID: timerID
|
||||
}
|
||||
timerID: timerID,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -94,8 +94,8 @@ export function loginUserFailure(error) {
|
||||
return {
|
||||
type: LOGIN_USER_FAILURE,
|
||||
payload: {
|
||||
error: error
|
||||
}
|
||||
error: error,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -111,8 +111,8 @@ export function loginUserExpired(error) {
|
||||
return {
|
||||
type: LOGIN_USER_EXPIRED,
|
||||
payload: {
|
||||
error: error
|
||||
}
|
||||
error: error,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -125,7 +125,7 @@ export const LOGIN_USER_REQUEST = "LOGIN_USER_REQUEST";
|
||||
*/
|
||||
export function loginUserRequest() {
|
||||
return {
|
||||
type: LOGIN_USER_REQUEST
|
||||
type: LOGIN_USER_REQUEST,
|
||||
};
|
||||
}
|
||||
|
||||
@ -152,7 +152,7 @@ export function logout() {
|
||||
Cookies.remove("token");
|
||||
Cookies.remove("endpoint");
|
||||
dispatch({
|
||||
type: LOGOUT_USER
|
||||
type: LOGOUT_USER,
|
||||
});
|
||||
};
|
||||
}
|
||||
@ -223,7 +223,7 @@ export function loginUser(username, passwordOrToken, endpoint, rememberMe, redir
|
||||
// Get token from the API
|
||||
const token = {
|
||||
token: jsonData.auth,
|
||||
expires: new Date(jsonData.sessionExpire)
|
||||
expires: new Date(jsonData.sessionExpire),
|
||||
};
|
||||
// Handle session keep alive timer
|
||||
const timerID = setInterval(
|
||||
@ -242,12 +242,12 @@ export function loginUser(username, passwordOrToken, endpoint, rememberMe, redir
|
||||
// Redirect
|
||||
dispatch(push(redirect));
|
||||
},
|
||||
loginUserFailure
|
||||
loginUserFailure,
|
||||
],
|
||||
action: "handshake",
|
||||
auth: passphrase,
|
||||
username: username,
|
||||
extraParams: {timestamp: time}
|
||||
}
|
||||
extraParams: {timestamp: time},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -17,8 +17,8 @@ export function pushEntities(entities, refCountType=["album", "artist", "song"])
|
||||
type: PUSH_ENTITIES,
|
||||
payload: {
|
||||
entities: entities,
|
||||
refCountType: refCountType
|
||||
}
|
||||
refCountType: refCountType,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -36,8 +36,8 @@ export function incrementRefCount(entities) {
|
||||
return {
|
||||
type: INCREMENT_REFCOUNT,
|
||||
payload: {
|
||||
entities: entities
|
||||
}
|
||||
entities: entities,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -55,7 +55,7 @@ export function decrementRefCount(entities) {
|
||||
return {
|
||||
type: DECREMENT_REFCOUNT,
|
||||
payload: {
|
||||
entities: entities
|
||||
}
|
||||
entities: entities,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -7,8 +7,8 @@ import { decrementRefCount } from "./entities";
|
||||
|
||||
|
||||
/** Define an action to invalidate results in paginated store. */
|
||||
export const CLEAR_RESULTS = "CLEAR_RESULTS";
|
||||
export function clearResults() {
|
||||
export const CLEAR_PAGINATED_RESULTS = "CLEAR_PAGINATED_RESULTS";
|
||||
export function clearPaginatedResults() {
|
||||
return (dispatch, getState) => {
|
||||
// Decrement reference counter
|
||||
const paginatedStore = getState().paginated;
|
||||
@ -18,7 +18,7 @@ export function clearResults() {
|
||||
|
||||
// Clear results in store
|
||||
dispatch({
|
||||
type: CLEAR_RESULTS
|
||||
type: CLEAR_PAGINATED_RESULTS,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
@ -7,6 +7,6 @@
|
||||
export const INVALIDATE_STORE = "INVALIDATE_STORE";
|
||||
export function invalidateStore() {
|
||||
return {
|
||||
type: INVALIDATE_STORE
|
||||
type: INVALIDATE_STORE,
|
||||
};
|
||||
}
|
||||
|
@ -1,93 +1,284 @@
|
||||
// TODO: This file is not finished
|
||||
/**
|
||||
* These actions are actions acting on the webplayer.
|
||||
*/
|
||||
|
||||
// Other actions
|
||||
import { decrementRefCount, incrementRefCount } from "./entities";
|
||||
|
||||
|
||||
export const PLAY_PAUSE = "PLAY_PAUSE";
|
||||
/**
|
||||
* true to play, false to pause.
|
||||
* Toggle play / pause for the webplayer.
|
||||
*
|
||||
* @param playPause [Optional] True to play, false to pause. If not given,
|
||||
* toggle the current state.
|
||||
*
|
||||
* @return Dispatch a PLAY_PAUSE action.
|
||||
*/
|
||||
export function togglePlaying(playPause) {
|
||||
return (dispatch, getState) => {
|
||||
let isPlaying = false;
|
||||
let newIsPlaying = false;
|
||||
if (typeof playPause !== "undefined") {
|
||||
isPlaying = playPause;
|
||||
// If we want to force a mode
|
||||
newIsPlaying = playPause;
|
||||
} else {
|
||||
isPlaying = !(getState().webplayer.isPlaying);
|
||||
// Else, just toggle
|
||||
newIsPlaying = !(getState().webplayer.isPlaying);
|
||||
}
|
||||
// Dispatch action
|
||||
dispatch({
|
||||
type: PLAY_PAUSE,
|
||||
payload: {
|
||||
isPlaying: isPlaying
|
||||
}
|
||||
isPlaying: newIsPlaying,
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export const PUSH_PLAYLIST = "PUSH_PLAYLIST";
|
||||
export function playTrack(trackID) {
|
||||
|
||||
export const STOP_PLAYBACK = "STOP_PLAYBACK";
|
||||
/**
|
||||
* Stop the webplayer, clearing the playlist.
|
||||
*
|
||||
* Handle the entities store reference counting.
|
||||
*
|
||||
* @return Dispatch a STOP_PLAYBACK action.
|
||||
*/
|
||||
export function stopPlayback() {
|
||||
return (dispatch, getState) => {
|
||||
const track = getState().api.entities.getIn(["track", trackID]);
|
||||
const album = getState().api.entities.getIn(["album", track.get("album")]);
|
||||
const artist = getState().api.entities.getIn(["artist", track.get("artist")]);
|
||||
dispatch({
|
||||
type: PUSH_PLAYLIST,
|
||||
payload: {
|
||||
playlist: [trackID],
|
||||
tracks: [
|
||||
[trackID, track]
|
||||
],
|
||||
albums: [
|
||||
[album.get("id"), album]
|
||||
],
|
||||
artists: [
|
||||
[artist.get("id"), artist]
|
||||
]
|
||||
}
|
||||
// Handle reference counting
|
||||
dispatch(decrementRefCount({
|
||||
song: getState().webplayer.get("playlist").toArray(),
|
||||
}));
|
||||
// Stop playback
|
||||
dispatch ({
|
||||
type: STOP_PLAYBACK,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export const SET_PLAYLIST = "SET_PLAYLIST";
|
||||
/**
|
||||
* Set a given playlist.
|
||||
*
|
||||
* Handle the entities store reference counting.
|
||||
*
|
||||
* @param playlist A list of song IDs.
|
||||
*
|
||||
* @return Dispatch a SET_PLAYLIST action.
|
||||
*/
|
||||
export function setPlaylist(playlist) {
|
||||
return (dispatch, getState) => {
|
||||
// Handle reference counting
|
||||
dispatch(decrementRefCount({
|
||||
song: getState().webplayer.get("playlist").toArray(),
|
||||
}));
|
||||
dispatch(incrementRefCount({
|
||||
song: playlist,
|
||||
}));
|
||||
// Set playlist
|
||||
dispatch ({
|
||||
type: SET_PLAYLIST,
|
||||
payload: {
|
||||
playlist: playlist,
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Play a given song, emptying the current playlist.
|
||||
*
|
||||
* Handle the entities store reference counting.
|
||||
*
|
||||
* @param songID The id of the song to play.
|
||||
*
|
||||
* @return Dispatch a SET_PLAYLIST action to play this song and start playing.
|
||||
*/
|
||||
export function playSong(songID) {
|
||||
return (dispatch, getState) => {
|
||||
// Handle reference counting
|
||||
dispatch(decrementRefCount({
|
||||
song: getState().webplayer.get("playlist").toArray(),
|
||||
}));
|
||||
dispatch(incrementRefCount({
|
||||
song: [songID],
|
||||
}));
|
||||
// Set new playlist
|
||||
dispatch({
|
||||
type: SET_PLAYLIST,
|
||||
payload: {
|
||||
playlist: [songID],
|
||||
},
|
||||
});
|
||||
// Force playing
|
||||
dispatch(togglePlaying(true));
|
||||
};
|
||||
}
|
||||
|
||||
export const CHANGE_TRACK = "CHANGE_TRACK";
|
||||
|
||||
export const PUSH_SONG = "PUSH_SONG";
|
||||
/**
|
||||
* Push a given song in the playlist.
|
||||
*
|
||||
* Handle the entities store reference counting.
|
||||
*
|
||||
* @param songID The id of the song to push.
|
||||
* @param index [Optional] The position to insert at in the playlist.
|
||||
* If negative, counts from the end. Defaults to last.
|
||||
*
|
||||
* @return Dispatch a PUSH_SONG action.
|
||||
*/
|
||||
export function pushSong(songID, index=-1) {
|
||||
return (dispatch) => {
|
||||
// Handle reference counting
|
||||
dispatch(incrementRefCount({
|
||||
song: [songID],
|
||||
}));
|
||||
// Push song
|
||||
dispatch({
|
||||
type: PUSH_SONG,
|
||||
payload: {
|
||||
song: songID,
|
||||
index: index,
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export const POP_SONG = "POP_SONG";
|
||||
/**
|
||||
* Pop a given song from the playlist.
|
||||
*
|
||||
* Handle the entities store reference counting.
|
||||
*
|
||||
* @param songID The id of the song to pop.
|
||||
*
|
||||
* @return Dispatch a POP_SONG action.
|
||||
*/
|
||||
export function popSong(songID) {
|
||||
return (dispatch) => {
|
||||
// Handle reference counting
|
||||
dispatch(decrementRefCount({
|
||||
song: [songID],
|
||||
}));
|
||||
// Pop song
|
||||
dispatch({
|
||||
type: POP_SONG,
|
||||
payload: {
|
||||
song: songID,
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export const JUMP_TO_SONG = "JUMP_TO_SONG";
|
||||
/**
|
||||
* Set current playlist index to specific song.
|
||||
*
|
||||
* @param songID The id of the song to play.
|
||||
*
|
||||
* @return Dispatch a JUMP_TO_SONG action.
|
||||
*/
|
||||
export function jumpToSong(songID) {
|
||||
return (dispatch) => {
|
||||
// Push song
|
||||
dispatch({
|
||||
type: JUMP_TO_SONG,
|
||||
payload: {
|
||||
song: songID,
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export const PLAY_PREVIOUS = "PLAY_PREVIOUS";
|
||||
/**
|
||||
* Move one song backwards in the playlist.
|
||||
*
|
||||
* @return Dispatch a PLAY_PREVIOUS action.
|
||||
*/
|
||||
export function playPrevious() {
|
||||
// TODO: Playlist overflow
|
||||
return (dispatch, getState) => {
|
||||
let { index } = getState().webplayer;
|
||||
return (dispatch) => {
|
||||
dispatch({
|
||||
type: CHANGE_TRACK,
|
||||
payload: {
|
||||
index: index - 1
|
||||
}
|
||||
type: PLAY_PREVIOUS,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export const PLAY_NEXT = "PLAY_NEXT";
|
||||
/**
|
||||
* Move one song forward in the playlist.
|
||||
*
|
||||
* @return Dispatch a PLAY_NEXT action.
|
||||
*/
|
||||
export function playNext() {
|
||||
// TODO: Playlist overflow
|
||||
return (dispatch, getState) => {
|
||||
let { index } = getState().webplayer;
|
||||
return (dispatch) => {
|
||||
dispatch({
|
||||
type: CHANGE_TRACK,
|
||||
payload: {
|
||||
index: index + 1
|
||||
}
|
||||
type: PLAY_NEXT,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export const TOGGLE_RANDOM = "TOGGLE_RANDOM";
|
||||
/**
|
||||
* Toggle random mode.
|
||||
*
|
||||
* @return Dispatch a TOGGLE_RANDOM action.
|
||||
*/
|
||||
export function toggleRandom() {
|
||||
return {
|
||||
type: TOGGLE_RANDOM
|
||||
type: TOGGLE_RANDOM,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export const TOGGLE_REPEAT = "TOGGLE_REPEAT";
|
||||
/**
|
||||
* Toggle repeat mode.
|
||||
*
|
||||
* @return Dispatch a TOGGLE_REPEAT action.
|
||||
*/
|
||||
export function toggleRepeat() {
|
||||
return {
|
||||
type: TOGGLE_REPEAT
|
||||
type: TOGGLE_REPEAT,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export const TOGGLE_MUTE = "TOGGLE_MUTE";
|
||||
/**
|
||||
* Toggle mute mode.
|
||||
*
|
||||
* @return Dispatch a TOGGLE_MUTE action.
|
||||
*/
|
||||
export function toggleMute() {
|
||||
return {
|
||||
type: TOGGLE_MUTE
|
||||
type: TOGGLE_MUTE,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export const SET_VOLUME = "SET_VOLUME";
|
||||
/**
|
||||
* Set the volume.
|
||||
*
|
||||
* @param volume Volume to set (between 0 and 100)
|
||||
*
|
||||
* @return Dispatch a SET_VOLUME action.
|
||||
*/
|
||||
export function setVolume(volume) {
|
||||
return {
|
||||
type: SET_VOLUME,
|
||||
payload: {
|
||||
volume: volume,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
4
app/common/utils/jquery.js
vendored
4
app/common/utils/jquery.js
vendored
@ -12,8 +12,8 @@
|
||||
*
|
||||
* @return The element it was applied one, for chaining.
|
||||
*/
|
||||
$.fn.shake = function(intShakes, intDistance, intDuration) {
|
||||
this.each(function() {
|
||||
$.fn.shake = function (intShakes, intDistance, intDuration) {
|
||||
this.each(function () {
|
||||
$(this).css("position","relative");
|
||||
for (let x=1; x<=intShakes; x++) {
|
||||
$(this).animate({left:(intDistance*-1)}, (((intDuration/intShakes)/4)))
|
||||
|
@ -22,7 +22,7 @@ const albumMessages = defineMessages(messagesMap(Array.concat([], commonMessages
|
||||
* Track row in an album tracks table.
|
||||
*/
|
||||
class AlbumTrackRowCSSIntl extends Component {
|
||||
render () {
|
||||
render() {
|
||||
const { formatMessage } = this.props.intl;
|
||||
const length = formatLength(this.props.track.get("time"));
|
||||
return (
|
||||
@ -45,7 +45,7 @@ class AlbumTrackRowCSSIntl extends Component {
|
||||
AlbumTrackRowCSSIntl.propTypes = {
|
||||
playAction: PropTypes.func.isRequired,
|
||||
track: PropTypes.instanceOf(Immutable.Map).isRequired,
|
||||
intl: intlShape.isRequired
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
export let AlbumTrackRow = injectIntl(CSSModules(AlbumTrackRowCSSIntl, css));
|
||||
|
||||
@ -54,7 +54,7 @@ export let AlbumTrackRow = injectIntl(CSSModules(AlbumTrackRowCSSIntl, css));
|
||||
* Tracks table of an album.
|
||||
*/
|
||||
class AlbumTracksTableCSS extends Component {
|
||||
render () {
|
||||
render() {
|
||||
let rows = [];
|
||||
// Build rows for each track
|
||||
const playAction = this.props.playAction;
|
||||
@ -72,7 +72,7 @@ class AlbumTracksTableCSS extends Component {
|
||||
}
|
||||
AlbumTracksTableCSS.propTypes = {
|
||||
playAction: PropTypes.func.isRequired,
|
||||
tracks: PropTypes.instanceOf(Immutable.List).isRequired
|
||||
tracks: PropTypes.instanceOf(Immutable.List).isRequired,
|
||||
};
|
||||
export let AlbumTracksTable = CSSModules(AlbumTracksTableCSS, css);
|
||||
|
||||
@ -81,7 +81,7 @@ export let AlbumTracksTable = CSSModules(AlbumTracksTableCSS, css);
|
||||
* An entire album row containing art and tracks table.
|
||||
*/
|
||||
class AlbumRowCSS extends Component {
|
||||
render () {
|
||||
render() {
|
||||
return (
|
||||
<div className="row" styleName="row">
|
||||
<div className="col-sm-offset-2 col-xs-9 col-sm-10" styleName="nameRow">
|
||||
@ -104,6 +104,6 @@ class AlbumRowCSS extends Component {
|
||||
AlbumRowCSS.propTypes = {
|
||||
playAction: PropTypes.func.isRequired,
|
||||
album: PropTypes.instanceOf(Immutable.Map).isRequired,
|
||||
songs: PropTypes.instanceOf(Immutable.List).isRequired
|
||||
songs: PropTypes.instanceOf(Immutable.List).isRequired,
|
||||
};
|
||||
export let AlbumRow = CSSModules(AlbumRowCSS, css);
|
||||
|
@ -11,7 +11,7 @@ import DismissibleAlert from "./elements/DismissibleAlert";
|
||||
* Paginated albums grid
|
||||
*/
|
||||
export default class Albums extends Component {
|
||||
render () {
|
||||
render() {
|
||||
// Handle error
|
||||
let error = null;
|
||||
if (this.props.error) {
|
||||
@ -25,7 +25,7 @@ export default class Albums extends Component {
|
||||
itemsType: "album",
|
||||
itemsLabel: "app.common.album",
|
||||
subItemsType: "tracks",
|
||||
subItemsLabel: "app.common.track"
|
||||
subItemsLabel: "app.common.track",
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -26,7 +26,7 @@ const artistMessages = defineMessages(messagesMap(Array.concat([], commonMessage
|
||||
* Single artist page
|
||||
*/
|
||||
class ArtistCSS extends Component {
|
||||
render () {
|
||||
render() {
|
||||
// Define loading message
|
||||
let loading = null;
|
||||
if (this.props.isFetching) {
|
||||
@ -87,6 +87,6 @@ ArtistCSS.propTypes = {
|
||||
playAction: PropTypes.func.isRequired,
|
||||
artist: PropTypes.instanceOf(Immutable.Map),
|
||||
albums: PropTypes.instanceOf(Immutable.List),
|
||||
songs: PropTypes.instanceOf(Immutable.Map)
|
||||
songs: PropTypes.instanceOf(Immutable.Map),
|
||||
};
|
||||
export default CSSModules(ArtistCSS, css);
|
||||
|
@ -11,7 +11,7 @@ import DismissibleAlert from "./elements/DismissibleAlert";
|
||||
* Paginated artists grid
|
||||
*/
|
||||
export default class Artists extends Component {
|
||||
render () {
|
||||
render() {
|
||||
// Handle error
|
||||
let error = null;
|
||||
if (this.props.error) {
|
||||
@ -25,7 +25,7 @@ export default class Artists extends Component {
|
||||
itemsType: "artist",
|
||||
itemsLabel: "app.common.artist",
|
||||
subItemsType: "albums",
|
||||
subItemsLabel: "app.common.album"
|
||||
subItemsLabel: "app.common.album",
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -6,7 +6,7 @@ import FontAwesome from "react-fontawesome";
|
||||
import css from "../styles/Discover.scss";
|
||||
|
||||
export default class DiscoverCSS extends Component {
|
||||
render () {
|
||||
render() {
|
||||
const artistsAlbumsSongsDropdown = (
|
||||
<div className="btn-group">
|
||||
<button type="button" className="btn btn-default dropdown-toggle" styleName="h2Title" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
|
@ -23,7 +23,7 @@ const loginMessages = defineMessages(messagesMap(Array.concat([], APIMessages, m
|
||||
* Login form component
|
||||
*/
|
||||
class LoginFormCSSIntl extends Component {
|
||||
constructor (props) {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleSubmit = this.handleSubmit.bind(this); // bind this to handleSubmit
|
||||
}
|
||||
@ -36,7 +36,7 @@ class LoginFormCSSIntl extends Component {
|
||||
*
|
||||
* @return True if an error is set, false otherwise
|
||||
*/
|
||||
setError (formGroup, hasError) {
|
||||
setError(formGroup, hasError) {
|
||||
if (hasError) {
|
||||
// If error is true, then add error class
|
||||
formGroup.classList.add("has-error");
|
||||
@ -54,7 +54,7 @@ class LoginFormCSSIntl extends Component {
|
||||
*
|
||||
* @param e JS Event.
|
||||
*/
|
||||
handleSubmit (e) {
|
||||
handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
// Don't handle submit if already logging in
|
||||
@ -79,7 +79,7 @@ class LoginFormCSSIntl extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate () {
|
||||
componentDidUpdate() {
|
||||
if (this.props.error) {
|
||||
// On unsuccessful login, set error classes and shake the form
|
||||
$(this.refs.loginForm).shake(3, 10, 300);
|
||||
@ -89,7 +89,7 @@ class LoginFormCSSIntl extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
render() {
|
||||
const {formatMessage} = this.props.intl;
|
||||
|
||||
// Handle info message
|
||||
@ -187,7 +187,7 @@ export let LoginForm = injectIntl(CSSModules(LoginFormCSSIntl, css));
|
||||
* Main login page, including title and login form.
|
||||
*/
|
||||
class LoginCSS extends Component {
|
||||
render () {
|
||||
render() {
|
||||
const greeting = (
|
||||
<p>
|
||||
<FormattedMessage {...loginMessages["app.login.greeting"]} />
|
||||
@ -212,6 +212,6 @@ LoginCSS.propTypes = {
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
isAuthenticating: PropTypes.bool,
|
||||
info: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
||||
error: PropTypes.oneOfType([PropTypes.string, PropTypes.object])
|
||||
error: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
||||
};
|
||||
export default CSSModules(LoginCSS, css);
|
||||
|
@ -30,7 +30,7 @@ const songsMessages = defineMessages(messagesMap(Array.concat([], commonMessages
|
||||
* A single row for a single song in the songs table.
|
||||
*/
|
||||
class SongsTableRowCSSIntl extends Component {
|
||||
render () {
|
||||
render() {
|
||||
const { formatMessage } = this.props.intl;
|
||||
|
||||
const length = formatLength(this.props.song.get("time"));
|
||||
@ -59,7 +59,7 @@ class SongsTableRowCSSIntl extends Component {
|
||||
SongsTableRowCSSIntl.propTypes = {
|
||||
playAction: PropTypes.func.isRequired,
|
||||
song: PropTypes.instanceOf(Immutable.Map).isRequired,
|
||||
intl: intlShape.isRequired
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
export let SongsTableRow = injectIntl(CSSModules(SongsTableRowCSSIntl, css));
|
||||
|
||||
@ -68,7 +68,7 @@ export let SongsTableRow = injectIntl(CSSModules(SongsTableRowCSSIntl, css));
|
||||
* The songs table.
|
||||
*/
|
||||
class SongsTableCSS extends Component {
|
||||
render () {
|
||||
render() {
|
||||
// Handle filtering
|
||||
let displayedSongs = this.props.songs;
|
||||
if (this.props.filterText) {
|
||||
@ -78,7 +78,7 @@ class SongsTableCSS extends Component {
|
||||
{
|
||||
"keys": ["name"],
|
||||
"threshold": 0.4,
|
||||
"include": ["score"]
|
||||
"include": ["score"],
|
||||
}).search(this.props.filterText);
|
||||
// Keep only items in results
|
||||
displayedSongs = displayedSongs.map(function (item) { return new Immutable.Map(item.item); });
|
||||
@ -135,7 +135,7 @@ class SongsTableCSS extends Component {
|
||||
SongsTableCSS.propTypes = {
|
||||
playAction: PropTypes.func.isRequired,
|
||||
songs: PropTypes.instanceOf(Immutable.List).isRequired,
|
||||
filterText: PropTypes.string
|
||||
filterText: PropTypes.string,
|
||||
};
|
||||
export let SongsTable = CSSModules(SongsTableCSS, css);
|
||||
|
||||
@ -144,10 +144,10 @@ export let SongsTable = CSSModules(SongsTableCSS, css);
|
||||
* Complete songs table view with filter and pagination
|
||||
*/
|
||||
export default class FilterablePaginatedSongsTable extends Component {
|
||||
constructor (props) {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
filterText: "" // Initial state, no filter text
|
||||
filterText: "", // Initial state, no filter text
|
||||
};
|
||||
|
||||
this.handleUserInput = this.handleUserInput.bind(this); // Bind this on user input handling
|
||||
@ -160,13 +160,13 @@ export default class FilterablePaginatedSongsTable extends Component {
|
||||
*
|
||||
* @param filterText Content of the filter input.
|
||||
*/
|
||||
handleUserInput (filterText) {
|
||||
handleUserInput(filterText) {
|
||||
this.setState({
|
||||
filterText: filterText
|
||||
filterText: filterText,
|
||||
});
|
||||
}
|
||||
|
||||
render () {
|
||||
render() {
|
||||
// Handle error
|
||||
let error = null;
|
||||
if (this.props.error) {
|
||||
@ -176,13 +176,13 @@ export default class FilterablePaginatedSongsTable extends Component {
|
||||
// Set props
|
||||
const filterProps = {
|
||||
filterText: this.state.filterText,
|
||||
onUserInput: this.handleUserInput
|
||||
onUserInput: this.handleUserInput,
|
||||
};
|
||||
const songsTableProps = {
|
||||
playAction: this.props.playAction,
|
||||
isFetching: this.props.isFetching,
|
||||
songs: this.props.songs,
|
||||
filterText: this.state.filterText
|
||||
filterText: this.state.filterText,
|
||||
};
|
||||
|
||||
return (
|
||||
@ -200,5 +200,5 @@ FilterablePaginatedSongsTable.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
error: PropTypes.string,
|
||||
songs: PropTypes.instanceOf(Immutable.List).isRequired,
|
||||
pagination: PropTypes.object.isRequired
|
||||
pagination: PropTypes.object.isRequired,
|
||||
};
|
||||
|
@ -6,7 +6,7 @@ import React, { Component, PropTypes } from "react";
|
||||
* A dismissible Bootstrap alert.
|
||||
*/
|
||||
export default class DismissibleAlert extends Component {
|
||||
render () {
|
||||
render() {
|
||||
// Set correct alert type
|
||||
let alertType = "alert-danger";
|
||||
if (this.props.type) {
|
||||
@ -27,5 +27,5 @@ export default class DismissibleAlert extends Component {
|
||||
}
|
||||
DismissibleAlert.propTypes = {
|
||||
type: PropTypes.string,
|
||||
text: PropTypes.string
|
||||
text: PropTypes.string,
|
||||
};
|
||||
|
@ -20,7 +20,7 @@ const filterMessages = defineMessages(messagesMap(Array.concat([], messages)));
|
||||
* Filter bar element with input filter.
|
||||
*/
|
||||
class FilterBarCSSIntl extends Component {
|
||||
constructor (props) {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
// Bind this on methods
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
@ -33,12 +33,12 @@ class FilterBarCSSIntl extends Component {
|
||||
*
|
||||
* @param e A JS event.
|
||||
*/
|
||||
handleChange (e) {
|
||||
handleChange(e) {
|
||||
e.preventDefault();
|
||||
this.props.onUserInput(this.refs.filterTextInput.value);
|
||||
}
|
||||
|
||||
render () {
|
||||
render() {
|
||||
const {formatMessage} = this.props.intl;
|
||||
|
||||
return (
|
||||
@ -60,6 +60,6 @@ class FilterBarCSSIntl extends Component {
|
||||
FilterBarCSSIntl.propTypes = {
|
||||
onUserInput: PropTypes.func,
|
||||
filterText: PropTypes.string,
|
||||
intl: intlShape.isRequired
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
export default injectIntl(CSSModules(FilterBarCSSIntl, css));
|
||||
|
@ -31,7 +31,7 @@ const gridMessages = defineMessages(messagesMap(Array.concat([], commonMessages,
|
||||
const ISOTOPE_OPTIONS = { /** Default options for Isotope grid layout. */
|
||||
getSortData: {
|
||||
name: ".name",
|
||||
nSubitems: ".sub-items .n-sub-items"
|
||||
nSubitems: ".sub-items .n-sub-items",
|
||||
},
|
||||
transitionDuration: 0,
|
||||
sortBy: "name",
|
||||
@ -40,8 +40,8 @@ const ISOTOPE_OPTIONS = { /** Default options for Isotope grid layout. */
|
||||
layoutMode: "fitRows",
|
||||
filter: "*",
|
||||
fitRows: {
|
||||
gutter: 0
|
||||
}
|
||||
gutter: 0,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@ -49,7 +49,7 @@ const ISOTOPE_OPTIONS = { /** Default options for Isotope grid layout. */
|
||||
* A single item in the grid, art + text under the art.
|
||||
*/
|
||||
class GridItemCSSIntl extends Component {
|
||||
render () {
|
||||
render() {
|
||||
const {formatMessage} = this.props.intl;
|
||||
|
||||
// Get number of sub-items
|
||||
@ -85,7 +85,7 @@ GridItemCSSIntl.propTypes = {
|
||||
itemsLabel: PropTypes.string.isRequired,
|
||||
subItemsType: PropTypes.string.isRequired,
|
||||
subItemsLabel: PropTypes.string.isRequired,
|
||||
intl: intlShape.isRequired
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
export let GridItem = injectIntl(CSSModules(GridItemCSSIntl, css));
|
||||
|
||||
@ -94,7 +94,7 @@ export let GridItem = injectIntl(CSSModules(GridItemCSSIntl, css));
|
||||
* A grid, formatted using Isotope.JS
|
||||
*/
|
||||
export class Grid extends Component {
|
||||
constructor (props) {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
// Init grid data member
|
||||
@ -108,7 +108,7 @@ export class Grid extends Component {
|
||||
/**
|
||||
* Create an isotope container if none already exist.
|
||||
*/
|
||||
createIsotopeContainer () {
|
||||
createIsotopeContainer() {
|
||||
if (this.iso == null) {
|
||||
this.iso = new Isotope(this.refs.grid, ISOTOPE_OPTIONS);
|
||||
}
|
||||
@ -117,7 +117,7 @@ export class Grid extends Component {
|
||||
/**
|
||||
* Handle filtering on the grid.
|
||||
*/
|
||||
handleFiltering (props) {
|
||||
handleFiltering(props) {
|
||||
// If no query provided, drop any filter in use
|
||||
if (props.filterText == "") {
|
||||
return this.iso.arrange(ISOTOPE_OPTIONS);
|
||||
@ -129,7 +129,7 @@ export class Grid extends Component {
|
||||
{
|
||||
"keys": ["name"],
|
||||
"threshold": 0.4,
|
||||
"include": ["score"]
|
||||
"include": ["score"],
|
||||
}
|
||||
).search(props.filterText);
|
||||
|
||||
@ -149,9 +149,9 @@ export class Grid extends Component {
|
||||
}
|
||||
return p;
|
||||
}, 0);
|
||||
}
|
||||
},
|
||||
},
|
||||
sortBy: "relevance"
|
||||
sortBy: "relevance",
|
||||
});
|
||||
this.iso.updateSortData();
|
||||
this.iso.arrange();
|
||||
@ -169,7 +169,7 @@ export class Grid extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
componentDidMount() {
|
||||
// Setup grid
|
||||
this.createIsotopeContainer();
|
||||
// Only arrange if there are elements to arrange
|
||||
@ -212,7 +212,7 @@ export class Grid extends Component {
|
||||
}
|
||||
|
||||
// Layout again after images are loaded
|
||||
imagesLoaded(this.refs.grid).on("progress", function() {
|
||||
imagesLoaded(this.refs.grid).on("progress", function () {
|
||||
// Layout after each image load, fix for responsive grid
|
||||
if (!iso) { // Grid could have been destroyed in the meantime
|
||||
return;
|
||||
@ -221,7 +221,7 @@ export class Grid extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
render () {
|
||||
render() {
|
||||
// Handle loading
|
||||
let loading = null;
|
||||
if (this.props.isFetching) {
|
||||
@ -264,7 +264,7 @@ Grid.propTypes = {
|
||||
itemsLabel: PropTypes.string.isRequired,
|
||||
subItemsType: PropTypes.string.isRequired,
|
||||
subItemsLabel: PropTypes.string.isRequired,
|
||||
filterText: PropTypes.string
|
||||
filterText: PropTypes.string,
|
||||
};
|
||||
|
||||
|
||||
@ -272,11 +272,11 @@ Grid.propTypes = {
|
||||
* Full grid with pagination and filtering input.
|
||||
*/
|
||||
export default class FilterablePaginatedGrid extends Component {
|
||||
constructor (props) {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
filterText: "" // No filterText at init
|
||||
filterText: "", // No filterText at init
|
||||
};
|
||||
|
||||
// Bind this
|
||||
@ -290,13 +290,13 @@ export default class FilterablePaginatedGrid extends Component {
|
||||
*
|
||||
* @param filterText Content of the filter input.
|
||||
*/
|
||||
handleUserInput (filterText) {
|
||||
handleUserInput(filterText) {
|
||||
this.setState({
|
||||
filterText: filterText
|
||||
filterText: filterText,
|
||||
});
|
||||
}
|
||||
|
||||
render () {
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<FilterBar filterText={this.state.filterText} onUserInput={this.handleUserInput} />
|
||||
@ -309,5 +309,5 @@ export default class FilterablePaginatedGrid extends Component {
|
||||
|
||||
FilterablePaginatedGrid.propTypes = {
|
||||
grid: PropTypes.object.isRequired,
|
||||
pagination: PropTypes.object.isRequired
|
||||
pagination: PropTypes.object.isRequired,
|
||||
};
|
||||
|
@ -22,7 +22,7 @@ const paginationMessages = defineMessages(messagesMap(Array.concat([], commonMes
|
||||
* Pagination button bar
|
||||
*/
|
||||
class PaginationCSSIntl extends Component {
|
||||
constructor (props) {
|
||||
constructor(props) {
|
||||
super (props);
|
||||
|
||||
// Bind this
|
||||
@ -74,7 +74,7 @@ class PaginationCSSIntl extends Component {
|
||||
$(this.refs.paginationModal).modal("hide");
|
||||
}
|
||||
|
||||
render () {
|
||||
render() {
|
||||
const { formatMessage } = this.props.intl;
|
||||
|
||||
// Get bounds
|
||||
|
@ -1,47 +1,66 @@
|
||||
// TODO: This file is to review
|
||||
// NPM imports
|
||||
import React, { Component, PropTypes } from "react";
|
||||
import CSSModules from "react-css-modules";
|
||||
import { defineMessages, injectIntl, intlShape, FormattedMessage } from "react-intl";
|
||||
import FontAwesome from "react-fontawesome";
|
||||
import Immutable from "immutable";
|
||||
import FontAwesome from "react-fontawesome";
|
||||
|
||||
// Local imports
|
||||
import { messagesMap } from "../../utils";
|
||||
|
||||
// Styles
|
||||
import css from "../../styles/elements/WebPlayer.scss";
|
||||
|
||||
// Translations
|
||||
import commonMessages from "../../locales/messagesDescriptors/common";
|
||||
import messages from "../../locales/messagesDescriptors/elements/WebPlayer";
|
||||
|
||||
// Define translations
|
||||
const webplayerMessages = defineMessages(messagesMap(Array.concat([], commonMessages, messages)));
|
||||
|
||||
|
||||
/**
|
||||
* Webplayer component.
|
||||
*/
|
||||
class WebPlayerCSSIntl extends Component {
|
||||
constructor (props) {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
// Bind this
|
||||
this.artOpacityHandler = this.artOpacityHandler.bind(this);
|
||||
}
|
||||
|
||||
artOpacityHandler (ev) {
|
||||
/**
|
||||
* Handle opacity on album art.
|
||||
*
|
||||
* Set opacity on mouseover / mouseout.
|
||||
*
|
||||
* @param ev A JS event.
|
||||
*/
|
||||
artOpacityHandler(ev) {
|
||||
if (ev.type == "mouseover") {
|
||||
// On mouse over, reduce opacity
|
||||
this.refs.art.style.opacity = "1";
|
||||
this.refs.artText.style.display = "none";
|
||||
} else {
|
||||
// On mouse out, set opacity back
|
||||
this.refs.art.style.opacity = "0.75";
|
||||
this.refs.artText.style.display = "block";
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
render() {
|
||||
const { formatMessage } = this.props.intl;
|
||||
|
||||
const song = this.props.currentTrack;
|
||||
if (!song) {
|
||||
return (<div></div>);
|
||||
}
|
||||
// Get current song (eventually undefined)
|
||||
const song = this.props.currentSong;
|
||||
|
||||
// Current status (play or pause) for localization
|
||||
const playPause = this.props.isPlaying ? "pause" : "play";
|
||||
const volumeMute = this.props.isMute ? "volume-off" : "volume-up";
|
||||
// Volume fontawesome icon
|
||||
const volumeIcon = this.props.isMute ? "volume-off" : "volume-up";
|
||||
|
||||
// Get classes for random and repeat buttons
|
||||
const randomBtnStyles = ["randomBtn"];
|
||||
const repeatBtnStyles = ["repeatBtn"];
|
||||
if (this.props.isRandom) {
|
||||
@ -51,18 +70,30 @@ class WebPlayerCSSIntl extends Component {
|
||||
repeatBtnStyles.push("active");
|
||||
}
|
||||
|
||||
// Check if a song is currently playing
|
||||
let art = null;
|
||||
let songTitle = null;
|
||||
let artistName = null;
|
||||
if (song) {
|
||||
art = song.get("art");
|
||||
songTitle = song.get("title");
|
||||
if (this.props.currentArtist) {
|
||||
artistName = this.props.currentArtist.get("name");
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div id="row" styleName="webplayer">
|
||||
<div className="col-xs-12">
|
||||
<div className="row" styleName="artRow" onMouseOver={this.artOpacityHandler} onMouseOut={this.artOpacityHandler}>
|
||||
<div className="col-xs-12">
|
||||
<img src={song.get("art")} width="200" height="200" alt={formatMessage(webplayerMessages["app.common.art"])} ref="art" styleName="art" />
|
||||
<img src={art} width="200" height="200" alt={formatMessage(webplayerMessages["app.common.art"])} ref="art" styleName="art" />
|
||||
<div ref="artText">
|
||||
<h2>{song.get("title")}</h2>
|
||||
<h2>{songTitle}</h2>
|
||||
<h3>
|
||||
<span className="text-capitalize">
|
||||
<FormattedMessage {...webplayerMessages["app.webplayer.by"]} />
|
||||
</span> { this.props.currentArtist.get("name") }
|
||||
</span> { artistName }
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
@ -82,7 +113,7 @@ class WebPlayerCSSIntl extends Component {
|
||||
</div>
|
||||
<div className="col-xs-12">
|
||||
<button styleName="volumeBtn" aria-label={formatMessage(webplayerMessages["app.webplayer.volume"])} title={formatMessage(webplayerMessages["app.webplayer.volume"])} onClick={this.props.onMute}>
|
||||
<FontAwesome name={volumeMute} />
|
||||
<FontAwesome name={volumeIcon} />
|
||||
</button>
|
||||
<button styleName={repeatBtnStyles.join(" ")} aria-label={formatMessage(webplayerMessages["app.webplayer.repeat"])} title={formatMessage(webplayerMessages["app.webplayer.repeat"])} aria-pressed={this.props.isRepeat} onClick={this.props.onRepeat}>
|
||||
<FontAwesome name="repeat" />
|
||||
@ -106,7 +137,10 @@ WebPlayerCSSIntl.propTypes = {
|
||||
isRandom: PropTypes.bool.isRequired,
|
||||
isRepeat: PropTypes.bool.isRequired,
|
||||
isMute: PropTypes.bool.isRequired,
|
||||
currentTrack: PropTypes.instanceOf(Immutable.Map),
|
||||
volume: PropTypes.number.isRequired,
|
||||
currentIndex: PropTypes.number.isRequired,
|
||||
playlist: PropTypes.instanceOf(Immutable.List).isRequired,
|
||||
currentSong: PropTypes.instanceOf(Immutable.Map),
|
||||
currentArtist: PropTypes.instanceOf(Immutable.Map),
|
||||
onPlayPause: PropTypes.func.isRequired,
|
||||
onPrev: PropTypes.func.isRequired,
|
||||
@ -114,7 +148,7 @@ WebPlayerCSSIntl.propTypes = {
|
||||
onRandom: PropTypes.func.isRequired,
|
||||
onRepeat: PropTypes.func.isRequired,
|
||||
onMute: PropTypes.func.isRequired,
|
||||
intl: intlShape.isRequired
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(CSSModules(WebPlayerCSSIntl, css, { allowMultiple: true }));
|
||||
|
@ -8,7 +8,7 @@ import { defineMessages, injectIntl, intlShape, FormattedMessage } from "react-i
|
||||
import { messagesMap } from "../../utils";
|
||||
|
||||
// Other components
|
||||
/* import WebPlayer from "../../views/WebPlayer"; TODO */
|
||||
import WebPlayer from "../../views/WebPlayer";
|
||||
|
||||
// Translations
|
||||
import commonMessages from "../../locales/messagesDescriptors/common";
|
||||
@ -25,7 +25,7 @@ const sidebarLayoutMessages = defineMessages(messagesMap(Array.concat([], common
|
||||
* Sidebar layout component, putting children next to the sidebar menu.
|
||||
*/
|
||||
class SidebarLayoutIntl extends Component {
|
||||
render () {
|
||||
render() {
|
||||
const { formatMessage } = this.props.intl;
|
||||
|
||||
// Check active links
|
||||
@ -35,7 +35,7 @@ class SidebarLayoutIntl extends Component {
|
||||
artists: (this.props.location.pathname == "/artists") ? "active" : "link",
|
||||
albums: (this.props.location.pathname == "/albums") ? "active" : "link",
|
||||
songs: (this.props.location.pathname == "/songs") ? "active" : "link",
|
||||
search: (this.props.location.pathname == "/search") ? "active" : "link"
|
||||
search: (this.props.location.pathname == "/search") ? "active" : "link",
|
||||
};
|
||||
|
||||
// Hamburger collapsing function
|
||||
@ -146,7 +146,7 @@ class SidebarLayoutIntl extends Component {
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
{ /** TODO <WebPlayer /> */ }
|
||||
<WebPlayer />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -159,6 +159,6 @@ class SidebarLayoutIntl extends Component {
|
||||
}
|
||||
SidebarLayoutIntl.propTypes = {
|
||||
children: PropTypes.node,
|
||||
intl: intlShape.isRequired
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
export default injectIntl(CSSModules(SidebarLayoutIntl, css));
|
||||
|
@ -6,7 +6,7 @@ import React, { Component } from "react";
|
||||
* Simple layout, meaning just enclosing children in a div.
|
||||
*/
|
||||
export default class SimpleLayout extends Component {
|
||||
render () {
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
{this.props.children}
|
||||
|
@ -6,7 +6,7 @@
|
||||
import React, { Component, PropTypes } from "react";
|
||||
|
||||
export default class App extends Component {
|
||||
render () {
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
{this.props.children}
|
||||
|
@ -7,12 +7,12 @@ import { connect } from "react-redux";
|
||||
|
||||
|
||||
export class RequireAuthentication extends Component {
|
||||
componentWillMount () {
|
||||
componentWillMount() {
|
||||
// Check authentication on mount
|
||||
this.checkAuth(this.props.isAuthenticated);
|
||||
}
|
||||
|
||||
componentWillUpdate (newProps) {
|
||||
componentWillUpdate(newProps) {
|
||||
// Check authentication on update
|
||||
this.checkAuth(newProps.isAuthenticated);
|
||||
}
|
||||
@ -23,20 +23,20 @@ export class RequireAuthentication extends Component {
|
||||
* @param isAuthenticated A boolean stating whether user has a valid
|
||||
* session or not.
|
||||
*/
|
||||
checkAuth (isAuthenticated) {
|
||||
checkAuth(isAuthenticated) {
|
||||
if (!isAuthenticated) {
|
||||
// Redirect to login, redirecting to the actual page after login.
|
||||
this.context.router.replace({
|
||||
pathname: "/login",
|
||||
state: {
|
||||
nextPathname: this.props.location.pathname,
|
||||
nextQuery: this.props.location.query
|
||||
}
|
||||
nextQuery: this.props.location.query,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
{this.props.isAuthenticated === true
|
||||
@ -50,15 +50,15 @@ export class RequireAuthentication extends Component {
|
||||
|
||||
RequireAuthentication.propTypes = {
|
||||
// Injected by React Router
|
||||
children: PropTypes.node
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
RequireAuthentication.contextTypes = {
|
||||
router: PropTypes.object.isRequired
|
||||
router: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
isAuthenticated: state.auth.isAuthenticated
|
||||
isAuthenticated: state.auth.isAuthenticated,
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(RequireAuthentication);
|
||||
|
@ -27,5 +27,5 @@ Root.propTypes = {
|
||||
render: PropTypes.func,
|
||||
locale: PropTypes.string.isRequired,
|
||||
messages: PropTypes.object.isRequired,
|
||||
defaultLocale: PropTypes.string.isRequired
|
||||
defaultLocale: PropTypes.string.isRequired,
|
||||
};
|
||||
|
@ -1,5 +1,5 @@
|
||||
// Export all the existing locales
|
||||
module.exports = {
|
||||
"en-US": require("./en-US"),
|
||||
"fr-FR": require("./fr-FR")
|
||||
"fr-FR": require("./fr-FR"),
|
||||
};
|
||||
|
@ -2,55 +2,55 @@ const messages = [
|
||||
{
|
||||
id: "app.login.username",
|
||||
defaultMessage: "Username",
|
||||
description: "Username input placeholder"
|
||||
description: "Username input placeholder",
|
||||
},
|
||||
{
|
||||
id: "app.login.password",
|
||||
defaultMessage: "Password",
|
||||
description: "Password input placeholder"
|
||||
description: "Password input placeholder",
|
||||
},
|
||||
{
|
||||
id: "app.login.signIn",
|
||||
defaultMessage: "Sign in",
|
||||
description: "Sign in"
|
||||
description: "Sign in",
|
||||
},
|
||||
{
|
||||
id: "app.login.endpointInputAriaLabel",
|
||||
defaultMessage: "URL of your Ampache instance (e.g. http://ampache.example.com)",
|
||||
description: "ARIA label for the endpoint input"
|
||||
description: "ARIA label for the endpoint input",
|
||||
},
|
||||
{
|
||||
id: "app.login.rememberMe",
|
||||
description: "Remember me checkbox label",
|
||||
defaultMessage: "Remember me"
|
||||
defaultMessage: "Remember me",
|
||||
},
|
||||
{
|
||||
id: "app.login.greeting",
|
||||
description: "Greeting to welcome the user to the app",
|
||||
defaultMessage: "Welcome back on Ampache, let's go!"
|
||||
defaultMessage: "Welcome back on Ampache, let's go!",
|
||||
},
|
||||
|
||||
// From the auth reducer
|
||||
{
|
||||
id: "app.login.connecting",
|
||||
defaultMessage: "Connecting…",
|
||||
description: "Info message while trying to connect"
|
||||
description: "Info message while trying to connect",
|
||||
},
|
||||
{
|
||||
id: "app.login.success",
|
||||
defaultMessage: "Successfully logged in as { username }!",
|
||||
description: "Info message on successful login."
|
||||
description: "Info message on successful login.",
|
||||
},
|
||||
{
|
||||
id: "app.login.byebye",
|
||||
defaultMessage: "See you soon!",
|
||||
description: "Info message on successful logout"
|
||||
description: "Info message on successful logout",
|
||||
},
|
||||
{
|
||||
id: "app.login.expired",
|
||||
defaultMessage: "Your session expired… =(",
|
||||
description: "Error message on expired session"
|
||||
}
|
||||
description: "Error message on expired session",
|
||||
},
|
||||
];
|
||||
|
||||
export default messages;
|
||||
|
@ -2,18 +2,18 @@ const messages = [
|
||||
{
|
||||
"id": "app.songs.title",
|
||||
"description": "Title (song)",
|
||||
"defaultMessage": "Title"
|
||||
"defaultMessage": "Title",
|
||||
},
|
||||
{
|
||||
"id": "app.songs.genre",
|
||||
"description": "Genre (song)",
|
||||
"defaultMessage": "Genre"
|
||||
"defaultMessage": "Genre",
|
||||
},
|
||||
{
|
||||
"id": "app.songs.length",
|
||||
"description": "Length (song)",
|
||||
"defaultMessage": "Length"
|
||||
}
|
||||
"defaultMessage": "Length",
|
||||
},
|
||||
];
|
||||
|
||||
export default messages;
|
||||
|
@ -2,18 +2,18 @@ const messages = [
|
||||
{
|
||||
id: "app.api.invalidResponse",
|
||||
defaultMessage: "Invalid response text.",
|
||||
description: "Invalid response from the API"
|
||||
description: "Invalid response from the API",
|
||||
},
|
||||
{
|
||||
id: "app.api.emptyResponse",
|
||||
defaultMessage: "Empty response text.",
|
||||
description: "Empty response from the API"
|
||||
description: "Empty response from the API",
|
||||
},
|
||||
{
|
||||
id: "app.api.error",
|
||||
defaultMessage: "Unknown API error.",
|
||||
description: "An unknown error occurred from the API"
|
||||
}
|
||||
description: "An unknown error occurred from the API",
|
||||
},
|
||||
];
|
||||
|
||||
export default messages;
|
||||
|
@ -2,52 +2,52 @@ const messages = [
|
||||
{
|
||||
id: "app.common.close",
|
||||
defaultMessage: "Close",
|
||||
description: "Close"
|
||||
description: "Close",
|
||||
},
|
||||
{
|
||||
id: "app.common.cancel",
|
||||
description: "Cancel",
|
||||
defaultMessage: "Cancel"
|
||||
defaultMessage: "Cancel",
|
||||
},
|
||||
{
|
||||
id: "app.common.go",
|
||||
description: "Go",
|
||||
defaultMessage: "Go"
|
||||
defaultMessage: "Go",
|
||||
},
|
||||
{
|
||||
id: "app.common.art",
|
||||
description: "Art",
|
||||
defaultMessage: "Art"
|
||||
defaultMessage: "Art",
|
||||
},
|
||||
{
|
||||
id: "app.common.artist",
|
||||
description: "Artist",
|
||||
defaultMessage: "{itemCount, plural, one {artist} other {artists}}"
|
||||
defaultMessage: "{itemCount, plural, one {artist} other {artists}}",
|
||||
},
|
||||
{
|
||||
id: "app.common.album",
|
||||
description: "Album",
|
||||
defaultMessage: "{itemCount, plural, one {album} other {albums}}"
|
||||
defaultMessage: "{itemCount, plural, one {album} other {albums}}",
|
||||
},
|
||||
{
|
||||
id: "app.common.track",
|
||||
description: "Track",
|
||||
defaultMessage: "{itemCount, plural, one {track} other {tracks}}"
|
||||
defaultMessage: "{itemCount, plural, one {track} other {tracks}}",
|
||||
},
|
||||
{
|
||||
id: "app.common.loading",
|
||||
description: "Loading indicator",
|
||||
defaultMessage: "Loading…"
|
||||
defaultMessage: "Loading…",
|
||||
},
|
||||
{
|
||||
id: "app.common.play",
|
||||
description: "Play icon description",
|
||||
defaultMessage: "Play"
|
||||
defaultMessage: "Play",
|
||||
},
|
||||
{
|
||||
id: "app.common.pause",
|
||||
description: "Pause icon description",
|
||||
defaultMessage: "Pause"
|
||||
defaultMessage: "Pause",
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -2,12 +2,12 @@ const messages = [
|
||||
{
|
||||
id: "app.filter.filter",
|
||||
defaultMessage: "Filter…",
|
||||
description: "Filtering input placeholder"
|
||||
description: "Filtering input placeholder",
|
||||
},
|
||||
{
|
||||
id: "app.filter.whatAreWeListeningToToday",
|
||||
description: "Description for the filter bar",
|
||||
defaultMessage: "What are we listening to today?"
|
||||
defaultMessage: "What are we listening to today?",
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -2,28 +2,28 @@ const messages = [
|
||||
{
|
||||
id: "app.pagination.goToPage",
|
||||
defaultMessage: "<span class=\"sr-only\">Go to page </span>{pageNumber}",
|
||||
description: "Link content to go to page N. span is here for screen-readers"
|
||||
description: "Link content to go to page N. span is here for screen-readers",
|
||||
},
|
||||
{
|
||||
id: "app.pagination.goToPageWithoutMarkup",
|
||||
defaultMessage: "Go to page {pageNumber}",
|
||||
description: "Link title to go to page N"
|
||||
description: "Link title to go to page N",
|
||||
},
|
||||
{
|
||||
id: "app.pagination.pageNavigation",
|
||||
defaultMessage: "Page navigation",
|
||||
description: "ARIA label for the nav block containing pagination"
|
||||
description: "ARIA label for the nav block containing pagination",
|
||||
},
|
||||
{
|
||||
id: "app.pagination.pageToGoTo",
|
||||
description: "Title of the pagination modal",
|
||||
defaultMessage: "Page to go to?"
|
||||
defaultMessage: "Page to go to?",
|
||||
},
|
||||
{
|
||||
id: "app.pagination.current",
|
||||
description: "Current (page)",
|
||||
defaultMessage: "current"
|
||||
}
|
||||
defaultMessage: "current",
|
||||
},
|
||||
];
|
||||
|
||||
export default messages;
|
||||
|
@ -2,38 +2,38 @@ const messages = [
|
||||
{
|
||||
id: "app.webplayer.by",
|
||||
defaultMessage: "by",
|
||||
description: "Artist affiliation of a song"
|
||||
description: "Artist affiliation of a song",
|
||||
},
|
||||
{
|
||||
id: "app.webplayer.previous",
|
||||
defaultMessage: "Previous",
|
||||
description: "Previous button description"
|
||||
description: "Previous button description",
|
||||
},
|
||||
{
|
||||
id: "app.webplayer.next",
|
||||
defaultMessage: "Next",
|
||||
description: "Next button description"
|
||||
description: "Next button description",
|
||||
},
|
||||
{
|
||||
id: "app.webplayer.volume",
|
||||
defaultMessage: "Volume",
|
||||
description: "Volume button description"
|
||||
description: "Volume button description",
|
||||
},
|
||||
{
|
||||
id: "app.webplayer.repeat",
|
||||
defaultMessage: "Repeat",
|
||||
description: "Repeat button description"
|
||||
description: "Repeat button description",
|
||||
},
|
||||
{
|
||||
id: "app.webplayer.random",
|
||||
defaultMessage: "Random",
|
||||
description: "Random button description"
|
||||
description: "Random button description",
|
||||
},
|
||||
{
|
||||
id: "app.webplayer.playlist",
|
||||
defaultMessage: "Playlist",
|
||||
description: "Playlist button description"
|
||||
}
|
||||
description: "Playlist button description",
|
||||
},
|
||||
];
|
||||
|
||||
export default messages;
|
||||
|
@ -2,13 +2,13 @@ const messages = [
|
||||
{
|
||||
id: "app.grid.goToArtistPage",
|
||||
defaultMessage: "Go to artist page",
|
||||
description: "Artist thumbnail link title"
|
||||
description: "Artist thumbnail link title",
|
||||
},
|
||||
{
|
||||
id: "app.grid.goToAlbumPage",
|
||||
defaultMessage: "Go to album page",
|
||||
description: "Album thumbnail link title"
|
||||
}
|
||||
description: "Album thumbnail link title",
|
||||
},
|
||||
];
|
||||
|
||||
export default messages;
|
||||
|
@ -2,53 +2,53 @@ const messages = [
|
||||
{
|
||||
id: "app.sidebarLayout.mainNavigationMenu",
|
||||
description: "ARIA label for the main navigation menu",
|
||||
defaultMessage: "Main navigation menu"
|
||||
defaultMessage: "Main navigation menu",
|
||||
},
|
||||
{
|
||||
id: "app.sidebarLayout.home",
|
||||
description: "Home",
|
||||
defaultMessage: "Home"
|
||||
defaultMessage: "Home",
|
||||
},
|
||||
{
|
||||
id: "app.sidebarLayout.settings",
|
||||
description: "Settings",
|
||||
defaultMessage: "Settings"
|
||||
defaultMessage: "Settings",
|
||||
},
|
||||
{
|
||||
id: "app.sidebarLayout.logout",
|
||||
description: "Logout",
|
||||
defaultMessage: "Logout"
|
||||
defaultMessage: "Logout",
|
||||
},
|
||||
{
|
||||
id: "app.sidebarLayout.discover",
|
||||
description: "Discover",
|
||||
defaultMessage: "Discover"
|
||||
defaultMessage: "Discover",
|
||||
},
|
||||
{
|
||||
id: "app.sidebarLayout.browse",
|
||||
description: "Browse",
|
||||
defaultMessage: "Browse"
|
||||
defaultMessage: "Browse",
|
||||
},
|
||||
{
|
||||
id: "app.sidebarLayout.browseArtists",
|
||||
description: "Browse artists",
|
||||
defaultMessage: "Browse artists"
|
||||
defaultMessage: "Browse artists",
|
||||
},
|
||||
{
|
||||
id: "app.sidebarLayout.browseAlbums",
|
||||
description: "Browse albums",
|
||||
defaultMessage: "Browse albums"
|
||||
defaultMessage: "Browse albums",
|
||||
},
|
||||
{
|
||||
id: "app.sidebarLayout.browseSongs",
|
||||
description: "Browse songs",
|
||||
defaultMessage: "Browse songs"
|
||||
defaultMessage: "Browse songs",
|
||||
},
|
||||
{
|
||||
id: "app.sidebarLayout.toggleNavigation",
|
||||
description: "Screen reader description of toggle navigation button",
|
||||
defaultMessage: "Toggle navigation"
|
||||
}
|
||||
defaultMessage: "Toggle navigation",
|
||||
},
|
||||
];
|
||||
|
||||
export default messages;
|
||||
|
@ -29,7 +29,7 @@ class APIError extends Error {}
|
||||
* @param response A XHR response object.
|
||||
* @return The response or a rejected Promise if the check failed.
|
||||
*/
|
||||
function _checkHTTPStatus (response) {
|
||||
function _checkHTTPStatus(response) {
|
||||
if (response.status >= 200 && response.status < 300) {
|
||||
return response;
|
||||
} else {
|
||||
@ -44,17 +44,17 @@ function _checkHTTPStatus (response) {
|
||||
* @param responseText The text from the API response.
|
||||
* @return The response as a JS object or a rejected Promise on error.
|
||||
*/
|
||||
function _parseToJSON (responseText) {
|
||||
function _parseToJSON(responseText) {
|
||||
let x2js = new X2JS({
|
||||
attributePrefix: "", // No prefix for attributes
|
||||
keepCData: false // Do not store __cdata and toString functions
|
||||
keepCData: false, // Do not store __cdata and toString functions
|
||||
});
|
||||
if (responseText) {
|
||||
return x2js.xml_str2json(responseText).root;
|
||||
}
|
||||
return Promise.reject(new i18nRecord({
|
||||
id: "app.api.invalidResponse",
|
||||
values: {}
|
||||
values: {},
|
||||
}));
|
||||
}
|
||||
|
||||
@ -65,14 +65,14 @@ function _parseToJSON (responseText) {
|
||||
* @param jsonData A JS object representing the API response.
|
||||
* @return The input data or a rejected Promise if errors are present.
|
||||
*/
|
||||
function _checkAPIErrors (jsonData) {
|
||||
function _checkAPIErrors(jsonData) {
|
||||
if (jsonData.error) {
|
||||
return Promise.reject(jsonData.error);
|
||||
} else if (!jsonData) {
|
||||
// No data returned
|
||||
return Promise.reject(new i18nRecord({
|
||||
id: "app.api.emptyResponse",
|
||||
values: {}
|
||||
values: {},
|
||||
}));
|
||||
}
|
||||
return jsonData;
|
||||
@ -85,7 +85,7 @@ function _checkAPIErrors (jsonData) {
|
||||
* @param jsonData A JS object representing the API response.
|
||||
* @return A fixed JS object.
|
||||
*/
|
||||
function _uglyFixes (jsonData) {
|
||||
function _uglyFixes(jsonData) {
|
||||
// Fix songs array
|
||||
let _uglyFixesSongs = function (songs) {
|
||||
return songs.map(function (song) {
|
||||
@ -201,7 +201,7 @@ function _uglyFixes (jsonData) {
|
||||
*
|
||||
* @return A fetching Promise.
|
||||
*/
|
||||
function doAPICall (endpoint, action, auth, username, extraParams) {
|
||||
function doAPICall(endpoint, action, auth, username, extraParams) {
|
||||
// Translate the API action to real API action
|
||||
const APIAction = extraParams.filter ? action.rstrip("s") : action;
|
||||
// Set base params
|
||||
@ -209,7 +209,7 @@ function doAPICall (endpoint, action, auth, username, extraParams) {
|
||||
version: API_VERSION,
|
||||
action: APIAction,
|
||||
auth: auth,
|
||||
user: username
|
||||
user: username,
|
||||
};
|
||||
// Extend with extraParams
|
||||
const params = Object.assign({}, baseParams, extraParams);
|
||||
|
@ -14,15 +14,15 @@ export const song = new Schema("song"); /** Song schema */
|
||||
// Explicit relations between them
|
||||
artist.define({ // Artist has albums and songs (tracks)
|
||||
albums: arrayOf(album),
|
||||
songs: arrayOf(song)
|
||||
songs: arrayOf(song),
|
||||
});
|
||||
|
||||
album.define({ // Album has artist, tracks and tags
|
||||
artist: artist,
|
||||
tracks: arrayOf(song)
|
||||
tracks: arrayOf(song),
|
||||
});
|
||||
|
||||
song.define({ // Track has artist and album
|
||||
artist: artist,
|
||||
album: album
|
||||
album: album,
|
||||
});
|
||||
|
@ -9,7 +9,7 @@ import Immutable from "immutable";
|
||||
/** Record to store token parameters */
|
||||
export const tokenRecord = Immutable.Record({
|
||||
token: null, /** Token string */
|
||||
expires: null /** Token expiration date */
|
||||
expires: null, /** Token expiration date */
|
||||
});
|
||||
|
||||
|
||||
@ -23,5 +23,5 @@ export const stateRecord = new Immutable.Record({
|
||||
isAuthenticating: false, /** Whether authentication is in progress or not */
|
||||
error: null, /** An error string */
|
||||
info: null, /** An info string */
|
||||
timerID: null /** Timer ID for setInterval calls to revive API session */
|
||||
timerID: null, /** Timer ID for setInterval calls to revive API session */
|
||||
});
|
||||
|
@ -12,11 +12,11 @@ export const stateRecord = new Immutable.Record({
|
||||
refCounts: new Immutable.Map({
|
||||
album: new Immutable.Map(),
|
||||
artist: new Immutable.Map(),
|
||||
song: new Immutable.Map()
|
||||
song: new Immutable.Map(),
|
||||
}), /** Map of id => reference count for each object type (garbage collection) */
|
||||
entities: new Immutable.Map({
|
||||
album: new Immutable.Map(),
|
||||
artist: new Immutable.Map(),
|
||||
song: new Immutable.Map()
|
||||
}) /** Map of id => entity for each object type */
|
||||
song: new Immutable.Map(),
|
||||
}), /** Map of id => entity for each object type */
|
||||
});
|
||||
|
@ -8,5 +8,5 @@ import Immutable from "immutable";
|
||||
/** i18n record for passing errors to be localized from actions to components */
|
||||
export const i18nRecord = new Immutable.Record({
|
||||
id: null, /** Translation message id */
|
||||
values: new Immutable.Map() /** Values to pass to formatMessage */
|
||||
values: new Immutable.Map(), /** Values to pass to formatMessage */
|
||||
});
|
||||
|
@ -6,5 +6,5 @@ export const stateRecord = new Immutable.Record({
|
||||
type: null, /** Type of the paginated entries */
|
||||
result: new Immutable.List(), /** List of IDs of the resulting entries, maps to the entities store */
|
||||
currentPage: 1, /** Number of current page */
|
||||
nPages: 1 /** Total number of page in this batch */
|
||||
nPages: 1, /** Total number of page in this batch */
|
||||
});
|
||||
|
@ -14,5 +14,5 @@ export const stateRecord = new Immutable.Record({
|
||||
isMute: false, /** Whether sound is muted or not */
|
||||
volume: 100, /** Current volume, between 0 and 100 */
|
||||
currentIndex: 0, /** Current index in the playlist */
|
||||
playlist: new Immutable.List() /** List of songs IDs, references songs in the entities store */
|
||||
playlist: new Immutable.List(), /** List of songs IDs, references songs in the entities store */
|
||||
});
|
||||
|
@ -68,8 +68,8 @@ export default createReducer(initialState, {
|
||||
isAuthenticating: true,
|
||||
info: new i18nRecord({
|
||||
id: "app.login.connecting",
|
||||
values: {}
|
||||
})
|
||||
values: {},
|
||||
}),
|
||||
});
|
||||
},
|
||||
[LOGIN_USER_SUCCESS]: (state, payload) => {
|
||||
@ -81,28 +81,28 @@ export default createReducer(initialState, {
|
||||
"rememberMe": payload.rememberMe,
|
||||
"info": new i18nRecord({
|
||||
id: "app.login.success",
|
||||
values: {username: payload.username}
|
||||
values: {username: payload.username},
|
||||
}),
|
||||
"timerID": payload.timerID
|
||||
"timerID": payload.timerID,
|
||||
});
|
||||
},
|
||||
[LOGIN_USER_FAILURE]: (state, payload) => {
|
||||
return new stateRecord({
|
||||
"error": payload.error
|
||||
"error": payload.error,
|
||||
});
|
||||
},
|
||||
[LOGIN_USER_EXPIRED]: (state, payload) => {
|
||||
return new stateRecord({
|
||||
"isAuthenticated": false,
|
||||
"error": payload.error
|
||||
"error": payload.error,
|
||||
});
|
||||
},
|
||||
[LOGOUT_USER]: () => {
|
||||
return new stateRecord({
|
||||
info: new i18nRecord({
|
||||
id: "app.login.byebye",
|
||||
values: {}
|
||||
})
|
||||
values: {},
|
||||
}),
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
@ -18,7 +18,7 @@ import {
|
||||
PUSH_ENTITIES,
|
||||
INCREMENT_REFCOUNT,
|
||||
DECREMENT_REFCOUNT,
|
||||
INVALIDATE_STORE
|
||||
INVALIDATE_STORE,
|
||||
} from "../actions";
|
||||
|
||||
|
||||
@ -171,9 +171,11 @@ export default createReducer(initialState, {
|
||||
|
||||
// Increment reference counter
|
||||
payload.refCountType.forEach(function (itemName) {
|
||||
newState.getIn(["entities", itemName]).forEach(function (entity, id) {
|
||||
const entities = payload.entities[itemName];
|
||||
for (let id in entities) {
|
||||
const entity = newState.getIn(["entities", itemName, id]);
|
||||
newState = updateEntityRefCount(newState, itemName, id, entity, 1);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return newState;
|
||||
@ -183,7 +185,9 @@ export default createReducer(initialState, {
|
||||
|
||||
// Increment reference counter
|
||||
for (let itemName in payload.entities) {
|
||||
newState.getIn(["entities", itemName]).forEach(function (entity, id) {
|
||||
const entities = payload.entities[itemName];
|
||||
entities.forEach(function (id) {
|
||||
const entity = newState.getIn(["entities", itemName, id]);
|
||||
newState = updateEntityRefCount(newState, itemName, id, entity, 1);
|
||||
});
|
||||
}
|
||||
@ -195,7 +199,9 @@ export default createReducer(initialState, {
|
||||
|
||||
// Decrement reference counter
|
||||
for (let itemName in payload.entities) {
|
||||
newState.getIn(["entities", itemName]).forEach(function (entity, id) {
|
||||
const entities = payload.entities[itemName];
|
||||
entities.forEach(function (id) {
|
||||
const entity = newState.getIn(["entities", itemName, id]);
|
||||
newState = updateEntityRefCount(newState, itemName, id, entity, -1);
|
||||
});
|
||||
}
|
||||
@ -207,5 +213,5 @@ export default createReducer(initialState, {
|
||||
},
|
||||
[INVALIDATE_STORE]: () => {
|
||||
return new stateRecord();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
@ -19,7 +19,7 @@ import * as ActionTypes from "../actions";
|
||||
const paginated = paginatedMaker([
|
||||
ActionTypes.API_REQUEST,
|
||||
ActionTypes.API_SUCCESS,
|
||||
ActionTypes.API_FAILURE
|
||||
ActionTypes.API_FAILURE,
|
||||
]);
|
||||
|
||||
// Export the combined reducers
|
||||
@ -28,5 +28,5 @@ export default combineReducers({
|
||||
auth,
|
||||
entities,
|
||||
paginated,
|
||||
webplayer
|
||||
webplayer,
|
||||
});
|
||||
|
@ -12,7 +12,7 @@ import { createReducer } from "../utils";
|
||||
import { stateRecord } from "../models/paginated";
|
||||
|
||||
// Actions
|
||||
import { CLEAR_RESULTS, INVALIDATE_STORE } from "../actions";
|
||||
import { CLEAR_PAGINATED_RESULTS, INVALIDATE_STORE } from "../actions";
|
||||
|
||||
|
||||
/** Initial state of the reducer */
|
||||
@ -50,12 +50,12 @@ export default function paginated(types) {
|
||||
[failureType]: (state) => {
|
||||
return state;
|
||||
},
|
||||
[CLEAR_RESULTS]: (state) => {
|
||||
[CLEAR_PAGINATED_RESULTS]: (state) => {
|
||||
return state.set("result", new Immutable.List());
|
||||
},
|
||||
[INVALIDATE_STORE]: () => {
|
||||
// Reset state on invalidation
|
||||
return new stateRecord();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -1,16 +1,32 @@
|
||||
// TODO: This is a WIP
|
||||
/**
|
||||
* This implements the webplayer reducers.
|
||||
*/
|
||||
|
||||
// NPM imports
|
||||
import Immutable from "immutable";
|
||||
|
||||
// Local imports
|
||||
import { createReducer } from "../utils";
|
||||
|
||||
// Models
|
||||
import { stateRecord } from "../models/webplayer";
|
||||
|
||||
// Actions
|
||||
import {
|
||||
PUSH_PLAYLIST,
|
||||
CHANGE_TRACK,
|
||||
PLAY_PAUSE,
|
||||
STOP_PLAYBACK,
|
||||
SET_PLAYLIST,
|
||||
PUSH_SONG,
|
||||
POP_SONG,
|
||||
JUMP_TO_SONG,
|
||||
PLAY_PREVIOUS,
|
||||
PLAY_NEXT,
|
||||
TOGGLE_RANDOM,
|
||||
TOGGLE_REPEAT,
|
||||
TOGGLE_MUTE,
|
||||
SET_VOLUME,
|
||||
INVALIDATE_STORE } from "../actions";
|
||||
import { createReducer } from "../utils";
|
||||
import { stateRecord } from "../models/webplayer";
|
||||
|
||||
|
||||
/**
|
||||
* Initial state
|
||||
@ -19,28 +35,80 @@ import { stateRecord } from "../models/webplayer";
|
||||
var initialState = new stateRecord();
|
||||
|
||||
|
||||
/**
|
||||
* Helper functions
|
||||
*/
|
||||
|
||||
/**
|
||||
* Stop playback in reducer helper.
|
||||
*
|
||||
* @param state Current state to update.
|
||||
*/
|
||||
function stopPlayback(state) {
|
||||
return (
|
||||
state
|
||||
.set("isPlaying", false)
|
||||
.set("currentIndex", 0)
|
||||
.set("playlist", new Immutable.List())
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reducers
|
||||
*/
|
||||
|
||||
export default createReducer(initialState, {
|
||||
[PLAY_PAUSE]: (state, payload) => {
|
||||
// Force play or pause
|
||||
return state.set("isPlaying", payload.isPlaying);
|
||||
},
|
||||
[CHANGE_TRACK]: (state, payload) => {
|
||||
return state.set("currentIndex", payload.index);
|
||||
[STOP_PLAYBACK]: (state) => {
|
||||
// Clear the playlist
|
||||
return stopPlayback(state);
|
||||
},
|
||||
[PUSH_PLAYLIST]: (state, payload) => {
|
||||
[SET_PLAYLIST]: (state, payload) => {
|
||||
// Set current playlist, reset playlist index
|
||||
return (
|
||||
state
|
||||
.set("playlist", new Immutable.List(payload.playlist))
|
||||
.setIn(["entities", "artists"], new Immutable.Map(payload.artists))
|
||||
.setIn(["entities", "albums"], new Immutable.Map(payload.albums))
|
||||
.setIn(["entities", "tracks"], new Immutable.Map(payload.tracks))
|
||||
.set("currentIndex", 0)
|
||||
.set("isPlaying", true)
|
||||
);
|
||||
},
|
||||
[PUSH_SONG]: (state, payload) => {
|
||||
// Push song to playlist
|
||||
const newPlaylist = state.get("playlist").insert(payload.index, payload.song);
|
||||
return state.set("playlist", newPlaylist);
|
||||
},
|
||||
[POP_SONG]: (state, payload) => {
|
||||
// Pop song from playlist
|
||||
return state.deleteIn(["playlist", payload.index]);
|
||||
},
|
||||
[JUMP_TO_SONG]: (state, payload) => {
|
||||
// Set current index
|
||||
const newCurrentIndex = state.get("playlist").findKey(x => x == payload.song);
|
||||
return state.set("currentIndex", newCurrentIndex);
|
||||
},
|
||||
[PLAY_PREVIOUS]: (state) => {
|
||||
const newIndex = state.get("currentIndex") - 1;
|
||||
if (newIndex < 0) {
|
||||
// If there is an overlow on the left of the playlist, just stop
|
||||
// playback
|
||||
return stopPlayback(state);
|
||||
} else {
|
||||
return state.set("currentIndex", newIndex);
|
||||
}
|
||||
},
|
||||
[PLAY_NEXT]: (state) => {
|
||||
const newIndex = state.get("currentIndex") + 1;
|
||||
if (newIndex > state.get("playlist").size) {
|
||||
// If there is an overflow, just stop playback
|
||||
return stopPlayback(state);
|
||||
} else {
|
||||
// Else, play next item
|
||||
return state.set("currentIndex", newIndex);
|
||||
}
|
||||
},
|
||||
[TOGGLE_RANDOM]: (state) => {
|
||||
return state.set("isRandom", !state.get("isRandom"));
|
||||
},
|
||||
@ -50,7 +118,10 @@ export default createReducer(initialState, {
|
||||
[TOGGLE_MUTE]: (state) => {
|
||||
return state.set("isMute", !state.get("isMute"));
|
||||
},
|
||||
[SET_VOLUME]: (state, payload) => {
|
||||
return state.set("volume", payload.volume);
|
||||
},
|
||||
[INVALIDATE_STORE]: () => {
|
||||
return new stateRecord();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
@ -15,7 +15,7 @@ import jsSHA from "jssha";
|
||||
* @remark This builds an HMAC as expected by Ampache API, which is not a
|
||||
* standard HMAC.
|
||||
*/
|
||||
export function buildHMAC (password) {
|
||||
export function buildHMAC(password) {
|
||||
const time = Math.floor(Date.now() / 1000);
|
||||
|
||||
let shaObj = new jsSHA("SHA-256", "TEXT");
|
||||
@ -27,6 +27,6 @@ export function buildHMAC (password) {
|
||||
|
||||
return {
|
||||
time: time,
|
||||
passphrase: shaObj.getHash("HEX")
|
||||
passphrase: shaObj.getHash("HEX"),
|
||||
};
|
||||
}
|
||||
|
@ -10,7 +10,7 @@
|
||||
* @param b Second Immutable object.
|
||||
* @returns An Immutable object equal to a except for the items in b.
|
||||
*/
|
||||
export function immutableDiff (a, b) {
|
||||
export function immutableDiff(a, b) {
|
||||
return a.filter(function (i) {
|
||||
return b.indexOf(i) < 0;
|
||||
});
|
||||
|
@ -6,7 +6,7 @@ import { i18nRecord } from "../models/i18n";
|
||||
/**
|
||||
* Get the preferred locales from the browser, as an array sorted by preferences.
|
||||
*/
|
||||
export function getBrowserLocales () {
|
||||
export function getBrowserLocales() {
|
||||
let langs = [];
|
||||
|
||||
if (navigator.languages) {
|
||||
|
@ -10,7 +10,7 @@
|
||||
* @return Either NaN if the string was not a valid int representation, or the
|
||||
* int.
|
||||
*/
|
||||
export function filterInt (value) {
|
||||
export function filterInt(value) {
|
||||
if (/^(\-|\+)?([0-9]+|Infinity)$/.test(value)) {
|
||||
return Number(value);
|
||||
}
|
||||
@ -24,7 +24,7 @@ export function filterInt (value) {
|
||||
* @param time Length of the song in seconds.
|
||||
* @return Formatted length as MM:SS.
|
||||
*/
|
||||
export function formatLength (time) {
|
||||
export function formatLength(time) {
|
||||
const min = Math.floor(time / 60);
|
||||
let sec = (time - 60 * min);
|
||||
if (sec < 10) {
|
||||
|
@ -17,14 +17,14 @@ export function buildPaginationObject(location, currentPage, nPages, goToPageAct
|
||||
const buildLinkToPage = function (pageNumber) {
|
||||
return {
|
||||
pathname: location.pathname,
|
||||
query: Object.assign({}, location.query, { page: pageNumber })
|
||||
query: Object.assign({}, location.query, { page: pageNumber }),
|
||||
};
|
||||
};
|
||||
return {
|
||||
currentPage: currentPage,
|
||||
nPages: nPages,
|
||||
goToPage: pageNumber => goToPageAction(buildLinkToPage(pageNumber)),
|
||||
buildLinkToPage: buildLinkToPage
|
||||
buildLinkToPage: buildLinkToPage,
|
||||
};
|
||||
}
|
||||
|
||||
@ -57,6 +57,6 @@ export function computePaginationBounds(currentPage, nPages, maxNumberPagesShown
|
||||
|
||||
return {
|
||||
lowerLimit: lowerLimit,
|
||||
upperLimit: upperLimit + 1 // +1 to ease iteration in for with <
|
||||
upperLimit: upperLimit + 1, // +1 to ease iteration in for with <
|
||||
};
|
||||
}
|
||||
|
@ -11,7 +11,7 @@
|
||||
*
|
||||
* @return A string with the full URL with GET params.
|
||||
*/
|
||||
export function assembleURLAndParams (endpoint, params) {
|
||||
export function assembleURLAndParams(endpoint, params) {
|
||||
let url = endpoint + "?";
|
||||
Object.keys(params).forEach(
|
||||
key => {
|
||||
@ -34,7 +34,7 @@ export function assembleURLAndParams (endpoint, params) {
|
||||
* @param An URL
|
||||
* @return The cleaned URL
|
||||
*/
|
||||
export function cleanURL (endpoint) {
|
||||
export function cleanURL(endpoint) {
|
||||
if (
|
||||
!endpoint.startsWith("//") &&
|
||||
!endpoint.startsWith("http://") &&
|
||||
|
@ -10,16 +10,16 @@ import Album from "../components/Album";
|
||||
// TODO: AlbumPage should be scrolled ArtistPage
|
||||
|
||||
export class AlbumPage extends Component {
|
||||
componentWillMount () {
|
||||
componentWillMount() {
|
||||
// Load the data
|
||||
this.props.actions.loadAlbums({
|
||||
pageNumber: 1,
|
||||
filter: this.props.params.id,
|
||||
include: ["songs"]
|
||||
include: ["songs"],
|
||||
});
|
||||
}
|
||||
|
||||
render () {
|
||||
render() {
|
||||
if (this.props.album) {
|
||||
return (
|
||||
<Album album={this.props.album} songs={this.props.songs} />
|
||||
@ -52,12 +52,12 @@ const mapStateToProps = (state, ownProps) => {
|
||||
}
|
||||
return {
|
||||
album: album,
|
||||
songs: songs
|
||||
songs: songs,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
actions: bindActionCreators(actionCreators, dispatch)
|
||||
actions: bindActionCreators(actionCreators, dispatch),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(AlbumPage);
|
||||
|
@ -25,13 +25,13 @@ const albumsMessages = defineMessages(messagesMap(Array.concat([], APIMessages))
|
||||
* Albums page, grid layout of albums arts.
|
||||
*/
|
||||
class AlbumsPageIntl extends Component {
|
||||
componentWillMount () {
|
||||
componentWillMount() {
|
||||
// Load the data for current page
|
||||
const currentPage = parseInt(this.props.location.query.page) || 1;
|
||||
this.props.actions.loadPaginatedAlbums({ pageNumber: currentPage });
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
componentWillReceiveProps(nextProps) {
|
||||
// Load the data if page has changed
|
||||
const currentPage = parseInt(this.props.location.query.page) || 1;
|
||||
const nextPage = parseInt(nextProps.location.query.page) || 1;
|
||||
@ -40,12 +40,12 @@ class AlbumsPageIntl extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
componentWillUnmount() {
|
||||
// Unload data on page change
|
||||
this.props.actions.clearResults();
|
||||
this.props.actions.clearPaginatedResults();
|
||||
}
|
||||
|
||||
render () {
|
||||
render() {
|
||||
const {formatMessage} = this.props.intl;
|
||||
|
||||
const pagination = buildPaginationObject(this.props.location, this.props.currentPage, this.props.nPages, this.props.actions.goToPage);
|
||||
@ -74,12 +74,12 @@ const mapStateToProps = (state) => {
|
||||
error: state.entities.error,
|
||||
albumsList: albumsList,
|
||||
currentPage: state.paginated.currentPage,
|
||||
nPages: state.paginated.nPages
|
||||
nPages: state.paginated.nPages,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
actions: bindActionCreators(actionCreators, dispatch)
|
||||
actions: bindActionCreators(actionCreators, dispatch),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(AlbumsPageIntl));
|
||||
|
@ -25,27 +25,27 @@ const artistMessages = defineMessages(messagesMap(Array.concat([], APIMessages))
|
||||
* Single artist page.
|
||||
*/
|
||||
class ArtistPageIntl extends Component {
|
||||
componentWillMount () {
|
||||
componentWillMount() {
|
||||
// Load the data
|
||||
this.props.actions.loadArtist({
|
||||
filter: this.props.params.id,
|
||||
include: ["albums", "songs"]
|
||||
include: ["albums", "songs"],
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
componentWillUnmount() {
|
||||
this.props.actions.decrementRefCount({
|
||||
"artist": [this.props.artist.get("id")]
|
||||
"artist": [this.props.artist.get("id")],
|
||||
});
|
||||
}
|
||||
|
||||
render () {
|
||||
render() {
|
||||
const {formatMessage} = this.props.intl;
|
||||
|
||||
const error = handleErrorI18nObject(this.props.error, formatMessage, artistMessages);
|
||||
|
||||
return (
|
||||
<Artist playAction={this.props.actions.playTrack} isFetching={this.props.isFetching} error={error} artist={this.props.artist} albums={this.props.albums} songs={this.props.songs} />
|
||||
<Artist playAction={this.props.actions.playSong} isFetching={this.props.isFetching} error={error} artist={this.props.artist} albums={this.props.albums} songs={this.props.songs} />
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -82,12 +82,12 @@ const mapStateToProps = (state, ownProps) => {
|
||||
error: state.entities.error,
|
||||
artist: artist,
|
||||
albums: albums,
|
||||
songs: songs
|
||||
songs: songs,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
actions: bindActionCreators(actionCreators, dispatch)
|
||||
actions: bindActionCreators(actionCreators, dispatch),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(ArtistPageIntl));
|
||||
|
@ -25,13 +25,13 @@ const artistsMessages = defineMessages(messagesMap(Array.concat([], APIMessages)
|
||||
* Grid of artists arts.
|
||||
*/
|
||||
class ArtistsPageIntl extends Component {
|
||||
componentWillMount () {
|
||||
componentWillMount() {
|
||||
// Load the data for the current page
|
||||
const currentPage = parseInt(this.props.location.query.page) || 1;
|
||||
this.props.actions.loadPaginatedArtists({pageNumber: currentPage});
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
componentWillReceiveProps(nextProps) {
|
||||
// Load the data if page has changed
|
||||
const currentPage = parseInt(this.props.location.query.page) || 1;
|
||||
const nextPage = parseInt(nextProps.location.query.page) || 1;
|
||||
@ -40,12 +40,12 @@ class ArtistsPageIntl extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
componentWillUnmount() {
|
||||
// Unload data on page change
|
||||
this.props.actions.clearResults();
|
||||
this.props.actions.clearPaginatedResults();
|
||||
}
|
||||
|
||||
render () {
|
||||
render() {
|
||||
const {formatMessage} = this.props.intl;
|
||||
|
||||
const pagination = buildPaginationObject(this.props.location, this.props.currentPage, this.props.nPages, this.props.actions.goToPage);
|
||||
@ -79,7 +79,7 @@ const mapStateToProps = (state) => {
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
actions: bindActionCreators(actionCreators, dispatch)
|
||||
actions: bindActionCreators(actionCreators, dispatch),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(ArtistsPageIntl));
|
||||
|
@ -9,7 +9,7 @@ import ArtistsPage from "./ArtistsPage";
|
||||
* Browse page is an alias for artists page at the moment.
|
||||
*/
|
||||
export default class BrowsePage extends Component {
|
||||
render () {
|
||||
render() {
|
||||
return (
|
||||
<ArtistsPage {...this.props} />
|
||||
);
|
||||
|
@ -8,7 +8,7 @@ import Discover from "../components/Discover";
|
||||
* Discover page
|
||||
*/
|
||||
export default class DiscoverPage extends Component {
|
||||
render () {
|
||||
render() {
|
||||
return (
|
||||
<Discover />
|
||||
);
|
||||
|
@ -8,7 +8,7 @@ import ArtistsPage from "./ArtistsPage";
|
||||
* Homepage is an alias for Artists page at the moment.
|
||||
*/
|
||||
export default class HomePage extends Component {
|
||||
render () {
|
||||
render() {
|
||||
return (
|
||||
<ArtistsPage {...this.props} />
|
||||
);
|
||||
|
@ -14,7 +14,7 @@ import Login from "../components/Login";
|
||||
* Login page
|
||||
*/
|
||||
export class LoginPage extends Component {
|
||||
constructor (props) {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
// Bind this
|
||||
@ -37,11 +37,11 @@ export class LoginPage extends Component {
|
||||
}
|
||||
return {
|
||||
pathname: redirectPathname,
|
||||
query: redirectQuery
|
||||
query: redirectQuery,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
componentWillMount() {
|
||||
// This checks if the user is already connected or not and redirects
|
||||
// them if it is the case.
|
||||
|
||||
@ -67,14 +67,14 @@ export class LoginPage extends Component {
|
||||
/**
|
||||
* Handle click on submit button.
|
||||
*/
|
||||
handleSubmit (username, password, endpoint, rememberMe) {
|
||||
handleSubmit(username, password, endpoint, rememberMe) {
|
||||
// Get page to redirect to
|
||||
const redirectTo = this._getRedirectTo();
|
||||
// Trigger login action
|
||||
this.props.actions.loginUser(username, password, endpoint, rememberMe, redirectTo);
|
||||
}
|
||||
|
||||
render () {
|
||||
render() {
|
||||
return (
|
||||
<Login onSubmit={this.handleSubmit} username={this.props.username} endpoint={this.props.endpoint} rememberMe={this.props.rememberMe} isAuthenticating={this.props.isAuthenticating} error={this.props.error} info={this.props.info} />
|
||||
);
|
||||
@ -82,7 +82,7 @@ export class LoginPage extends Component {
|
||||
}
|
||||
|
||||
LoginPage.contextTypes = {
|
||||
router: PropTypes.object.isRequired
|
||||
router: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
@ -93,11 +93,11 @@ const mapStateToProps = (state) => ({
|
||||
isAuthenticated: state.auth.isAuthenticated,
|
||||
token: state.auth.token,
|
||||
error: state.auth.error,
|
||||
info: state.auth.info
|
||||
info: state.auth.info,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
actions: bindActionCreators(actionCreators, dispatch)
|
||||
actions: bindActionCreators(actionCreators, dispatch),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(LoginPage);
|
||||
|
@ -11,12 +11,12 @@ import * as actionCreators from "../actions";
|
||||
* Logout page
|
||||
*/
|
||||
export class LogoutPage extends Component {
|
||||
componentWillMount () {
|
||||
componentWillMount() {
|
||||
// Logout when component is mounted
|
||||
this.props.actions.logoutAndRedirect();
|
||||
}
|
||||
|
||||
render () {
|
||||
render() {
|
||||
return (
|
||||
<div></div>
|
||||
);
|
||||
@ -24,7 +24,7 @@ export class LogoutPage extends Component {
|
||||
}
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
actions: bindActionCreators(actionCreators, dispatch)
|
||||
actions: bindActionCreators(actionCreators, dispatch),
|
||||
});
|
||||
|
||||
export default connect(null, mapDispatchToProps)(LogoutPage);
|
||||
|
@ -25,13 +25,13 @@ const songsMessages = defineMessages(messagesMap(Array.concat([], APIMessages)))
|
||||
* Paginated table of available songs
|
||||
*/
|
||||
class SongsPageIntl extends Component {
|
||||
componentWillMount () {
|
||||
componentWillMount() {
|
||||
// Load the data for current page
|
||||
const currentPage = parseInt(this.props.location.query.page) || 1;
|
||||
this.props.actions.loadPaginatedSongs({pageNumber: currentPage});
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
componentWillReceiveProps(nextProps) {
|
||||
// Load the data if page has changed
|
||||
const currentPage = parseInt(this.props.location.query.page) || 1;
|
||||
const nextPage = parseInt(nextProps.location.query.page) || 1;
|
||||
@ -40,12 +40,12 @@ class SongsPageIntl extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
componentWillUnmount() {
|
||||
// Unload data on page change
|
||||
this.props.actions.clearResults();
|
||||
this.props.actions.clearPaginatedResults();
|
||||
}
|
||||
|
||||
render () {
|
||||
render() {
|
||||
const {formatMessage} = this.props.intl;
|
||||
|
||||
const pagination = buildPaginationObject(this.props.location, this.props.currentPage, this.props.nPages, this.props.actions.goToPage);
|
||||
@ -53,7 +53,7 @@ class SongsPageIntl extends Component {
|
||||
const error = handleErrorI18nObject(this.props.error, formatMessage, songsMessages);
|
||||
|
||||
return (
|
||||
<Songs playAction={this.props.actions.playTrack} isFetching={this.props.isFetching} error={error} songs={this.props.songsList} pagination={pagination} />
|
||||
<Songs playAction={this.props.actions.playSong} isFetching={this.props.isFetching} error={error} songs={this.props.songsList} pagination={pagination} />
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -82,12 +82,12 @@ const mapStateToProps = (state) => {
|
||||
error: state.entities.error,
|
||||
songsList: songsList,
|
||||
currentPage: state.paginated.currentPage,
|
||||
nPages: state.paginated.nPages
|
||||
nPages: state.paginated.nPages,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
actions: bindActionCreators(actionCreators, dispatch)
|
||||
actions: bindActionCreators(actionCreators, dispatch),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(SongsPageIntl));
|
||||
|
@ -1,112 +1,146 @@
|
||||
// TODO: This file is not finished
|
||||
// NPM imports
|
||||
import React, { Component } from "react";
|
||||
import { bindActionCreators } from "redux";
|
||||
import { connect } from "react-redux";
|
||||
import { Howl } from "howler";
|
||||
import Immutable from "immutable";
|
||||
|
||||
// Actions
|
||||
import * as actionCreators from "../actions";
|
||||
|
||||
// Components
|
||||
import WebPlayerComponent from "../components/elements/WebPlayer";
|
||||
|
||||
|
||||
/**
|
||||
* Webplayer container.
|
||||
*/
|
||||
class WebPlayer extends Component {
|
||||
constructor (props) {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.play = this.play.bind(this);
|
||||
|
||||
// Data attributes
|
||||
this.howl = null;
|
||||
|
||||
// Bind this
|
||||
this.startPlaying = this.startPlaying.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this.play(this.props.isPlaying);
|
||||
componentDidMount() {
|
||||
// Start playback upon component mount
|
||||
this.startPlaying(this.props);
|
||||
}
|
||||
|
||||
componentWillUpdate (nextProps) {
|
||||
componentWillUpdate(nextProps) {
|
||||
// Handle stop
|
||||
if (!nextProps.currentSong || nextProps.playlist.size < 1) {
|
||||
if (this.howl) {
|
||||
this.howl.stop();
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle play / pause
|
||||
if (nextProps.isPlaying != this.props.isPlaying) {
|
||||
// This check ensure we do not start multiple times the same music.
|
||||
this.play(nextProps);
|
||||
// This check ensure we do not start playing multiple times the
|
||||
// same song
|
||||
this.startPlaying(nextProps);
|
||||
}
|
||||
|
||||
// Toggle mute / unmute
|
||||
// If something is playing back
|
||||
if (this.howl) {
|
||||
// Set mute / unmute
|
||||
this.howl.mute(nextProps.isMute);
|
||||
// Set volume
|
||||
this.howl.volume(nextProps.volume / 100);
|
||||
}
|
||||
}
|
||||
|
||||
getCurrentTrackPath (props) {
|
||||
return [
|
||||
"tracks",
|
||||
props.playlist.get(props.currentIndex)
|
||||
];
|
||||
}
|
||||
|
||||
play (props) {
|
||||
if (props.isPlaying) {
|
||||
/**
|
||||
* Handle playback through Howler and Web Audio API.
|
||||
*
|
||||
* @params props A set of props to use for setting play parameters.
|
||||
*/
|
||||
startPlaying(props) {
|
||||
if (props.isPlaying && props.currentSong) {
|
||||
// If it should be playing any song
|
||||
if (!this.howl) {
|
||||
const url = props.entities.getIn(
|
||||
Array.concat([], this.getCurrentTrackPath(props), ["url"])
|
||||
);
|
||||
// Build a new Howler object with current song to play
|
||||
const url = props.currentSong.get("url");
|
||||
if (!url) {
|
||||
// TODO: Error handling
|
||||
console.error("URL not found.");
|
||||
return;
|
||||
}
|
||||
this.howl = new Howl({
|
||||
src: [url],
|
||||
html5: true,
|
||||
loop: false,
|
||||
html5: true, // Use HTML5 by default to allow streaming
|
||||
mute: props.isMute,
|
||||
autoplay: false,
|
||||
volume: props.volume / 100, // Set current volume
|
||||
autoplay: false, // No autoplay, we handle it manually
|
||||
});
|
||||
} else {
|
||||
// Else, something is playing
|
||||
// TODO If it is not the expected song, change it
|
||||
}
|
||||
// Start playing
|
||||
this.howl.play();
|
||||
}
|
||||
else {
|
||||
// If it should not be playing
|
||||
if (this.howl) {
|
||||
// Pause any running music
|
||||
this.howl.pause();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render () {
|
||||
const currentTrack = this.props.entities.getIn(this.getCurrentTrackPath(this.props));
|
||||
let currentArtist = new Immutable.Map();
|
||||
if (currentTrack) {
|
||||
currentArtist = this.props.entities.getIn(["artists", currentTrack.get("artist")]);
|
||||
}
|
||||
|
||||
render() {
|
||||
const webplayerProps = {
|
||||
isPlaying: this.props.isPlaying,
|
||||
isRandom: this.props.isRandom,
|
||||
isRepeat: this.props.isRepeat,
|
||||
isMute: this.props.isMute,
|
||||
currentTrack: currentTrack,
|
||||
currentArtist: currentArtist,
|
||||
volume: this.props.volume,
|
||||
currentIndex: this.props.currentIndex,
|
||||
playlist: this.props.playlist,
|
||||
currentSong: this.props.currentSong,
|
||||
currentArtist: this.props.currentArtist,
|
||||
// Use a lambda to ensure no first argument is passed to
|
||||
// togglePlaying
|
||||
onPlayPause: (() => this.props.actions.togglePlaying()),
|
||||
onPrev: this.props.actions.playPrevious,
|
||||
onSkip: this.props.actions.playNext,
|
||||
onRandom: this.props.actions.toggleRandom,
|
||||
onRepeat: this.props.actions.toggleRepeat,
|
||||
onMute: this.props.actions.toggleMute
|
||||
onMute: this.props.actions.toggleMute,
|
||||
};
|
||||
return (
|
||||
<WebPlayerComponent {...webplayerProps} />
|
||||
);
|
||||
}
|
||||
}
|
||||
const mapStateToProps = (state) => {
|
||||
const currentIndex = state.webplayer.currentIndex;
|
||||
const playlist = state.webplayer.playlist;
|
||||
|
||||
const mapStateToProps = (state) => ({
|
||||
isPlaying: state.webplayer.isPlaying,
|
||||
isRandom: state.webplayer.isRandom,
|
||||
isRepeat: state.webplayer.isRepeat,
|
||||
isMute: state.webplayer.isMute,
|
||||
currentIndex: state.webplayer.currentIndex,
|
||||
playlist: state.webplayer.playlist
|
||||
});
|
||||
|
||||
// Get current song and artist from entities store
|
||||
const currentSong = state.entities.getIn(["entities", "song", playlist.get(currentIndex)]);
|
||||
let currentArtist = undefined;
|
||||
if (currentSong) {
|
||||
currentArtist = state.entities.getIn(["entities", "artist", currentSong.get("artist")]);
|
||||
}
|
||||
return {
|
||||
isPlaying: state.webplayer.isPlaying,
|
||||
isRandom: state.webplayer.isRandom,
|
||||
isRepeat: state.webplayer.isRepeat,
|
||||
isMute: state.webplayer.isMute,
|
||||
volume: state.webplayer.volume,
|
||||
currentIndex: currentIndex,
|
||||
playlist: playlist,
|
||||
currentSong: currentSong,
|
||||
currentArtist: currentArtist,
|
||||
};
|
||||
};
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
actions: bindActionCreators(actionCreators, dispatch)
|
||||
actions: bindActionCreators(actionCreators, dispatch),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(WebPlayer);
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,2 +1,2 @@
|
||||
!function(e){function t(r){if(n[r])return n[r].exports;var a=n[r]={exports:{},id:r,loaded:!1};return e[r].call(a.exports,a,a.exports,t),a.loaded=!0,a.exports}var n={};return t.m=e,t.c=n,t.p="./",t(0)}({0:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n(629);Object.keys(r).forEach(function(e){"default"!==e&&"__esModule"!==e&&Object.defineProperty(t,e,{enumerable:!0,get:function(){return r[e]}})})},629:function(e,t){!function(t,n){function r(e,t){var n=e.createElement("p"),r=e.getElementsByTagName("head")[0]||e.documentElement;return n.innerHTML="x<style>"+t+"</style>",r.insertBefore(n.lastChild,r.firstChild)}function a(){var e=b.elements;return"string"==typeof e?e.split(" "):e}function o(e,t){var n=b.elements;"string"!=typeof n&&(n=n.join(" ")),"string"!=typeof e&&(e=e.join(" ")),b.elements=n+" "+e,s(t)}function c(e){var t=E[e[v]];return t||(t={},y++,e[v]=y,E[y]=t),t}function i(e,t,r){if(t||(t=n),f)return t.createElement(e);r||(r=c(t));var a;return a=r.cache[e]?r.cache[e].cloneNode():g.test(e)?(r.cache[e]=r.createElem(e)).cloneNode():r.createElem(e),!a.canHaveChildren||p.test(e)||a.tagUrn?a:r.frag.appendChild(a)}function l(e,t){if(e||(e=n),f)return e.createDocumentFragment();t=t||c(e);for(var r=t.frag.cloneNode(),o=0,i=a(),l=i.length;o<l;o++)r.createElement(i[o]);return r}function u(e,t){t.cache||(t.cache={},t.createElem=e.createElement,t.createFrag=e.createDocumentFragment,t.frag=t.createFrag()),e.createElement=function(n){return b.shivMethods?i(n,e,t):t.createElem(n)},e.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+a().join().replace(/[\w\-:]+/g,function(e){return t.createElem(e),t.frag.createElement(e),'c("'+e+'")'})+");return n}")(b,t.frag)}function s(e){e||(e=n);var t=c(e);return!b.shivCSS||d||t.hasCSS||(t.hasCSS=!!r(e,"article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}mark{background:#FF0;color:#000}template{display:none}")),f||u(e,t),e}var d,f,m="3.7.3-pre",h=t.html5||{},p=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,g=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,v="_html5shiv",y=0,E={};!function(){try{var e=n.createElement("a");e.innerHTML="<xyz></xyz>",d="hidden"in e,f=1==e.childNodes.length||function(){n.createElement("a");var e=n.createDocumentFragment();return"undefined"==typeof e.cloneNode||"undefined"==typeof e.createDocumentFragment||"undefined"==typeof e.createElement}()}catch(t){d=!0,f=!0}}();var b={elements:h.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output picture progress section summary template time video",version:m,shivCSS:h.shivCSS!==!1,supportsUnknownElements:f,shivMethods:h.shivMethods!==!1,type:"default",shivDocument:s,createElement:i,createDocumentFragment:l,addElements:o};t.html5=b,s(n),"object"==typeof e&&e.exports&&(e.exports=b)}("undefined"!=typeof window?window:this,document)}});
|
||||
!function(e){function t(r){if(n[r])return n[r].exports;var a=n[r]={exports:{},id:r,loaded:!1};return e[r].call(a.exports,a,a.exports,t),a.loaded=!0,a.exports}var n={};return t.m=e,t.c=n,t.p="./",t(0)}({0:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n(634);Object.keys(r).forEach(function(e){"default"!==e&&"__esModule"!==e&&Object.defineProperty(t,e,{enumerable:!0,get:function(){return r[e]}})})},634:function(e,t){!function(t,n){function r(e,t){var n=e.createElement("p"),r=e.getElementsByTagName("head")[0]||e.documentElement;return n.innerHTML="x<style>"+t+"</style>",r.insertBefore(n.lastChild,r.firstChild)}function a(){var e=b.elements;return"string"==typeof e?e.split(" "):e}function o(e,t){var n=b.elements;"string"!=typeof n&&(n=n.join(" ")),"string"!=typeof e&&(e=e.join(" ")),b.elements=n+" "+e,s(t)}function c(e){var t=E[e[v]];return t||(t={},y++,e[v]=y,E[y]=t),t}function i(e,t,r){if(t||(t=n),f)return t.createElement(e);r||(r=c(t));var a;return a=r.cache[e]?r.cache[e].cloneNode():g.test(e)?(r.cache[e]=r.createElem(e)).cloneNode():r.createElem(e),!a.canHaveChildren||p.test(e)||a.tagUrn?a:r.frag.appendChild(a)}function l(e,t){if(e||(e=n),f)return e.createDocumentFragment();t=t||c(e);for(var r=t.frag.cloneNode(),o=0,i=a(),l=i.length;o<l;o++)r.createElement(i[o]);return r}function u(e,t){t.cache||(t.cache={},t.createElem=e.createElement,t.createFrag=e.createDocumentFragment,t.frag=t.createFrag()),e.createElement=function(n){return b.shivMethods?i(n,e,t):t.createElem(n)},e.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+a().join().replace(/[\w\-:]+/g,function(e){return t.createElem(e),t.frag.createElement(e),'c("'+e+'")'})+");return n}")(b,t.frag)}function s(e){e||(e=n);var t=c(e);return!b.shivCSS||d||t.hasCSS||(t.hasCSS=!!r(e,"article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}mark{background:#FF0;color:#000}template{display:none}")),f||u(e,t),e}var d,f,m="3.7.3-pre",h=t.html5||{},p=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,g=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,v="_html5shiv",y=0,E={};!function(){try{var e=n.createElement("a");e.innerHTML="<xyz></xyz>",d="hidden"in e,f=1==e.childNodes.length||function(){n.createElement("a");var e=n.createDocumentFragment();return"undefined"==typeof e.cloneNode||"undefined"==typeof e.createDocumentFragment||"undefined"==typeof e.createElement}()}catch(t){d=!0,f=!0}}();var b={elements:h.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output picture progress section summary template time video",version:m,shivCSS:h.shivCSS!==!1,supportsUnknownElements:f,shivMethods:h.shivMethods!==!1,type:"default",shivDocument:s,createElement:i,createDocumentFragment:l,addElements:o};t.html5=b,s(n),"object"==typeof e&&e.exports&&(e.exports=b)}("undefined"!=typeof window?window:this,document)}});
|
||||
//# sourceMappingURL=fix.ie9.js.map
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user