Rework webplayer

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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