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>
|
||||