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,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -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")]);
|
||||
// Handle reference counting
|
||||
dispatch(decrementRefCount({
|
||||
song: getState().webplayer.get("playlist").toArray(),
|
||||
}));
|
||||
// Stop playback
|
||||
dispatch ({
|
||||
type: PUSH_PLAYLIST,
|
||||
payload: {
|
||||
playlist: [trackID],
|
||||
tracks: [
|
||||
[trackID, track]
|
||||
],
|
||||
albums: [
|
||||
[album.get("id"), album]
|
||||
],
|
||||
artists: [
|
||||
[artist.get("id"), artist]
|
||||
]
|
||||
}
|
||||
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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -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));
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
@ -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 (
|
||||
|
@ -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);
|
||||
|
@ -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 (
|
||||
|
@ -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);
|
||||
|
@ -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));
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -147,7 +147,7 @@ export default class FilterablePaginatedSongsTable extends Component {
|
||||
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
|
||||
@ -162,7 +162,7 @@ export default class FilterablePaginatedSongsTable extends Component {
|
||||
*/
|
||||
handleUserInput(filterText) {
|
||||
this.setState({
|
||||
filterText: filterText
|
||||
filterText: filterText,
|
||||
});
|
||||
}
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
@ -27,5 +27,5 @@ export default class DismissibleAlert extends Component {
|
||||
}
|
||||
DismissibleAlert.propTypes = {
|
||||
type: PropTypes.string,
|
||||
text: PropTypes.string
|
||||
text: PropTypes.string,
|
||||
};
|
||||
|
@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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();
|
||||
@ -264,7 +264,7 @@ Grid.propTypes = {
|
||||
itemsLabel: PropTypes.string.isRequired,
|
||||
subItemsType: PropTypes.string.isRequired,
|
||||
subItemsLabel: PropTypes.string.isRequired,
|
||||
filterText: PropTypes.string
|
||||
filterText: PropTypes.string,
|
||||
};
|
||||
|
||||
|
||||
@ -276,7 +276,7 @@ export default class FilterablePaginatedGrid extends Component {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
filterText: "" // No filterText at init
|
||||
filterText: "", // No filterText at init
|
||||
};
|
||||
|
||||
// Bind this
|
||||
@ -292,7 +292,7 @@ export default class FilterablePaginatedGrid extends Component {
|
||||
*/
|
||||
handleUserInput(filterText) {
|
||||
this.setState({
|
||||
filterText: filterText
|
||||
filterText: filterText,
|
||||
});
|
||||
}
|
||||
|
||||
@ -309,5 +309,5 @@ export default class FilterablePaginatedGrid extends Component {
|
||||
|
||||
FilterablePaginatedGrid.propTypes = {
|
||||
grid: PropTypes.object.isRequired,
|
||||
pagination: PropTypes.object.isRequired
|
||||
pagination: PropTypes.object.isRequired,
|
||||
};
|
||||
|
@ -1,31 +1,49 @@
|
||||
// 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) {
|
||||
super(props);
|
||||
|
||||
// Bind this
|
||||
this.artOpacityHandler = this.artOpacityHandler.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle opacity on album art.
|
||||
*
|
||||
* Set opacity on mouseover / mouseout.
|
||||
*
|
||||
* @param ev A JS event.
|
||||
*/
|
||||
artOpacityHandler(ev) {
|
||||
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";
|
||||
}
|
||||
@ -34,14 +52,15 @@ class WebPlayerCSSIntl extends Component {
|
||||
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";
|
||||
@ -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));
|
||||
|
@ -30,8 +30,8 @@ export class RequireAuthentication extends Component {
|
||||
pathname: "/login",
|
||||
state: {
|
||||
nextPathname: this.props.location.pathname,
|
||||
nextQuery: this.props.location.query
|
||||
}
|
||||
nextQuery: this.props.location.query,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -50,15 +50,15 @@ export class RequireAuthentication extends Component {
|
||||
|
||||
RequireAuthentication.propTypes = {
|
||||
// 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;
|
||||
|
@ -47,14 +47,14 @@ function _checkHTTPStatus (response) {
|
||||
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: {},
|
||||
}));
|
||||
}
|
||||
|
||||
@ -72,7 +72,7 @@ function _checkAPIErrors (jsonData) {
|
||||
// No data returned
|
||||
return Promise.reject(new i18nRecord({
|
||||
id: "app.api.emptyResponse",
|
||||
values: {}
|
||||
values: {},
|
||||
}));
|
||||
}
|
||||
return jsonData;
|
||||
@ -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();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
@ -27,6 +27,6 @@ export function buildHMAC (password) {
|
||||
|
||||
return {
|
||||
time: time,
|
||||
passphrase: shaObj.getHash("HEX")
|
||||
passphrase: shaObj.getHash("HEX"),
|
||||
};
|
||||
}
|
||||
|
@ -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 <
|
||||
};
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ export class AlbumPage extends Component {
|
||||
this.props.actions.loadAlbums({
|
||||
pageNumber: 1,
|
||||
filter: this.props.params.id,
|
||||
include: ["songs"]
|
||||
include: ["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);
|
||||
|
@ -42,7 +42,7 @@ class AlbumsPageIntl extends Component {
|
||||
|
||||
componentWillUnmount() {
|
||||
// Unload data on page change
|
||||
this.props.actions.clearResults();
|
||||
this.props.actions.clearPaginatedResults();
|
||||
}
|
||||
|
||||
render() {
|
||||
@ -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));
|
||||
|
@ -29,13 +29,13 @@ class ArtistPageIntl extends Component {
|
||||
// Load the data
|
||||
this.props.actions.loadArtist({
|
||||
filter: this.props.params.id,
|
||||
include: ["albums", "songs"]
|
||||
include: ["albums", "songs"],
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.actions.decrementRefCount({
|
||||
"artist": [this.props.artist.get("id")]
|
||||
"artist": [this.props.artist.get("id")],
|
||||
});
|
||||
}
|
||||
|
||||
@ -45,7 +45,7 @@ class ArtistPageIntl extends Component {
|
||||
const error = handleErrorI18nObject(this.props.error, formatMessage, artistMessages);
|
||||
|
||||
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));
|
||||
|
@ -42,7 +42,7 @@ class ArtistsPageIntl extends Component {
|
||||
|
||||
componentWillUnmount() {
|
||||
// Unload data on page change
|
||||
this.props.actions.clearResults();
|
||||
this.props.actions.clearPaginatedResults();
|
||||
}
|
||||
|
||||
render() {
|
||||
@ -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));
|
||||
|
@ -37,7 +37,7 @@ export class LoginPage extends Component {
|
||||
}
|
||||
return {
|
||||
pathname: redirectPathname,
|
||||
query: redirectQuery
|
||||
query: redirectQuery,
|
||||
};
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -42,7 +42,7 @@ class SongsPageIntl extends Component {
|
||||
|
||||
componentWillUnmount() {
|
||||
// Unload data on page change
|
||||
this.props.actions.clearResults();
|
||||
this.props.actions.clearPaginatedResults();
|
||||
}
|
||||
|
||||
render() {
|
||||
@ -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) {
|
||||
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);
|
||||
// Start playback upon component mount
|
||||
this.startPlaying(this.props);
|
||||
}
|
||||
|
||||
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")]);
|
||||
}
|
||||
|
||||
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) => ({
|
||||
// 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,
|
||||
currentIndex: state.webplayer.currentIndex,
|
||||
playlist: state.webplayer.playlist
|
||||
});
|
||||
|
||||
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