Finish API refactor, use normalizr + immutable

This commit is contained in:
Lucas Verney 2016-08-05 00:00:25 +02:00
parent 288039e732
commit b73b4ba200
43 changed files with 523 additions and 561 deletions

6
TODO
View File

@ -1,9 +1,7 @@
4. Refactor API * PropTypes.object
5. Web player 5. Web player
6. Homepage 6. Homepage
7. Settings 7. Settings
8. Search 8. Search
9. Discover 9. Discover
# API middleware
* Immutable.js : entities in API

View File

@ -1,17 +1,42 @@
import { normalize, arrayOf } from "normalizr";
import humps from "humps"; import humps from "humps";
import { CALL_API } from "../middleware/api"; import { CALL_API } from "../middleware/api";
import { artist, track, album } from "../models/api";
export const DEFAULT_LIMIT = 30; /** Default max number of elements to retrieve. */ export const DEFAULT_LIMIT = 30; /** Default max number of elements to retrieve. */
export default function (action, requestType, successType, failureType) { export default function (action, requestType, successType, failureType) {
const itemName = action.rstrip("s"); const itemName = action.rstrip("s");
const fetchItemsSuccess = function (itemsList, itemsCount, pageNumber) {
const nPages = Math.ceil(itemsCount / DEFAULT_LIMIT); const fetchItemsSuccess = function (jsonData, pageNumber) {
// Normalize data
jsonData = normalize(
jsonData,
{
artist: arrayOf(artist),
album: arrayOf(album),
song: arrayOf(track)
},
{
assignEntity: function (output, key, value) {
// Delete useless fields
if (key == "sessionExpire") {
delete output.sessionExpire;
} else {
output[key] = value;
}
}
}
);
const nPages = Math.ceil(jsonData.result[itemName].length / DEFAULT_LIMIT);
return { return {
type: successType, type: successType,
payload: { payload: {
items: itemsList, result: jsonData.result,
entities: jsonData.entities,
nPages: nPages, nPages: nPages,
currentPage: pageNumber currentPage: pageNumber
} }
@ -34,7 +59,7 @@ export default function (action, requestType, successType, failureType) {
}; };
const fetchItems = function (endpoint, username, passphrase, filter, pageNumber, include = [], limit=DEFAULT_LIMIT) { const fetchItems = function (endpoint, username, passphrase, filter, pageNumber, include = [], limit=DEFAULT_LIMIT) {
const offset = (pageNumber - 1) * DEFAULT_LIMIT; const offset = (pageNumber - 1) * DEFAULT_LIMIT;
var extraParams = { let extraParams = {
offset: offset, offset: offset,
limit: limit limit: limit
}; };
@ -51,7 +76,7 @@ export default function (action, requestType, successType, failureType) {
dispatch: [ dispatch: [
fetchItemsRequest, fetchItemsRequest,
jsonData => dispatch => { jsonData => dispatch => {
dispatch(fetchItemsSuccess(jsonData[itemName], jsonData.totalCount, pageNumber)); dispatch(fetchItemsSuccess(jsonData, pageNumber));
}, },
fetchItemsFailure fetchItemsFailure
], ],

View File

@ -27,7 +27,7 @@ function _buildHMAC (password) {
// Handle Ampache HMAC generation // Handle Ampache HMAC generation
const time = Math.floor(Date.now() / 1000); const time = Math.floor(Date.now() / 1000);
var shaObj = new jsSHA("SHA-256", "TEXT"); let shaObj = new jsSHA("SHA-256", "TEXT");
shaObj.update(password); shaObj.update(password);
const key = shaObj.getHash("HEX"); const key = shaObj.getHash("HEX");
@ -120,8 +120,8 @@ export function logoutAndRedirect() {
export function loginUser(username, passwordOrToken, endpoint, rememberMe, redirect="/", isToken=false) { export function loginUser(username, passwordOrToken, endpoint, rememberMe, redirect="/", isToken=false) {
endpoint = _cleanEndpoint(endpoint); endpoint = _cleanEndpoint(endpoint);
var time = 0; let time = 0;
var passphrase = passwordOrToken; let passphrase = passwordOrToken;
if (!isToken) { if (!isToken) {
// Standard password connection // Standard password connection

View File

@ -2,20 +2,12 @@ export * from "./auth";
import APIAction from "./APIActions"; import APIAction from "./APIActions";
export const ARTISTS_SUCCESS = "ARTISTS_SUCCESS"; export const API_SUCCESS = "API_SUCCESS";
export const ARTISTS_REQUEST = "ARTISTS_REQUEST"; export const API_REQUEST = "API_REQUEST";
export const ARTISTS_FAILURE = "ARTISTS_FAILURE"; export const API_FAILURE = "API_FAILURE";
export var { loadArtists } = APIAction("artists", ARTISTS_REQUEST, ARTISTS_SUCCESS, ARTISTS_FAILURE); export var { loadArtists } = APIAction("artists", API_REQUEST, API_SUCCESS, API_FAILURE);
export var { loadAlbums } = APIAction("albums", API_REQUEST, API_SUCCESS, API_FAILURE);
export const ALBUMS_SUCCESS = "ALBUMS_SUCCESS"; export var { loadSongs } = APIAction("songs", API_REQUEST, API_SUCCESS, API_FAILURE);
export const ALBUMS_REQUEST = "ALBUMS_REQUEST";
export const ALBUMS_FAILURE = "ALBUMS_FAILURE";
export var { loadAlbums } = APIAction("albums", ALBUMS_REQUEST, ALBUMS_SUCCESS, ALBUMS_FAILURE);
export const SONGS_SUCCESS = "SONGS_SUCCESS";
export const SONGS_REQUEST = "SONGS_REQUEST";
export const SONGS_FAILURE = "SONGS_FAILURE";
export var { loadSongs } = APIAction("songs", SONGS_REQUEST, SONGS_SUCCESS, SONGS_FAILURE);
export * from "./paginate"; export * from "./paginate";
export * from "./store"; export * from "./store";

View File

@ -13,7 +13,7 @@ const albumMessages = defineMessages(messagesMap(commonMessages));
class AlbumTrackRowCSS extends Component { class AlbumTrackRowCSS extends Component {
render () { render () {
const length = formatLength(this.props.track.time); const length = formatLength(this.props.track.get("time"));
return ( return (
<tr> <tr>
<td> <td>
@ -24,14 +24,15 @@ class AlbumTrackRowCSS extends Component {
<FontAwesome name="play-circle-o" aria-hidden="true" /> <FontAwesome name="play-circle-o" aria-hidden="true" />
</button> </button>
</td> </td>
<td>{this.props.track.track}</td> <td>{this.props.track.get("track")}</td>
<td>{this.props.track.name}</td> <td>{this.props.track.get("name")}</td>
<td>{length}</td> <td>{length}</td>
</tr> </tr>
); );
} }
} }
// TODO: Not object
AlbumTrackRowCSS.propTypes = { AlbumTrackRowCSS.propTypes = {
track: PropTypes.object.isRequired track: PropTypes.object.isRequired
}; };
@ -41,9 +42,9 @@ export let AlbumTrackRow = CSSModules(AlbumTrackRowCSS, css);
class AlbumTracksTableCSS extends Component { class AlbumTracksTableCSS extends Component {
render () { render () {
var rows = []; let rows = [];
this.props.tracks.forEach(function (item) { this.props.tracks.forEach(function (item) {
rows.push(<AlbumTrackRow track={item} key={item.id} />); rows.push(<AlbumTrackRow track={item} key={item.get("id")} />);
}); });
return ( return (
<table className="table table-hover" styleName="songs"> <table className="table table-hover" styleName="songs">
@ -55,8 +56,9 @@ class AlbumTracksTableCSS extends Component {
} }
} }
// TODO: Not object
AlbumTracksTableCSS.propTypes = { AlbumTracksTableCSS.propTypes = {
tracks: PropTypes.array.isRequired tracks: PropTypes.object.isRequired
}; };
export let AlbumTracksTable = CSSModules(AlbumTracksTableCSS, css); export let AlbumTracksTable = CSSModules(AlbumTracksTableCSS, css);
@ -66,15 +68,15 @@ class AlbumRowCSS extends Component {
return ( return (
<div className="row" styleName="row"> <div className="row" styleName="row">
<div className="col-sm-offset-2 col-xs-9 col-sm-10" styleName="nameRow"> <div className="col-sm-offset-2 col-xs-9 col-sm-10" styleName="nameRow">
<h2>{this.props.album.name}</h2> <h2>{this.props.album.get("name")}</h2>
</div> </div>
<div className="col-xs-3 col-sm-2" styleName="artRow"> <div className="col-xs-3 col-sm-2" styleName="artRow">
<p className="text-center"><img src={this.props.album.art} width="200" height="200" className="img-responsive img-circle" styleName="art" alt={this.props.album.name} /></p> <p className="text-center"><img src={this.props.album.get("art")} width="200" height="200" className="img-responsive img-circle" styleName="art" alt={this.props.album.get("name")} /></p>
</div> </div>
<div className="col-xs-9 col-sm-10 table-responsive"> <div className="col-xs-9 col-sm-10 table-responsive">
{ {
Array.isArray(this.props.album.tracks) ? this.props.songs.size > 0 ?
<AlbumTracksTable tracks={this.props.album.tracks} /> : <AlbumTracksTable tracks={this.props.songs} /> :
null null
} }
</div> </div>
@ -83,8 +85,10 @@ class AlbumRowCSS extends Component {
} }
} }
// TODO: Not object
AlbumRowCSS.propTypes = { AlbumRowCSS.propTypes = {
album: PropTypes.object.isRequired album: PropTypes.object.isRequired,
songs: PropTypes.object.isRequired
}; };
export let AlbumRow = CSSModules(AlbumRowCSS, css); export let AlbumRow = CSSModules(AlbumRowCSS, css);
@ -92,11 +96,13 @@ export let AlbumRow = CSSModules(AlbumRowCSS, css);
export default class Album extends Component { export default class Album extends Component {
render () { render () {
return ( return (
<AlbumRow album={this.props.album} /> <AlbumRow album={this.props.album} songs={this.props.songs} />
); );
} }
} }
// TODO: Not object
Album.propTypes = { Album.propTypes = {
album: PropTypes.object.isRequired album: PropTypes.object.isRequired,
songs: PropTypes.object.isRequired
}; };

View File

@ -7,26 +7,32 @@ import css from "../styles/Artist.scss";
class ArtistCSS extends Component { class ArtistCSS extends Component {
render () { render () {
var albumsRows = []; let albumsRows = [];
if (Array.isArray(this.props.artist.albums)) { if (this.props.artist.get("albums").size > 0) {
this.props.artist.albums.forEach(function (item) { const artistAlbums = this.props.albums;
albumsRows.push(<AlbumRow album={item} key={item.id} />); const artistSongs = this.props.songs;
this.props.artist.get("albums").forEach(function (album) {
album = artistAlbums.get(album);
const songs = album.get("tracks").map(
id => artistSongs.get(id)
);
albumsRows.push(<AlbumRow album={album} songs={songs} key={album.get("id")} />);
}); });
} }
return ( return (
<div> <div>
<div className="row" styleName="name"> <div className="row" styleName="name">
<div className="col-sm-12"> <div className="col-sm-12">
<h1>{this.props.artist.name}</h1> <h1>{this.props.artist.get("name")}</h1>
<hr/> <hr/>
</div> </div>
</div> </div>
<div className="row"> <div className="row">
<div className="col-sm-9"> <div className="col-sm-9">
<p>{this.props.artist.summary}</p> <p>{this.props.artist.get("summary")}</p>
</div> </div>
<div className="col-sm-3 text-center"> <div className="col-sm-3 text-center">
<p><img src={this.props.artist.art} width="200" height="200" className="img-responsive img-circle" styleName="art" alt={this.props.artist.name}/></p> <p><img src={this.props.artist.get("art")} width="200" height="200" className="img-responsive img-circle" styleName="art" alt={this.props.artist.get("name")}/></p>
</div> </div>
</div> </div>
{ albumsRows } { albumsRows }
@ -35,8 +41,11 @@ class ArtistCSS extends Component {
} }
} }
// TODO: Not object
ArtistCSS.propTypes = { ArtistCSS.propTypes = {
artist: PropTypes.object.isRequired artist: PropTypes.object.isRequired,
albums: PropTypes.object.isRequired,
songs: PropTypes.object.isRequired
}; };
export default CSSModules(ArtistCSS, css); export default CSSModules(ArtistCSS, css);

View File

@ -41,7 +41,7 @@ class LoginFormCSSIntl extends Component {
const endpoint = this.refs.endpoint.value.trim(); const endpoint = this.refs.endpoint.value.trim();
const rememberMe = this.refs.rememberMe.checked; const rememberMe = this.refs.rememberMe.checked;
var hasError = this.setError(this.refs.usernameFormGroup, !username); let hasError = this.setError(this.refs.usernameFormGroup, !username);
hasError |= this.setError(this.refs.passwordFormGroup, !password); hasError |= this.setError(this.refs.passwordFormGroup, !password);
hasError |= this.setError(this.refs.endpointFormGroup, !endpoint); hasError |= this.setError(this.refs.endpointFormGroup, !endpoint);
@ -61,13 +61,13 @@ class LoginFormCSSIntl extends Component {
render () { render () {
const {formatMessage} = this.props.intl; const {formatMessage} = this.props.intl;
var infoMessage = this.props.info; let infoMessage = this.props.info;
if (this.props.info && this.props.info instanceof i18nRecord) { if (this.props.info && this.props.info instanceof i18nRecord) {
infoMessage = ( infoMessage = (
<FormattedMessage {...loginMessages[this.props.info.id]} values={ this.props.info.values} /> <FormattedMessage {...loginMessages[this.props.info.id]} values={ this.props.info.values} />
); );
} }
var errorMessage = this.props.error; let errorMessage = this.props.error;
if (this.props.error && this.props.error instanceof i18nRecord) { if (this.props.error && this.props.error instanceof i18nRecord) {
errorMessage = ( errorMessage = (
<FormattedMessage {...loginMessages[this.props.error.id]} values={ this.props.error.values} /> <FormattedMessage {...loginMessages[this.props.error.id]} values={ this.props.error.values} />

View File

@ -19,9 +19,9 @@ const songsMessages = defineMessages(messagesMap(Array.concat([], commonMessages
class SongsTableRowCSS extends Component { class SongsTableRowCSS extends Component {
render () { render () {
const length = formatLength(this.props.song.time); const length = formatLength(this.props.song.get("time"));
const linkToArtist = "/artist/" + this.props.song.artist.id; const linkToArtist = "/artist/" + this.props.song.getIn(["artist", "id"]);
const linkToAlbum = "/album/" + this.props.song.album.id; const linkToAlbum = "/album/" + this.props.song.getIn(["album", "id"]);
return ( return (
<tr> <tr>
<td> <td>
@ -32,10 +32,10 @@ class SongsTableRowCSS extends Component {
<FontAwesome name="play-circle-o" aria-hidden="true" /> <FontAwesome name="play-circle-o" aria-hidden="true" />
</button> </button>
</td> </td>
<td className="title">{this.props.song.name}</td> <td className="title">{this.props.song.get("name")}</td>
<td className="artist"><Link to={linkToArtist}>{this.props.song.artist.name}</Link></td> <td className="artist"><Link to={linkToArtist}>{this.props.song.getIn(["artist", "name"])}</Link></td>
<td className="album"><Link to={linkToAlbum}>{this.props.song.album.name}</Link></td> <td className="album"><Link to={linkToAlbum}>{this.props.song.getIn(["album", "name"])}</Link></td>
<td className="genre">{this.props.song.genre}</td> <td className="genre">{this.props.song.get("genre")}</td>
<td className="length">{length}</td> <td className="length">{length}</td>
</tr> </tr>
); );
@ -51,7 +51,7 @@ export let SongsTableRow = CSSModules(SongsTableRowCSS, css);
class SongsTableCSS extends Component { class SongsTableCSS extends Component {
render () { render () {
var displayedSongs = this.props.songs; let displayedSongs = this.props.songs;
if (this.props.filterText) { if (this.props.filterText) {
// Use Fuse for the filter // Use Fuse for the filter
displayedSongs = new Fuse( displayedSongs = new Fuse(
@ -65,11 +65,11 @@ class SongsTableCSS extends Component {
displayedSongs = displayedSongs.map(function (item) { return item.item; }); displayedSongs = displayedSongs.map(function (item) { return item.item; });
} }
var rows = []; let rows = [];
displayedSongs.forEach(function (song) { displayedSongs.forEach(function (song) {
rows.push(<SongsTableRow song={song} key={song.id} />); rows.push(<SongsTableRow song={song} key={song.get("id")} />);
}); });
var loading = null; let loading = null;
if (rows.length == 0 && this.props.isFetching) { if (rows.length == 0 && this.props.isFetching) {
// If we are fetching and there is nothing to show // If we are fetching and there is nothing to show
loading = ( loading = (

View File

@ -24,22 +24,22 @@ class GridItemCSSIntl extends Component {
render () { render () {
const {formatMessage} = this.props.intl; const {formatMessage} = this.props.intl;
var nSubItems = this.props.item[this.props.subItemsType]; let nSubItems = this.props.item.get(this.props.subItemsType);
if (Array.isArray(nSubItems)) { if (Array.isArray(nSubItems)) {
nSubItems = nSubItems.length; nSubItems = nSubItems.length;
} }
var subItemsLabel = formatMessage(gridMessages[this.props.subItemsLabel], { itemCount: nSubItems }); let subItemsLabel = formatMessage(gridMessages[this.props.subItemsLabel], { itemCount: nSubItems });
const to = "/" + this.props.itemsType + "/" + this.props.item.id; const to = "/" + this.props.itemsType + "/" + this.props.item.get("id");
const id = "grid-item-" + this.props.itemsType + "/" + this.props.item.id; const id = "grid-item-" + this.props.itemsType + "/" + this.props.item.get("id");
const title = formatMessage(gridMessages["app.grid.goTo" + this.props.itemsType.capitalize() + "Page"]); const title = formatMessage(gridMessages["app.grid.goTo" + this.props.itemsType.capitalize() + "Page"]);
return ( return (
<div className="grid-item col-xs-6 col-sm-3" styleName="placeholders" id={id}> <div className="grid-item col-xs-6 col-sm-3" styleName="placeholders" id={id}>
<div className="grid-item-content text-center"> <div className="grid-item-content text-center">
<Link title={title} to={to}><img src={this.props.item.art} width="200" height="200" className="img-responsive img-circle art" styleName="art" alt={this.props.item.name}/></Link> <Link title={title} to={to}><img src={this.props.item.get("art")} width="200" height="200" className="img-responsive img-circle art" styleName="art" alt={this.props.item.get("name")}/></Link>
<h4 className="name" styleName="name">{this.props.item.name}</h4> <h4 className="name" styleName="name">{this.props.item.get("name")}</h4>
<span className="sub-items text-muted"><span className="n-sub-items">{nSubItems}</span> <span className="sub-items-type">{subItemsLabel}</span></span> <span className="sub-items text-muted"><span className="n-sub-items">{nSubItems}</span> <span className="sub-items-type">{subItemsLabel}</span></span>
</div> </div>
</div> </div>
@ -97,7 +97,7 @@ export class Grid extends Component {
return this.iso.arrange(ISOTOPE_OPTIONS); return this.iso.arrange(ISOTOPE_OPTIONS);
} }
// Use Fuse for the filter // Use Fuse for the filter
var result = new Fuse( let result = new Fuse(
props.items.toArray(), props.items.toArray(),
{ {
"keys": ["name"], "keys": ["name"],
@ -108,13 +108,13 @@ export class Grid extends Component {
// Apply filter on grid // Apply filter on grid
this.iso.arrange({ this.iso.arrange({
filter: function (item) { filter: function (item) {
var name = $(item).find(".name").text(); let name = $(item).find(".name").text();
return result.find(function (i) { return i.item.name == name; }); return result.find(function (i) { return i.item.name == name; });
}, },
transitionDuration: "0.4s", transitionDuration: "0.4s",
getSortData: { getSortData: {
relevance: function (item) { relevance: function (item) {
var name = $(item).find(".name").text(); let name = $(item).find(".name").text();
return result.reduce(function (p, c) { return result.reduce(function (p, c) {
if (c.item.name == name) { if (c.item.name == name) {
return c.score + p; return c.score + p;
@ -152,12 +152,12 @@ export class Grid extends Component {
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
// The list of keys seen in the previous render // The list of keys seen in the previous render
let currentKeys = prevProps.items.map( let currentKeys = prevProps.items.map(
(n) => "grid-item-" + prevProps.itemsType + "/" + n.id); (n) => "grid-item-" + prevProps.itemsType + "/" + n.get("id"));
// The latest list of keys that have been rendered // The latest list of keys that have been rendered
const {itemsType} = this.props; const {itemsType} = this.props;
let newKeys = this.props.items.map( let newKeys = this.props.items.map(
(n) => "grid-item-" + itemsType + "/" + n.id); (n) => "grid-item-" + itemsType + "/" + n.get("id"));
// Find which keys are new between the current set of keys and any new children passed to this component // Find which keys are new between the current set of keys and any new children passed to this component
let addKeys = immutableDiff(newKeys, currentKeys); let addKeys = immutableDiff(newKeys, currentKeys);
@ -165,7 +165,7 @@ export class Grid extends Component {
// Find which keys have been removed between the current set of keys and any new children passed to this component // Find which keys have been removed between the current set of keys and any new children passed to this component
let removeKeys = immutableDiff(currentKeys, newKeys); let removeKeys = immutableDiff(currentKeys, newKeys);
var iso = this.iso; let iso = this.iso;
if (removeKeys.count() > 0) { if (removeKeys.count() > 0) {
removeKeys.forEach(removeKey => iso.remove(document.getElementById(removeKey))); removeKeys.forEach(removeKey => iso.remove(document.getElementById(removeKey)));
iso.arrange(); iso.arrange();
@ -187,12 +187,12 @@ export class Grid extends Component {
} }
render () { render () {
var gridItems = []; let gridItems = [];
const { itemsType, itemsLabel, subItemsType, subItemsLabel } = this.props; const { itemsType, itemsLabel, subItemsType, subItemsLabel } = this.props;
this.props.items.forEach(function (item) { this.props.items.forEach(function (item) {
gridItems.push(<GridItem item={item} itemsType={itemsType} itemsLabel={itemsLabel} subItemsType={subItemsType} subItemsLabel={subItemsLabel} key={item.id} />); gridItems.push(<GridItem item={item} itemsType={itemsType} itemsLabel={itemsLabel} subItemsType={subItemsType} subItemsLabel={subItemsLabel} key={item.get("id")} />);
}); });
var loading = null; let loading = null;
if (gridItems.length == 0 && this.props.isFetching) { if (gridItems.length == 0 && this.props.isFetching) {
loading = ( loading = (
<div className="row text-center"> <div className="row text-center">

View File

@ -14,8 +14,8 @@ const paginationMessages = defineMessages(messagesMap(Array.concat([], commonMes
class PaginationCSSIntl extends Component { class PaginationCSSIntl extends Component {
computePaginationBounds(currentPage, nPages, maxNumberPagesShown=5) { computePaginationBounds(currentPage, nPages, maxNumberPagesShown=5) {
// Taken from http://stackoverflow.com/a/8608998/2626416 // Taken from http://stackoverflow.com/a/8608998/2626416
var lowerLimit = currentPage; let lowerLimit = currentPage;
var upperLimit = currentPage; let upperLimit = currentPage;
for (let b = 1; b < maxNumberPagesShown && b < nPages;) { for (let b = 1; b < maxNumberPagesShown && b < nPages;) {
if (lowerLimit > 1 ) { if (lowerLimit > 1 ) {
@ -62,8 +62,8 @@ class PaginationCSSIntl extends Component {
render () { render () {
const { formatMessage } = this.props.intl; const { formatMessage } = this.props.intl;
const { lowerLimit, upperLimit } = this.computePaginationBounds(this.props.currentPage, this.props.nPages); const { lowerLimit, upperLimit } = this.computePaginationBounds(this.props.currentPage, this.props.nPages);
var pagesButton = []; let pagesButton = [];
var key = 0; // key increment to ensure correct ordering let key = 0; // key increment to ensure correct ordering
if (lowerLimit > 1) { if (lowerLimit > 1) {
// Push first page // Push first page
pagesButton.push( pagesButton.push(
@ -85,8 +85,8 @@ class PaginationCSSIntl extends Component {
} }
} }
for (let i = lowerLimit; i < upperLimit; i++) { for (let i = lowerLimit; i < upperLimit; i++) {
var className = "page-item"; let className = "page-item";
var currentSpan = null; let currentSpan = null;
if (this.props.currentPage == i) { if (this.props.currentPage == i) {
className += " active"; className += " active";
currentSpan = <span className="sr-only">(<FormattedMessage {...paginationMessages["app.pagination.current"]} />)</span>; currentSpan = <span className="sr-only">(<FormattedMessage {...paginationMessages["app.pagination.current"]} />)</span>;

View File

@ -1,54 +1,86 @@
import React, { Component, PropTypes } from "react"; import React, { Component, PropTypes } from "react";
import CSSModules from "react-css-modules"; import CSSModules from "react-css-modules";
import { defineMessages, injectIntl, intlShape, FormattedMessage } from "react-intl";
import FontAwesome from "react-fontawesome";
import { messagesMap } from "../../utils";
import css from "../../styles/elements/WebPlayer.scss"; import css from "../../styles/elements/WebPlayer.scss";
class WebPlayerCSS extends Component { import commonMessages from "../../locales/messagesDescriptors/common";
componentDidMount () { import messages from "../../locales/messagesDescriptors/elements/WebPlayer";
// TODO: Should be in the container mounting WebPlayer
$(".sidebar").css("bottom", "15vh"); const webplayerMessages = defineMessages(messagesMap(Array.concat([], commonMessages, messages)));
$(".main-panel").css("margin-bottom", "15vh");
class WebPlayerCSSIntl extends Component {
constructor (props) {
super(props);
this.artOpacityHandler = this.artOpacityHandler.bind(this);
}
artOpacityHandler (ev) {
if (ev.type == "mouseover") {
this.refs.art.style.opacity = "1";
} else {
this.refs.art.style.opacity = "0.75";
}
} }
render () { render () {
const { formatMessage } = this.props.intl;
const playPause = this.props.isPlaying ? "pause" : "play";
const randomBtnStyles = ["randomBtn"];
const repeatBtnStyles = ["repeatBtn"];
if (this.props.isRandom) {
randomBtnStyles.push("active");
}
if (this.props.isRepeat) {
repeatBtnStyles.push("active");
}
return ( return (
<div id="row"> <div id="row" styleName="webplayer">
<div id="webplayer" className="col-xs-12" styleName="body"> <div className="col-xs-12">
{ /* Top Info */ } <div className="row" styleName="artRow" onMouseOver={this.artOpacityHandler} onMouseOut={this.artOpacityHandler}>
<div id="title" styleName="title"> <div className="col-xs-12">
<span id="track">Foobar</span> <img src={this.props.song.art} width="200" height="200" ref="art" styleName="art" />
<div id="timer" styleName="timer">0:00</div> <h2>{this.props.song.title}</h2>
<div id="duration" styleName="duration">0:00</div> <h3>
</div> <span className="text-capitalize">
<FormattedMessage {...webplayerMessages["app.webplayer.by"]} />
{ /* Controls */ } </span> {this.props.song.artist}
<div styleName="controlsOuter"> </h3>
<div styleName="controlsInner">
<div id="loading" styleName="loading"></div>
<div id="playBtn" styleName="playBtn"></div>
<div id="pauseBtn" styleName="pauseBtn"></div>
<div id="prevBtn" styleName="prevBtn"></div>
<div id="nextBtn" styleName="nextBtn"></div>
</div> </div>
<div id="playlistBtn" styleName="playlistBtn"></div>
<div id="volumeBtn" styleName="volumeBtn"></div>
</div> </div>
{ /* Progress */ } <div className="row text-center" styleName="controls">
<div id="waveform" styleName="waveform"></div> <div className="col-xs-12">
<div id="bar" styleName="progressBar"></div> <button styleName="prevBtn" aria-label={formatMessage(webplayerMessages["app.webplayer.previous"])} title={formatMessage(webplayerMessages["app.webplayer.previous"])}>
<div id="progress" styleName="progress"></div> <FontAwesome name="step-backward" />
</button>
{ /* Playlist */ } <button className="play" styleName="playPauseBtn" aria-label={formatMessage(webplayerMessages["app.common." + playPause])} title={formatMessage(webplayerMessages["app.common." + playPause])}>
<div id="playlist" styleName="playlist"> <FontAwesome name={playPause} />
<div id="list" styleName="list"></div> </button>
</div> <button styleName="nextBtn" aria-label={formatMessage(webplayerMessages["app.webplayer.next"])} title={formatMessage(webplayerMessages["app.webplayer.next"])}>
<FontAwesome name="step-forward" />
{ /* Volume */ } </button>
<div id="volume" styleName="volume-fadeout"> </div>
<div id="barFull" styleName="barFull"></div> <div className="col-xs-12">
<div id="barEmpty" styleName="barEmpty"></div> <button styleName="volumeBtn" aria-label={formatMessage(webplayerMessages["app.webplayer.volume"])} title={formatMessage(webplayerMessages["app.webplayer.volume"])}>
<div id="sliderBtn" styleName="sliderBtn"></div> <FontAwesome name="volume-up" />
</button>
<button styleName={repeatBtnStyles.join(" ")} aria-label={formatMessage(webplayerMessages["app.webplayer.repeat"])} title={formatMessage(webplayerMessages["app.webplayer.repeat"])} aria-pressed={this.props.isRepeat}>
<FontAwesome name="repeat" />
</button>
<button styleName={randomBtnStyles.join(" ")} aria-label={formatMessage(webplayerMessages["app.webplayer.random"])} title={formatMessage(webplayerMessages["app.webplayer.random"])} aria-pressed={this.props.isRandom}>
<FontAwesome name="random" />
</button>
<button styleName="playlistBtn" aria-label={formatMessage(webplayerMessages["app.webplayer.playlist"])} title={formatMessage(webplayerMessages["app.webplayer.playlist"])}>
<FontAwesome name="list" />
</button>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -56,4 +88,12 @@ class WebPlayerCSS extends Component {
} }
} }
export default CSSModules(WebPlayerCSS, css); WebPlayerCSSIntl.propTypes = {
song: PropTypes.object.isRequired,
isPlaying: PropTypes.bool.isRequired,
isRandom: PropTypes.bool.isRequired,
isRepeat: PropTypes.bool.isRequired,
intl: intlShape.isRequired
};
export default injectIntl(CSSModules(WebPlayerCSSIntl, css, { allowMultiple: true }));

View File

@ -7,7 +7,7 @@ import { messagesMap } from "../../utils";
import commonMessages from "../../locales/messagesDescriptors/common"; import commonMessages from "../../locales/messagesDescriptors/common";
import messages from "../../locales/messagesDescriptors/layouts/Sidebar"; import messages from "../../locales/messagesDescriptors/layouts/Sidebar";
import WebPlayer from "../elements/WebPlayer"; import WebPlayer from "../../containers/WebPlayer";
import css from "../../styles/layouts/Sidebar.scss"; import css from "../../styles/layouts/Sidebar.scss";
@ -138,13 +138,13 @@ class SidebarLayoutIntl extends Component {
</li> </li>
</ul> </ul>
</nav> </nav>
<WebPlayer />
</div> </div>
</div> </div>
<div className="col-xs-12 col-md-11 col-md-offset-1 col-lg-10 col-lg-offset-2 main-panel" styleName="main-panel" onClick={collapseHamburger} role="main"> <div className="col-xs-12 col-md-11 col-md-offset-1 col-lg-10 col-lg-offset-2 main-panel" styleName="main-panel" onClick={collapseHamburger} role="main">
{this.props.children} {this.props.children}
</div> </div>
{ /* TODO <WebPlayer /> */ }
</div> </div>
); );
} }

View File

@ -0,0 +1,21 @@
import React, { Component } from "react";
import WebPlayerComponent from "../components/elements/WebPlayer";
export default class WebPlayer extends Component {
render () {
const webplayerProps = {
song: {
art: "http://albumartcollection.com/wp-content/uploads/2011/07/summer-album-art.jpg",
title: "Tel-ho",
artist: "Lapso Laps",
},
isPlaying: false,
isRandom: false,
isRepeat: true
};
return (
<WebPlayerComponent {...webplayerProps} />
);
}
}

View File

@ -8,7 +8,8 @@ module.exports = {
"app.common.close": "Close", // Close "app.common.close": "Close", // Close
"app.common.go": "Go", // Go "app.common.go": "Go", // Go
"app.common.loading": "Loading…", // Loading indicator "app.common.loading": "Loading…", // Loading indicator
"app.common.play": "Play", // PLay icon description "app.common.pause": "Pause", // Pause icon description
"app.common.play": "Play", // Play icon description
"app.common.track": "{itemCount, plural, one {track} other {tracks}}", // Track "app.common.track": "{itemCount, plural, one {track} other {tracks}}", // Track
"app.filter.filter": "Filter…", // Filtering input placeholder "app.filter.filter": "Filter…", // Filtering input placeholder
"app.filter.whatAreWeListeningToToday": "What are we listening to today?", // Description for the filter bar "app.filter.whatAreWeListeningToToday": "What are we listening to today?", // Description for the filter bar
@ -43,4 +44,11 @@ module.exports = {
"app.songs.genre": "Genre", // Genre (song) "app.songs.genre": "Genre", // Genre (song)
"app.songs.length": "Length", // Length (song) "app.songs.length": "Length", // Length (song)
"app.songs.title": "Title", // Title (song) "app.songs.title": "Title", // Title (song)
"app.webplayer.by": "by", // Artist affiliation of a song
"app.webplayer.next": "Next", // Next button description
"app.webplayer.playlist": "Playlist", // Playlist button description
"app.webplayer.previous": "Previous", // Previous button description
"app.webplayer.random": "Random", // Random button description
"app.webplayer.repeat": "Repeat", // Repeat button description
"app.webplayer.volume": "Volume", // Volume button description
}; };

View File

@ -8,6 +8,7 @@ module.exports = {
"app.common.close": "Fermer", // Close "app.common.close": "Fermer", // Close
"app.common.go": "Aller", // Go "app.common.go": "Aller", // Go
"app.common.loading": "Chargement…", // Loading indicator "app.common.loading": "Chargement…", // Loading indicator
"app.common.pause": "Pause", // Pause icon description
"app.common.play": "Jouer", // PLay icon description "app.common.play": "Jouer", // PLay icon description
"app.common.track": "{itemCount, plural, one {piste} other {pistes}}", // Track "app.common.track": "{itemCount, plural, one {piste} other {pistes}}", // Track
"app.filter.filter": "Filtrer…", // Filtering input placeholder "app.filter.filter": "Filtrer…", // Filtering input placeholder
@ -43,4 +44,11 @@ module.exports = {
"app.songs.genre": "Genre", // Genre (song) "app.songs.genre": "Genre", // Genre (song)
"app.songs.length": "Durée", // Length (song) "app.songs.length": "Durée", // Length (song)
"app.songs.title": "Titre", // Title (song) "app.songs.title": "Titre", // Title (song)
"app.webplayer.by": "par", // Artist affiliation of a song
"app.webplayer.next": "Suivant", // Next button description
"app.webplayer.playlist": "Liste de lecture", // Playlist button description
"app.webplayer.previous": "Précédent", // Previous button description
"app.webplayer.random": "Aléatoire", // Random button description
"app.webplayer.repeat": "Répéter", // Repeat button description
"app.webplayer.volume": "Volume", // Volume button description
}; };

View File

@ -36,9 +36,14 @@ const messages = [
}, },
{ {
id: "app.common.play", id: "app.common.play",
description: "PLay icon description", description: "Play icon description",
defaultMessage: "Play" defaultMessage: "Play"
}, },
{
id: "app.common.pause",
description: "Pause icon description",
defaultMessage: "Pause"
},
]; ];
export default messages; export default messages;

View File

@ -0,0 +1,39 @@
const messages = [
{
id: "app.webplayer.by",
defaultMessage: "by",
description: "Artist affiliation of a song"
},
{
id: "app.webplayer.previous",
defaultMessage: "Previous",
description: "Previous button description"
},
{
id: "app.webplayer.next",
defaultMessage: "Next",
description: "Next button description"
},
{
id: "app.webplayer.volume",
defaultMessage: "Volume",
description: "Volume button description"
},
{
id: "app.webplayer.repeat",
defaultMessage: "Repeat",
description: "Repeat button description"
},
{
id: "app.webplayer.random",
defaultMessage: "Random",
description: "Random button description"
},
{
id: "app.webplayer.playlist",
defaultMessage: "Playlist",
description: "Playlist button description"
}
];
export default messages;

View File

@ -20,7 +20,7 @@ function _checkHTTPStatus (response) {
} }
function _parseToJSON (responseText) { function _parseToJSON (responseText) {
var x2js = new X2JS({ let x2js = new X2JS({
attributePrefix: "", attributePrefix: "",
keepCData: false keepCData: false
}); });
@ -47,7 +47,7 @@ function _checkAPIErrors (jsonData) {
} }
function _uglyFixes (jsonData) { function _uglyFixes (jsonData) {
var _uglyFixesSongs = function (songs) { let _uglyFixesSongs = function (songs) {
return songs.map(function (song) { return songs.map(function (song) {
// Fix for cdata left in artist and album // Fix for cdata left in artist and album
song.artist.name = song.artist.cdata; song.artist.name = song.artist.cdata;
@ -56,7 +56,7 @@ function _uglyFixes (jsonData) {
}); });
}; };
var _uglyFixesAlbums = function (albums) { let _uglyFixesAlbums = function (albums) {
return albums.map(function (album) { return albums.map(function (album) {
// TODO // TODO
// Fix for absence of distinction between disks in the same album // Fix for absence of distinction between disks in the same album
@ -80,7 +80,7 @@ function _uglyFixes (jsonData) {
}); });
}; };
var _uglyFixesArtists = function (artists) { let _uglyFixesArtists = function (artists) {
return artists.map(function (artist) { return artists.map(function (artist) {
// Move albums one node top // Move albums one node top
if (artist.albums.album) { if (artist.albums.album) {
@ -149,11 +149,6 @@ function _uglyFixes (jsonData) {
return jsonData; return jsonData;
} }
function _normalizeResponse(jsonData) {
// TODO
return jsonData;
}
// Fetches an API response and normalizes the result JSON according to schema. // Fetches an API response and normalizes the result JSON according to schema.
// This makes every API response have the same shape, regardless of how nested it was. // This makes every API response have the same shape, regardless of how nested it was.
function doAPICall (endpoint, action, auth, username, extraParams) { function doAPICall (endpoint, action, auth, username, extraParams) {
@ -175,8 +170,7 @@ function doAPICall (endpoint, action, auth, username, extraParams) {
.then(_parseToJSON) .then(_parseToJSON)
.then(_checkAPIErrors) .then(_checkAPIErrors)
.then(jsonData => humps.camelizeKeys(jsonData)) // Camelize .then(jsonData => humps.camelizeKeys(jsonData)) // Camelize
.then(_uglyFixes) .then(_uglyFixes);
.then(_normalizeResponse);
} }
// Action key that carries API call info interpreted by this Redux middleware. // Action key that carries API call info interpreted by this Redux middleware.

22
app/models/api.js Normal file
View File

@ -0,0 +1,22 @@
import { Schema, arrayOf } from "normalizr";
export const artist = new Schema("artist");
export const album = new Schema("album");
export const track = new Schema("track");
export const tag = new Schema("tag");
artist.define({
albums: arrayOf(album),
songs: arrayOf(track)
});
album.define({
artist: artist,
tracks: arrayOf(track),
tag: arrayOf(tag)
});
track.define({
artist: artist,
album: album
});

View File

@ -2,7 +2,8 @@ import Immutable from "immutable";
export const stateRecord = new Immutable.Record({ export const stateRecord = new Immutable.Record({
isFetching: false, isFetching: false,
items: new Immutable.List(), result: new Immutable.Map(),
entities: new Immutable.Map(),
error: null, error: null,
currentPage: 1, currentPage: 1,
nPages: 1 nPages: 1

View File

@ -7,28 +7,16 @@ import paginate from "./paginate";
import * as ActionTypes from "../actions"; import * as ActionTypes from "../actions";
// Updates the pagination data for different actions. // Updates the pagination data for different actions.
const pagination = combineReducers({ const api = paginate([
artists: paginate([ ActionTypes.API_REQUEST,
ActionTypes.ARTISTS_REQUEST, ActionTypes.API_SUCCESS,
ActionTypes.ARTISTS_SUCCESS, ActionTypes.API_FAILURE
ActionTypes.ARTISTS_FAILURE ]);
]),
albums: paginate([
ActionTypes.ALBUMS_REQUEST,
ActionTypes.ALBUMS_SUCCESS,
ActionTypes.ALBUMS_FAILURE
]),
songs: paginate([
ActionTypes.SONGS_REQUEST,
ActionTypes.SONGS_SUCCESS,
ActionTypes.SONGS_FAILURE
])
});
const rootReducer = combineReducers({ const rootReducer = combineReducers({
routing, routing,
auth, auth,
pagination api
}); });
export default rootReducer; export default rootReducer;

View File

@ -30,7 +30,8 @@ export default function paginate(types) {
return ( return (
state state
.set("isFetching", false) .set("isFetching", false)
.set("items", new Immutable.List(payload.items)) .set("result", Immutable.fromJS(payload.result))
.set("entities", Immutable.fromJS(payload.entities))
.set("error", null) .set("error", null)
.set("nPages", payload.nPages) .set("nPages", payload.nPages)
.set("currentPage", payload.currentPage) .set("currentPage", payload.currentPage)

View File

@ -1,340 +1,51 @@
.body { $controlsMarginTop: 10px;
height: 15vh;
background: #bb71f3; .webplayer {
background: linear-gradient(135deg, #bb71f3 0%, #3d4d91 100%); margin-top: 1em;
font-family: "Helvetica Neue", "Futura", "Trebuchet MS", Arial;
user-select: none;
-webkit-tap-highlight-color: rgba(255, 255, 255, 0);
padding-left: 0;
padding-right: 0;
position: fixed;
bottom: 0;
} }
/* Top Info */ .art {
.title { opacity: 0.75;
position: absolute; position: absolute;
width: 100%; z-index: -10;
top: 3%;
line-height: 34px;
height: 34px;
text-align: center;
font-size: 34px;
opacity: 0.9;
font-weight: 300;
color: #fff;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.33);
} }
.timer { .artRow {
position: absolute; min-height: 200px;
top: 0;
left: 3%;
text-align: left;
font-size: 26px;
opacity: 0.9;
font-weight: 300;
color: #fff;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.33);
} }
.duration { /**
position: absolute; * Controls
top: 0; */
right: 3%;
text-align: right;
font-size: 26px;
opacity: 0.5;
font-weight: 300;
color: #fff;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.33);
}
/* Controls */ .controls {
.controlsOuter { margin-top: $controlsMarginTop;
position: absolute;
width: 100%;
height: 70px;
bottom: 3%;
}
.controlsInner {
position: absolute;
width: 340px;
height: 70px;
left: 50%;
margin: 0 -170px;
} }
.btn { .btn {
position: absolute; background: transparent;
cursor: pointer; border: none;
opacity: 0.9; opacity: 0.8;
-webkit-filter: drop-shadow(1px 1px 2px rgba(0, 0, 0, 0.33));
filter: drop-shadow(1px 1px 2px rgba(0, 0, 0, 0.33));
-webkit-user-select: none;
user-select: none;
} }
.btn:hover { .btn:hover {
opacity: 1; opacity: 1;
} }
.playBtn { .prevBtn,
composes: btn; .playPauseBtn,
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEUAAABGCAYAAACaGVmHAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTExIDc5LjE1ODMyNSwgMjAxNS8wOS8xMC0wMToxMDoyMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6QThERThENUFDNkJEMTFFNTk2RTBDMTRBMkYyNjVGMzQiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6QThERThENUJDNkJEMTFFNTk2RTBDMTRBMkYyNjVGMzQiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpBOERFOEQ1OEM2QkQxMUU1OTZFMEMxNEEyRjI2NUYzNCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpBOERFOEQ1OUM2QkQxMUU1OTZFMEMxNEEyRjI2NUYzNCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PnFM7yIAAAFWSURBVHja7NRJDoMwDEDRcPL2ZoVeLEUtCyoCZPBsf8mS10+Wp5zzK6X0XGdJ0bdpRcnbPgfOESUFzjmKe5wrFLc4NSjucFpQ3OD0oJjHGUExiwOBYg4HEsUMDgaKehxMFLU4FCjqcChR1OBwoIjH4UQRiyMBRRyOJBQxOBJR2HEko7DhaEAhx9GEQoajEQUdRzMKGo4FFHAcSyhgOBZRhnEso3TjeEBpxvGEUo3jEeUWxzPKKU6gFHACpYATKMfegbLDWOcRl/Jr2X7JHI+2gOEZ5RTDI8othieUagwPKM0YllG6MSyiDGNYQgHDsIACjqEZBQ1DIwo6hiYUMgwNKOQYklHYMCSisGNIQhGDIQFFHAYnilgMDhTxGJQoajAoUNRhYKKoxcBAUY8BiWIGAwLFHMYIilmMHhTzGC0objBqUNxhXKG4xSihuMfYo8yB8d9HgAEATyefBJWpw70AAAAASUVORK5CYII='); .nextBtn,
width: 69px; .volumeBtn,
height: 70px; .repeatBtn,
left: 50%; .randomBtn,
margin: auto -34.5px;
}
.pauseBtn {
composes: btn;
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEUAAABGCAYAAACaGVmHAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTExIDc5LjE1ODMyNSwgMjAxNS8wOS8xMC0wMToxMDoyMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MDE4MjkzRjNDNzI0MTFFNTk2RTBDMTRBMkYyNjVGMzQiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MDE4MjkzRjRDNzI0MTFFNTk2RTBDMTRBMkYyNjVGMzQiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpBOERFOEQ1Q0M2QkQxMUU1OTZFMEMxNEEyRjI2NUYzNCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDowMTgyOTNGMkM3MjQxMUU1OTZFMEMxNEEyRjI2NUYzNCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pne5d1AAAAB8SURBVHja7NCxCQAwAALBuP/QZoEUlinua0G4tD1j83Asv/4GChQoUKBAgQIFChQoUKBAgQIFChQoUKBAgQIFChQoUKBAgQIFChQoUKBAgQIFChQoUKBAgQIFChQoUKBAgQIFChQoUKBAgQIFChQoUKBAgQIFCpRXV4ABAOgpXYTDYwEwAAAAAElFTkSuQmCC');
width: 69px;
height: 70px;
left: 50%;
margin: auto -34.5px;
display: none;
}
.prevBtn {
composes: btn;
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACMAAAAjCAYAAAAe2bNZAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTExIDc5LjE1ODMyNSwgMjAxNS8wOS8xMC0wMToxMDoyMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MDE4MjkzRkJDNzI0MTFFNTk2RTBDMTRBMkYyNjVGMzQiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MDE4MjkzRkNDNzI0MTFFNTk2RTBDMTRBMkYyNjVGMzQiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDowMTgyOTNGOUM3MjQxMUU1OTZFMEMxNEEyRjI2NUYzNCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDowMTgyOTNGQUM3MjQxMUU1OTZFMEMxNEEyRjI2NUYzNCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PndeAH8AAACtSURBVHja7NXJCcAgEAVQEevK0lk6M1aW6CGQQBBn+ephBv7F0wNn8Vd7OWCWnOjd2FpzYk7K2cJAxFEA78cwA6I3porohWlCoDEkBArDQmhjRAgtjApCilFFcDEQBAcTUYinKLcJCqFinGEMYxjDgDFpJkzZwDsSRf2mE4ni9gwEJW1gVZTWNKmgtEdbhELtGRYKvfRIqF4buAnV+xxUUaNu0y9q9KH8oG4BBgDjdpk0jMoz1gAAAABJRU5ErkJggg==');
width: 35px;
height: 35px;
left: 0;
top: 50%;
margin: -17.5px auto;
}
.nextBtn {
composes: btn;
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACMAAAAjCAYAAAAe2bNZAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTExIDc5LjE1ODMyNSwgMjAxNS8wOS8xMC0wMToxMDoyMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MDE4MjkzRjdDNzI0MTFFNTk2RTBDMTRBMkYyNjVGMzQiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MDE4MjkzRjhDNzI0MTFFNTk2RTBDMTRBMkYyNjVGMzQiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDowMTgyOTNGNUM3MjQxMUU1OTZFMEMxNEEyRjI2NUYzNCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDowMTgyOTNGNkM3MjQxMUU1OTZFMEMxNEEyRjI2NUYzNCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PiFbkXsAAACvSURBVHja7NZBDoAgDARA9GPiz/yZ8DKsxgMHY1rYBQ5sshdCwiQcWpdSOqWb1BGrSn6RiTJjmKhiDANVjUGiYBgECo6pQdEwJSg6xoJqhtGgVFkeETZBekhjdqZ6g4H5QnXH5Cg/CkadiZmYiZkYdtYGb8QRMDdi144C1jfFd0AG69RG7jNB6nsvV3+IZhgNgo6xIGiYEgQcU4OAYRCIagwSUYxhIMwYJsKEuQQYABTs+qTMuF1rAAAAAElFTkSuQmCC');
width: 35px;
height: 35px;
right: 0;
top: 50%;
margin: -17.5px auto;
}
.playlistBtn { .playlistBtn {
composes: btn; composes: btn;
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACMAAAAjCAYAAAAe2bNZAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTExIDc5LjE1ODMyNSwgMjAxNS8wOS8xMC0wMToxMDoyMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MUVBQzMwNzRDNzI0MTFFNTk2RTBDMTRBMkYyNjVGMzQiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MUVBQzMwNzVDNzI0MTFFNTk2RTBDMTRBMkYyNjVGMzQiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDoxRUFDMzA3MkM3MjQxMUU1OTZFMEMxNEEyRjI2NUYzNCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDoxRUFDMzA3M0M3MjQxMUU1OTZFMEMxNEEyRjI2NUYzNCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PnWBPa0AAADGSURBVHjaYvz//z/DYAFMDIMIjDpm1DHDwjH/0fDugXAMCxK7AojPAnEHmpo0IFaisTsqIEECLPT+Q4ALlL0bihmQ+LQGYLsYoSXwf2iovAdiYyjbdaCiaQ+S2FkopjtgHK2bCESTC5r4+4GIKuQEjAz2DGQCHhTlDAtaLtoDjSJkoATN7iMzmjrRxO+NljODsZzpwBJNs0bLGShwhToCvWHVQYes7YruGJiFgljKmpEbTa5YKsrRcmbUMaOOGXUMNQBAgAEAm2XFJrn+jrsAAAAASUVORK5CYII=');
width: 35px;
height: 35px;
top: 50%;
left: 3%;
margin: -17.5px auto;
} }
.volumeBtn { .playPauseBtn {
composes: btn; font-size: $font-size-h2;
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACMAAAAjCAYAAAAe2bNZAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTExIDc5LjE1ODMyNSwgMjAxNS8wOS8xMC0wMToxMDoyMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MUVBQzMwNzBDNzI0MTFFNTk2RTBDMTRBMkYyNjVGMzQiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MUVBQzMwNzFDNzI0MTFFNTk2RTBDMTRBMkYyNjVGMzQiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDoxRUFDMzA2RUM3MjQxMUU1OTZFMEMxNEEyRjI2NUYzNCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDoxRUFDMzA2RkM3MjQxMUU1OTZFMEMxNEEyRjI2NUYzNCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PtAZ+FgAAAHRSURBVHja7FjRbYMwEIWqC7CCO4IzgjMCHYEV6AjuCHSEMAKMkIwAI5ARXFt9rox1phDA5KMnPSGwOZ4fx/mOVCmVPIu9JE9ke5GRGpejyeQanUapkXlj5lzEIMM1GqjRAyG1eNCLCeAVyDQq9WOdRoHrDeDP7YCM8reGiNQYQER6D6DIGHDc02xFJsfqjF00GDEnRCaBegp+RmMp8oyYERcmAAvMvWl8aLSBuQ2O58D4Ff7eqJiZa4MTF1OYUsZAwF9BKaOw0tsf6pjx+wwVXWU41Pwk1DF2opQRK7+skDIyECMlrv8GfoztwMaW9K7XTqKMujeZV8SQma31eO0iNpkW8L/a3t02Yu7aITL8CDL2S2VE/jqsnnHJ3I8urnpKldhkOEFmdB6TTO7kFveVRScjoExLqBWVTIbsax765RFh7n74GoFMiQefCLXcbWFEhs/MFfeFZGon9fsx1I78PVDPyA3qmXyqnhEzV1lgRT124/rBSq+Dj/Pa7sDUPVesrEGRvUSZMlQ/rSmgCqc7qBZ0B7aQ37RVsb2QJOIp1DcNUDXbg4wFw2qV06j5ZCqQYSE/yYZ1LxVPvjJ86v50p/8ztr96X3JT+v+zKGDfAgwAZkMcn0x+AsMAAAAASUVORK5CYII=');
width: 35px;
height: 35px;
top: 50%;
right: 3%;
margin: -17.5px auto;
} }
/* Progress */ .active {
.waveform { color: $blue;
width: 100%;
height: 30%;
position: absolute;
left: 0;
top: 50%;
margin: -15% auto;
display: none;
cursor: pointer;
opacity: 0.8;
-webkit-user-select: none;
user-select: none;
}
.waveform:hover {
opacity: 1;
}
.progressBar {
position: absolute;
top: 50%;
left: 0;
width: 100%;
height: 2px;
background-color: rgba(255, 255, 255, 0.9);
box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.33);
opacity: 0.9;
}
.progress {
position: absolute;
top: 0;
left: 0;
width: 0%;
height: 100%;
background-color: rgba(0, 0, 0, 0.1);
z-index: -1;
}
/* Loading */
.loading {
position: absolute;
left: 50%;
top: 50%;
margin: -35px;
width: 70px;
height: 70px;
background-color: #fff;
border-radius: 100%;
-webkit-animation: sk-scaleout 1.0s infinite ease-in-out;
animation: sk-scaleout 1.0s infinite ease-in-out;
display: none;
}
@-webkit-keyframes sk-scaleout {
0% { -webkit-transform: scale(0) }
100% {
-webkit-transform: scale(1.0);
opacity: 0;
}
}
@keyframes sk-scaleout {
0% {
-webkit-transform: scale(0);
transform: scale(0);
} 100% {
-webkit-transform: scale(1.0);
transform: scale(1.0);
opacity: 0;
}
}
/* Plylist */
.playlist {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
background-color: rgba(0, 0, 0, 0.5);
display: none;
}
.list {
width: 100%;
height: 360px;
position: absolute;
top: 50%;
left: 0;
margin: -180px auto;
}
.list-song {
width: 100%;
height: 120px;
font-size: 50px;
line-height: 120px;
text-align: center;
font-weight: bold;
color: #fff;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.33);
}
.list-song:hover {
background-color: rgba(255, 255, 255, 0.1);
cursor: pointer;
}
/* Volume */
.volume {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
background-color: rgba(0, 0, 0, 0.5);
touch-action: none;
-webkit-user-select: none;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
display: none;
}
.bar {
position: absolute;
top: 50%;
left: 5%;
margin: -5px auto;
height: 10px;
background-color: rgba(255, 255, 255, 0.9);
box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.33);
}
.barEmpty {
composes: bar;
width: 90%;
opacity: 0.5;
box-shadow: none;
cursor: pointer;
}
.barFull {
composes: bar;
width: 90%;
}
.sliderBtn {
width: 50px;
height: 50px;
position: absolute;
top: 50%;
left: 93.25%;
margin: -25px auto;
background-color: rgba(255, 255, 255, 0.8);
box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.33);
border-radius: 25px;
cursor: pointer;
}
/* Fade-In */
.fadeout {
webkit-animation: fadeout 0.5s;
-ms-animation: fadeout 0.5s;
animation: fadeout 0.5s;
}
.fadein {
webkit-animation: fadein 0.5s;
-ms-animation: fadein 0.5s;
animation: fadein 0.5s;
}
@keyframes fadein {
from { opacity: 0; }
to { opacity: 1; }
}
@-webkit-keyframes fadein {
from { opacity: 0; }
to { opacity: 1; }
}
@-ms-keyframes fadein {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes fadeout {
from { opacity: 1; }
to { opacity: 0; }
}
@-webkit-keyframes fadeout {
from { opacity: 1; }
to { opacity: 0; }
}
@-ms-keyframes fadeout {
from { opacity: 1; }
to { opacity: 0; }
}
/** Composed classes */
.volume-fadeout {
composes: volume;
composes: fadeout;
} }

View File

@ -1,3 +1,6 @@
// Make variables and mixins available when using CSS modules. // Make variables and mixins available when using CSS modules.
@import "node_modules/bootstrap-sass/assets/stylesheets/bootstrap/_variables"; @import "node_modules/bootstrap-sass/assets/stylesheets/bootstrap/_variables";
@import "node_modules/bootstrap-sass/assets/stylesheets/bootstrap/_mixins"; @import "node_modules/bootstrap-sass/assets/stylesheets/bootstrap/_mixins";
$blue: #3e90fa;
$orange: #faa83e;

View File

@ -11,6 +11,6 @@ String.prototype.capitalize = function () {
* @param chars A regex-like element to strip from the end. * @param chars A regex-like element to strip from the end.
*/ */
String.prototype.rstrip = function (chars) { String.prototype.rstrip = function (chars) {
var regex = new RegExp(chars + "$"); let regex = new RegExp(chars + "$");
return this.replace(regex, ""); return this.replace(regex, "");
}; };

View File

@ -1,5 +1,5 @@
export function getBrowserLocales () { export function getBrowserLocales () {
var langs; let langs;
if (navigator.languages) { if (navigator.languages) {
// chrome does not currently set navigator.language correctly https://code.google.com/p/chromium/issues/detail?id=101138 // chrome does not currently set navigator.language correctly https://code.google.com/p/chromium/issues/detail?id=101138
@ -14,8 +14,8 @@ export function getBrowserLocales () {
} }
// Some browsers does not return uppercase for second part // Some browsers does not return uppercase for second part
var locales = langs.map(function (lang) { let locales = langs.map(function (lang) {
var locale = lang.split("-"); let locale = lang.split("-");
return locale[1] ? `${locale[0]}-${locale[1].toUpperCase()}` : lang; return locale[1] ? `${locale[0]}-${locale[1].toUpperCase()}` : lang;
}); });
@ -23,7 +23,7 @@ export function getBrowserLocales () {
} }
export function messagesMap(messagesDescriptorsArray) { export function messagesMap(messagesDescriptorsArray) {
var messagesDescriptorsMap = {}; let messagesDescriptorsMap = {};
messagesDescriptorsArray.forEach(function (item) { messagesDescriptorsArray.forEach(function (item) {
messagesDescriptorsMap[item.id] = item; messagesDescriptorsMap[item.id] = item;

View File

@ -18,7 +18,7 @@ export function filterInt (value) {
*/ */
export function formatLength (time) { export function formatLength (time) {
const min = Math.floor(time / 60); const min = Math.floor(time / 60);
var sec = (time - 60 * min); let sec = (time - 60 * min);
if (sec < 10) { if (sec < 10) {
sec = "0" + sec; sec = "0" + sec;
} }

View File

@ -1,5 +1,5 @@
export function assembleURLAndParams (endpoint, params) { export function assembleURLAndParams (endpoint, params) {
var url = endpoint + "?"; let url = endpoint + "?";
Object.keys(params).forEach( Object.keys(params).forEach(
key => { key => {
if (Array.isArray(params[key])) { if (Array.isArray(params[key])) {

View File

@ -1,6 +1,7 @@
import React, { Component } from "react"; import React, { Component } from "react";
import { bindActionCreators } from "redux"; import { bindActionCreators } from "redux";
import { connect } from "react-redux"; import { connect } from "react-redux";
import Immutable from "immutable";
import * as actionCreators from "../actions"; import * as actionCreators from "../actions";
@ -19,21 +20,41 @@ export class AlbumPage extends Component {
} }
render () { render () {
const album = this.props.albums.find( if (this.props.album) {
item => item.id == this.props.params.id
);
if (album) {
return ( return (
<Album album={album} /> <Album album={this.props.album} songs={this.props.songs} />
); );
} }
return null; // Loading return (
<div></div>
); // TODO: Loading
} }
} }
const mapStateToProps = (state) => ({ const mapStateToProps = (state, ownProps) => {
albums: state.pagination.albums.items const albums = state.api.entities.get("album");
}); let album = undefined;
let songs = new Immutable.List();
if (albums) {
// Get artist
album = albums.find(
item => item.get("id") == ownProps.params.id
);
// Get songs
const tracks = album.get("tracks");
if (Immutable.List.isList(tracks)) {
songs = new Immutable.Map(
tracks.map(
id => [id, state.api.entities.getIn(["track", id])]
)
);
}
}
return {
album: album,
songs: songs
};
};
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
actions: bindActionCreators(actionCreators, dispatch) actions: bindActionCreators(actionCreators, dispatch)

View File

@ -2,6 +2,7 @@ import React, { Component, PropTypes } from "react";
import { bindActionCreators } from "redux"; import { bindActionCreators } from "redux";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { defineMessages, injectIntl, intlShape } from "react-intl"; import { defineMessages, injectIntl, intlShape } from "react-intl";
import Immutable from "immutable";
import * as actionCreators from "../actions"; import * as actionCreators from "../actions";
import { i18nRecord } from "../models/i18n"; import { i18nRecord } from "../models/i18n";
@ -32,13 +33,13 @@ class AlbumsPageIntl extends Component {
render () { render () {
const {formatMessage} = this.props.intl; const {formatMessage} = this.props.intl;
if (this.props.error) { if (this.props.error) {
var errorMessage = this.props.error; let errorMessage = this.props.error;
if (this.props.error instanceof i18nRecord) { if (this.props.error instanceof i18nRecord) {
errorMessage = formatMessage(albumsMessages[this.props.error.id], this.props.error.values); errorMessage = formatMessage(albumsMessages[this.props.error.id], this.props.error.values);
} }
alert(errorMessage); alert(errorMessage);
this.context.router.replace("/"); this.context.router.replace("/");
return null; return (<div></div>);
} }
const pagination = buildPaginationObject(this.props.location, this.props.currentPage, this.props.nPages, this.props.actions.goToPageAction); const pagination = buildPaginationObject(this.props.location, this.props.currentPage, this.props.nPages, this.props.actions.goToPageAction);
return ( return (
@ -55,13 +56,22 @@ AlbumsPageIntl.propTypes = {
intl: intlShape.isRequired, intl: intlShape.isRequired,
}; };
const mapStateToProps = (state) => ({ const mapStateToProps = (state) => {
isFetching: state.pagination.albums.isFetching, let albumsList = new Immutable.List();
error: state.pagination.albums.error, let albums = state.api.result.get("album");
albumsList: state.pagination.albums.items, if (albums) {
currentPage: state.pagination.albums.currentPage, albumsList = albums.map(
nPages: state.pagination.albums.nPages id => state.api.entities.getIn(["album", id])
}); );
}
return {
isFetching: state.api.isFetching,
error: state.api.error,
albumsList: albumsList,
currentPage: state.api.currentPage,
nPages: state.api.nPages
};
};
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
actions: bindActionCreators(actionCreators, dispatch) actions: bindActionCreators(actionCreators, dispatch)

View File

@ -1,6 +1,7 @@
import React, { Component } from "react"; import React, { Component } from "react";
import { bindActionCreators } from "redux"; import { bindActionCreators } from "redux";
import { connect } from "react-redux"; import { connect } from "react-redux";
import Immutable from "immutable";
import * as actionCreators from "../actions"; import * as actionCreators from "../actions";
@ -17,21 +18,52 @@ export class ArtistPage extends Component {
} }
render () { render () {
const artist = this.props.artists.find( if (this.props.artist) {
item => item.id == this.props.params.id
);
if (artist) {
return ( return (
<Artist artist={artist} /> <Artist artist={this.props.artist} albums={this.props.albums} songs={this.props.songs} />
); );
} }
return null; // Loading return (
<div></div>
); // TODO: Loading
} }
} }
const mapStateToProps = (state) => ({ const mapStateToProps = (state, ownProps) => {
artists: state.pagination.artists.items const artists = state.api.entities.get("artist");
}); let artist = undefined;
let albums = new Immutable.List();
let songs = new Immutable.List();
if (artists) {
// Get artist
artist = artists.find(
item => item.get("id") == ownProps.params.id
);
// Get albums
const artistAlbums = artist.get("albums");
if (Immutable.List.isList(artistAlbums)) {
albums = new Immutable.Map(
artistAlbums.map(
id => [id, state.api.entities.getIn(["album", id])]
)
);
}
// Get songs
const artistSongs = artist.get("songs");
if (Immutable.List.isList(artistSongs)) {
songs = new Immutable.Map(
artistSongs.map(
id => [id, state.api.entities.getIn(["track", id])]
)
);
}
}
return {
artist: artist,
albums: albums,
songs: songs
};
};
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
actions: bindActionCreators(actionCreators, dispatch) actions: bindActionCreators(actionCreators, dispatch)

View File

@ -2,6 +2,7 @@ import React, { Component, PropTypes } from "react";
import { bindActionCreators } from "redux"; import { bindActionCreators } from "redux";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { defineMessages, injectIntl, intlShape } from "react-intl"; import { defineMessages, injectIntl, intlShape } from "react-intl";
import Immutable from "immutable";
import * as actionCreators from "../actions"; import * as actionCreators from "../actions";
import { i18nRecord } from "../models/i18n"; import { i18nRecord } from "../models/i18n";
@ -32,13 +33,13 @@ class ArtistsPageIntl extends Component {
render () { render () {
const {formatMessage} = this.props.intl; const {formatMessage} = this.props.intl;
if (this.props.error) { if (this.props.error) {
var errorMessage = this.props.error; let errorMessage = this.props.error;
if (this.props.error instanceof i18nRecord) { if (this.props.error instanceof i18nRecord) {
errorMessage = formatMessage(artistsMessages[this.props.error.id], this.props.error.values); errorMessage = formatMessage(artistsMessages[this.props.error.id], this.props.error.values);
} }
alert(errorMessage); alert(errorMessage);
this.context.router.replace("/"); this.context.router.replace("/");
return null; return (<div></div>);
} }
const pagination = buildPaginationObject(this.props.location, this.props.currentPage, this.props.nPages, this.props.actions.goToPageAction); const pagination = buildPaginationObject(this.props.location, this.props.currentPage, this.props.nPages, this.props.actions.goToPageAction);
return ( return (
@ -55,13 +56,21 @@ ArtistsPageIntl.propTypes = {
intl: intlShape.isRequired, intl: intlShape.isRequired,
}; };
const mapStateToProps = (state) => ({ const mapStateToProps = (state) => {
isFetching: state.pagination.artists.isFetching, let artistsList = new Immutable.List();
error: state.pagination.artists.error, if (state.api.result.get("artist")) {
artistsList: state.pagination.artists.items, artistsList = state.api.result.get("artist").map(
currentPage: state.pagination.artists.currentPage, id => state.api.entities.getIn(["artist", id])
nPages: state.pagination.artists.nPages, );
}); }
return {
isFetching: state.api.isFetching,
error: state.api.error,
artistsList: artistsList,
currentPage: state.api.currentPage,
nPages: state.api.nPages,
};
};
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
actions: bindActionCreators(actionCreators, dispatch) actions: bindActionCreators(actionCreators, dispatch)

View File

@ -7,8 +7,8 @@ import * as actionCreators from "../actions";
import Login from "../components/Login"; import Login from "../components/Login";
function _getRedirectTo(props) { function _getRedirectTo(props) {
var redirectPathname = "/"; let redirectPathname = "/";
var redirectQuery = {}; let redirectQuery = {};
const { location } = props; const { location } = props;
if (location.state && location.state.nextPathname) { if (location.state && location.state.nextPathname) {
redirectPathname = location.state.nextPathname; redirectPathname = location.state.nextPathname;

View File

@ -11,7 +11,7 @@ export class LogoutPage extends Component {
render () { render () {
return ( return (
null <div></div>
); );
} }
} }

View File

@ -2,6 +2,7 @@ import React, { Component, PropTypes } from "react";
import { bindActionCreators } from "redux"; import { bindActionCreators } from "redux";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { defineMessages, injectIntl, intlShape } from "react-intl"; import { defineMessages, injectIntl, intlShape } from "react-intl";
import Immutable from "immutable";
import * as actionCreators from "../actions"; import * as actionCreators from "../actions";
import { i18nRecord } from "../models/i18n"; import { i18nRecord } from "../models/i18n";
@ -32,13 +33,13 @@ class SongsPageIntl extends Component {
render () { render () {
const {formatMessage} = this.props.intl; const {formatMessage} = this.props.intl;
if (this.props.error) { if (this.props.error) {
var errorMessage = this.props.error; let errorMessage = this.props.error;
if (this.props.error instanceof i18nRecord) { if (this.props.error instanceof i18nRecord) {
errorMessage = formatMessage(songsMessages[this.props.error.id], this.props.error.values); errorMessage = formatMessage(songsMessages[this.props.error.id], this.props.error.values);
} }
alert(errorMessage); alert(errorMessage);
this.context.router.replace("/"); this.context.router.replace("/");
return null; return (<div></div>);
} }
const pagination = buildPaginationObject(this.props.location, this.props.currentPage, this.props.nPages, this.props.actions.goToPageAction); const pagination = buildPaginationObject(this.props.location, this.props.currentPage, this.props.nPages, this.props.actions.goToPageAction);
return ( return (
@ -55,13 +56,29 @@ SongsPageIntl.propTypes = {
intl: intlShape.isRequired, intl: intlShape.isRequired,
}; };
const mapStateToProps = (state) => ({ const mapStateToProps = (state) => {
isFetching: state.pagination.songs.isFetching, let songsList = new Immutable.List();
error: state.pagination.songs.error, if (state.api.result.get("song")) {
songsList: state.pagination.songs.items, songsList = state.api.result.get("song").map(function (id) {
currentPage: state.pagination.songs.currentPage, let song = state.api.entities.getIn(["track", id]);
nPages: state.pagination.songs.nPages // Add artist and album infos
}); const artist = state.api.entities.getIn(["artist", song.get("artist")]);
const album = state.api.entities.getIn(["album", song.get("album")]);
song = song.set("artist", new Immutable.Map({id: artist.get("id"), name: artist.get("name")}));
song = song.set("album", new Immutable.Map({id: album.get("id"), name: album.get("name")}));
return song;
});
}
return {
isFetching: state.api.isFetching,
error: state.api.error,
artistsList: state.api.entities.get("artist"),
albumsList: state.api.entities.get("album"),
songsList: songsList,
currentPage: state.api.currentPage,
nPages: state.api.nPages
};
};
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = (dispatch) => ({
actions: bindActionCreators(actionCreators, dispatch) actions: bindActionCreators(actionCreators, dispatch)

View File

@ -36,6 +36,7 @@
"jquery": "^3.1.0", "jquery": "^3.1.0",
"js-cookie": "^2.1.2", "js-cookie": "^2.1.2",
"jssha": "^2.1.0", "jssha": "^2.1.0",
"normalizr": "^2.2.1",
"react": "^15.3.0", "react": "^15.3.0",
"react-addons-shallow-compare": "^15.3.0", "react-addons-shallow-compare": "^15.3.0",
"react-css-modules": "^3.7.9", "react-css-modules": "^3.7.9",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,2 +1,2 @@
!function(e){function t(r){if(n[r])return n[r].exports;var a=n[r]={exports:{},id:r,loaded:!1};return e[r].call(a.exports,a,a.exports,t),a.loaded=!0,a.exports}var n={};return t.m=e,t.c=n,t.p="./",t(0)}({0:function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});var r=n(620);Object.keys(r).forEach(function(e){"default"!==e&&Object.defineProperty(t,e,{enumerable:!0,get:function(){return r[e]}})})},620: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(625);Object.keys(r).forEach(function(e){"default"!==e&&Object.defineProperty(t,e,{enumerable:!0,get:function(){return r[e]}})})},625:function(e,t){!function(t,n){function r(e,t){var n=e.createElement("p"),r=e.getElementsByTagName("head")[0]||e.documentElement;return n.innerHTML="x<style>"+t+"</style>",r.insertBefore(n.lastChild,r.firstChild)}function a(){var e=b.elements;return"string"==typeof e?e.split(" "):e}function o(e,t){var n=b.elements;"string"!=typeof n&&(n=n.join(" ")),"string"!=typeof e&&(e=e.join(" ")),b.elements=n+" "+e,s(t)}function c(e){var t=E[e[v]];return t||(t={},y++,e[v]=y,E[y]=t),t}function i(e,t,r){if(t||(t=n),f)return t.createElement(e);r||(r=c(t));var a;return a=r.cache[e]?r.cache[e].cloneNode():g.test(e)?(r.cache[e]=r.createElem(e)).cloneNode():r.createElem(e),!a.canHaveChildren||p.test(e)||a.tagUrn?a:r.frag.appendChild(a)}function l(e,t){if(e||(e=n),f)return e.createDocumentFragment();t=t||c(e);for(var r=t.frag.cloneNode(),o=0,i=a(),l=i.length;o<l;o++)r.createElement(i[o]);return r}function u(e,t){t.cache||(t.cache={},t.createElem=e.createElement,t.createFrag=e.createDocumentFragment,t.frag=t.createFrag()),e.createElement=function(n){return b.shivMethods?i(n,e,t):t.createElem(n)},e.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+a().join().replace(/[\w\-:]+/g,function(e){return t.createElem(e),t.frag.createElement(e),'c("'+e+'")'})+");return n}")(b,t.frag)}function s(e){e||(e=n);var t=c(e);return!b.shivCSS||d||t.hasCSS||(t.hasCSS=!!r(e,"article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}mark{background:#FF0;color:#000}template{display:none}")),f||u(e,t),e}var d,f,m="3.7.3-pre",h=t.html5||{},p=/^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i,g=/^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i,v="_html5shiv",y=0,E={};!function(){try{var e=n.createElement("a");e.innerHTML="<xyz></xyz>",d="hidden"in e,f=1==e.childNodes.length||function(){n.createElement("a");var e=n.createDocumentFragment();return"undefined"==typeof e.cloneNode||"undefined"==typeof e.createDocumentFragment||"undefined"==typeof e.createElement}()}catch(t){d=!0,f=!0}}();var b={elements:h.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output picture progress section summary template time video",version:m,shivCSS:h.shivCSS!==!1,supportsUnknownElements:f,shivMethods:h.shivMethods!==!1,type:"default",shivDocument:s,createElement:i,createDocumentFragment:l,addElements:o};t.html5=b,s(n),"object"==typeof e&&e.exports&&(e.exports=b)}("undefined"!=typeof window?window:this,document)}});
//# sourceMappingURL=fix.ie9.js.map //# sourceMappingURL=fix.ie9.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long