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
6. Homepage
7. Settings
8. Search
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 { 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 default function (action, requestType, successType, failureType) {
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 {
type: successType,
payload: {
items: itemsList,
result: jsonData.result,
entities: jsonData.entities,
nPages: nPages,
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 offset = (pageNumber - 1) * DEFAULT_LIMIT;
var extraParams = {
let extraParams = {
offset: offset,
limit: limit
};
@ -51,7 +76,7 @@ export default function (action, requestType, successType, failureType) {
dispatch: [
fetchItemsRequest,
jsonData => dispatch => {
dispatch(fetchItemsSuccess(jsonData[itemName], jsonData.totalCount, pageNumber));
dispatch(fetchItemsSuccess(jsonData, pageNumber));
},
fetchItemsFailure
],

View File

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

View File

@ -2,20 +2,12 @@ export * from "./auth";
import APIAction from "./APIActions";
export const ARTISTS_SUCCESS = "ARTISTS_SUCCESS";
export const ARTISTS_REQUEST = "ARTISTS_REQUEST";
export const ARTISTS_FAILURE = "ARTISTS_FAILURE";
export var { loadArtists } = APIAction("artists", ARTISTS_REQUEST, ARTISTS_SUCCESS, ARTISTS_FAILURE);
export const ALBUMS_SUCCESS = "ALBUMS_SUCCESS";
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 const API_SUCCESS = "API_SUCCESS";
export const API_REQUEST = "API_REQUEST";
export const API_FAILURE = "API_FAILURE";
export var { loadArtists } = APIAction("artists", API_REQUEST, API_SUCCESS, API_FAILURE);
export var { loadAlbums } = APIAction("albums", API_REQUEST, API_SUCCESS, API_FAILURE);
export var { loadSongs } = APIAction("songs", API_REQUEST, API_SUCCESS, API_FAILURE);
export * from "./paginate";
export * from "./store";

View File

@ -13,7 +13,7 @@ const albumMessages = defineMessages(messagesMap(commonMessages));
class AlbumTrackRowCSS extends Component {
render () {
const length = formatLength(this.props.track.time);
const length = formatLength(this.props.track.get("time"));
return (
<tr>
<td>
@ -24,14 +24,15 @@ class AlbumTrackRowCSS extends Component {
<FontAwesome name="play-circle-o" aria-hidden="true" />
</button>
</td>
<td>{this.props.track.track}</td>
<td>{this.props.track.name}</td>
<td>{this.props.track.get("track")}</td>
<td>{this.props.track.get("name")}</td>
<td>{length}</td>
</tr>
);
}
}
// TODO: Not object
AlbumTrackRowCSS.propTypes = {
track: PropTypes.object.isRequired
};
@ -41,9 +42,9 @@ export let AlbumTrackRow = CSSModules(AlbumTrackRowCSS, css);
class AlbumTracksTableCSS extends Component {
render () {
var rows = [];
let rows = [];
this.props.tracks.forEach(function (item) {
rows.push(<AlbumTrackRow track={item} key={item.id} />);
rows.push(<AlbumTrackRow track={item} key={item.get("id")} />);
});
return (
<table className="table table-hover" styleName="songs">
@ -55,8 +56,9 @@ class AlbumTracksTableCSS extends Component {
}
}
// TODO: Not object
AlbumTracksTableCSS.propTypes = {
tracks: PropTypes.array.isRequired
tracks: PropTypes.object.isRequired
};
export let AlbumTracksTable = CSSModules(AlbumTracksTableCSS, css);
@ -66,15 +68,15 @@ class AlbumRowCSS extends Component {
return (
<div className="row" styleName="row">
<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 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 className="col-xs-9 col-sm-10 table-responsive">
{
Array.isArray(this.props.album.tracks) ?
<AlbumTracksTable tracks={this.props.album.tracks} /> :
this.props.songs.size > 0 ?
<AlbumTracksTable tracks={this.props.songs} /> :
null
}
</div>
@ -83,8 +85,10 @@ class AlbumRowCSS extends Component {
}
}
// TODO: Not object
AlbumRowCSS.propTypes = {
album: PropTypes.object.isRequired
album: PropTypes.object.isRequired,
songs: PropTypes.object.isRequired
};
export let AlbumRow = CSSModules(AlbumRowCSS, css);
@ -92,11 +96,13 @@ export let AlbumRow = CSSModules(AlbumRowCSS, css);
export default class Album extends Component {
render () {
return (
<AlbumRow album={this.props.album} />
<AlbumRow album={this.props.album} songs={this.props.songs} />
);
}
}
// TODO: Not object
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 {
render () {
var albumsRows = [];
if (Array.isArray(this.props.artist.albums)) {
this.props.artist.albums.forEach(function (item) {
albumsRows.push(<AlbumRow album={item} key={item.id} />);
let albumsRows = [];
if (this.props.artist.get("albums").size > 0) {
const artistAlbums = this.props.albums;
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 (
<div>
<div className="row" styleName="name">
<div className="col-sm-12">
<h1>{this.props.artist.name}</h1>
<h1>{this.props.artist.get("name")}</h1>
<hr/>
</div>
</div>
<div className="row">
<div className="col-sm-9">
<p>{this.props.artist.summary}</p>
<p>{this.props.artist.get("summary")}</p>
</div>
<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>
{ albumsRows }
@ -35,8 +41,11 @@ class ArtistCSS extends Component {
}
}
// TODO: Not object
ArtistCSS.propTypes = {
artist: PropTypes.object.isRequired
artist: PropTypes.object.isRequired,
albums: PropTypes.object.isRequired,
songs: PropTypes.object.isRequired
};
export default CSSModules(ArtistCSS, css);

View File

@ -41,7 +41,7 @@ class LoginFormCSSIntl extends Component {
const endpoint = this.refs.endpoint.value.trim();
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.endpointFormGroup, !endpoint);
@ -61,13 +61,13 @@ class LoginFormCSSIntl extends Component {
render () {
const {formatMessage} = this.props.intl;
var infoMessage = this.props.info;
let infoMessage = this.props.info;
if (this.props.info && this.props.info instanceof i18nRecord) {
infoMessage = (
<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) {
errorMessage = (
<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 {
render () {
const length = formatLength(this.props.song.time);
const linkToArtist = "/artist/" + this.props.song.artist.id;
const linkToAlbum = "/album/" + this.props.song.album.id;
const length = formatLength(this.props.song.get("time"));
const linkToArtist = "/artist/" + this.props.song.getIn(["artist", "id"]);
const linkToAlbum = "/album/" + this.props.song.getIn(["album", "id"]);
return (
<tr>
<td>
@ -32,10 +32,10 @@ class SongsTableRowCSS extends Component {
<FontAwesome name="play-circle-o" aria-hidden="true" />
</button>
</td>
<td className="title">{this.props.song.name}</td>
<td className="artist"><Link to={linkToArtist}>{this.props.song.artist.name}</Link></td>
<td className="album"><Link to={linkToAlbum}>{this.props.song.album.name}</Link></td>
<td className="genre">{this.props.song.genre}</td>
<td className="title">{this.props.song.get("name")}</td>
<td className="artist"><Link to={linkToArtist}>{this.props.song.getIn(["artist", "name"])}</Link></td>
<td className="album"><Link to={linkToAlbum}>{this.props.song.getIn(["album", "name"])}</Link></td>
<td className="genre">{this.props.song.get("genre")}</td>
<td className="length">{length}</td>
</tr>
);
@ -51,7 +51,7 @@ export let SongsTableRow = CSSModules(SongsTableRowCSS, css);
class SongsTableCSS extends Component {
render () {
var displayedSongs = this.props.songs;
let displayedSongs = this.props.songs;
if (this.props.filterText) {
// Use Fuse for the filter
displayedSongs = new Fuse(
@ -65,11 +65,11 @@ class SongsTableCSS extends Component {
displayedSongs = displayedSongs.map(function (item) { return item.item; });
}
var rows = [];
let rows = [];
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 we are fetching and there is nothing to show
loading = (

View File

@ -24,22 +24,22 @@ class GridItemCSSIntl extends Component {
render () {
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)) {
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 id = "grid-item-" + 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.get("id");
const title = formatMessage(gridMessages["app.grid.goTo" + this.props.itemsType.capitalize() + "Page"]);
return (
<div className="grid-item col-xs-6 col-sm-3" styleName="placeholders" id={id}>
<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>
<h4 className="name" styleName="name">{this.props.item.name}</h4>
<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.get("name")}</h4>
<span className="sub-items text-muted"><span className="n-sub-items">{nSubItems}</span> <span className="sub-items-type">{subItemsLabel}</span></span>
</div>
</div>
@ -97,7 +97,7 @@ export class Grid extends Component {
return this.iso.arrange(ISOTOPE_OPTIONS);
}
// Use Fuse for the filter
var result = new Fuse(
let result = new Fuse(
props.items.toArray(),
{
"keys": ["name"],
@ -108,13 +108,13 @@ export class Grid extends Component {
// Apply filter on grid
this.iso.arrange({
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; });
},
transitionDuration: "0.4s",
getSortData: {
relevance: function (item) {
var name = $(item).find(".name").text();
let name = $(item).find(".name").text();
return result.reduce(function (p, c) {
if (c.item.name == name) {
return c.score + p;
@ -152,12 +152,12 @@ export class Grid extends Component {
componentDidUpdate(prevProps) {
// The list of keys seen in the previous render
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
const {itemsType} = this.props;
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
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
let removeKeys = immutableDiff(currentKeys, newKeys);
var iso = this.iso;
let iso = this.iso;
if (removeKeys.count() > 0) {
removeKeys.forEach(removeKey => iso.remove(document.getElementById(removeKey)));
iso.arrange();
@ -187,12 +187,12 @@ export class Grid extends Component {
}
render () {
var gridItems = [];
let gridItems = [];
const { itemsType, itemsLabel, subItemsType, subItemsLabel } = this.props;
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) {
loading = (
<div className="row text-center">

View File

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

View File

@ -1,54 +1,86 @@
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 { messagesMap } from "../../utils";
import css from "../../styles/elements/WebPlayer.scss";
class WebPlayerCSS extends Component {
componentDidMount () {
// TODO: Should be in the container mounting WebPlayer
$(".sidebar").css("bottom", "15vh");
$(".main-panel").css("margin-bottom", "15vh");
import commonMessages from "../../locales/messagesDescriptors/common";
import messages from "../../locales/messagesDescriptors/elements/WebPlayer";
const webplayerMessages = defineMessages(messagesMap(Array.concat([], commonMessages, messages)));
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 () {
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 (
<div id="row">
<div id="webplayer" className="col-xs-12" styleName="body">
{ /* Top Info */ }
<div id="title" styleName="title">
<span id="track">Foobar</span>
<div id="timer" styleName="timer">0:00</div>
<div id="duration" styleName="duration">0:00</div>
<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={this.props.song.art} width="200" height="200" ref="art" styleName="art" />
<h2>{this.props.song.title}</h2>
<h3>
<span className="text-capitalize">
<FormattedMessage {...webplayerMessages["app.webplayer.by"]} />
</span> {this.props.song.artist}
</h3>
</div>
</div>
{ /* Controls */ }
<div styleName="controlsOuter">
<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 className="row text-center" styleName="controls">
<div className="col-xs-12">
<button styleName="prevBtn" aria-label={formatMessage(webplayerMessages["app.webplayer.previous"])} title={formatMessage(webplayerMessages["app.webplayer.previous"])}>
<FontAwesome name="step-backward" />
</button>
<button className="play" styleName="playPauseBtn" aria-label={formatMessage(webplayerMessages["app.common." + playPause])} title={formatMessage(webplayerMessages["app.common." + playPause])}>
<FontAwesome name={playPause} />
</button>
<button styleName="nextBtn" aria-label={formatMessage(webplayerMessages["app.webplayer.next"])} title={formatMessage(webplayerMessages["app.webplayer.next"])}>
<FontAwesome name="step-forward" />
</button>
</div>
<div id="playlistBtn" styleName="playlistBtn"></div>
<div id="volumeBtn" styleName="volumeBtn"></div>
<div className="col-xs-12">
<button styleName="volumeBtn" aria-label={formatMessage(webplayerMessages["app.webplayer.volume"])} title={formatMessage(webplayerMessages["app.webplayer.volume"])}>
<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>
{ /* Progress */ }
<div id="waveform" styleName="waveform"></div>
<div id="bar" styleName="progressBar"></div>
<div id="progress" styleName="progress"></div>
{ /* Playlist */ }
<div id="playlist" styleName="playlist">
<div id="list" styleName="list"></div>
</div>
{ /* Volume */ }
<div id="volume" styleName="volume-fadeout">
<div id="barFull" styleName="barFull"></div>
<div id="barEmpty" styleName="barEmpty"></div>
<div id="sliderBtn" styleName="sliderBtn"></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 messages from "../../locales/messagesDescriptors/layouts/Sidebar";
import WebPlayer from "../elements/WebPlayer";
import WebPlayer from "../../containers/WebPlayer";
import css from "../../styles/layouts/Sidebar.scss";
@ -138,13 +138,13 @@ class SidebarLayoutIntl extends Component {
</li>
</ul>
</nav>
<WebPlayer />
</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">
{this.props.children}
</div>
{ /* TODO <WebPlayer /> */ }
</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.go": "Go", // Go
"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.filter.filter": "Filter…", // Filtering input placeholder
"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.length": "Length", // Length (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.go": "Aller", // Go
"app.common.loading": "Chargement…", // Loading indicator
"app.common.pause": "Pause", // Pause icon description
"app.common.play": "Jouer", // PLay icon description
"app.common.track": "{itemCount, plural, one {piste} other {pistes}}", // Track
"app.filter.filter": "Filtrer…", // Filtering input placeholder
@ -43,4 +44,11 @@ module.exports = {
"app.songs.genre": "Genre", // Genre (song)
"app.songs.length": "Durée", // Length (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",
description: "PLay icon description",
description: "Play icon description",
defaultMessage: "Play"
},
{
id: "app.common.pause",
description: "Pause icon description",
defaultMessage: "Pause"
},
];
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) {
var x2js = new X2JS({
let x2js = new X2JS({
attributePrefix: "",
keepCData: false
});
@ -47,7 +47,7 @@ function _checkAPIErrors (jsonData) {
}
function _uglyFixes (jsonData) {
var _uglyFixesSongs = function (songs) {
let _uglyFixesSongs = function (songs) {
return songs.map(function (song) {
// Fix for cdata left in artist and album
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) {
// TODO
// 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) {
// Move albums one node top
if (artist.albums.album) {
@ -149,11 +149,6 @@ function _uglyFixes (jsonData) {
return jsonData;
}
function _normalizeResponse(jsonData) {
// TODO
return jsonData;
}
// 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.
function doAPICall (endpoint, action, auth, username, extraParams) {
@ -175,8 +170,7 @@ function doAPICall (endpoint, action, auth, username, extraParams) {
.then(_parseToJSON)
.then(_checkAPIErrors)
.then(jsonData => humps.camelizeKeys(jsonData)) // Camelize
.then(_uglyFixes)
.then(_normalizeResponse);
.then(_uglyFixes);
}
// 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({
isFetching: false,
items: new Immutable.List(),
result: new Immutable.Map(),
entities: new Immutable.Map(),
error: null,
currentPage: 1,
nPages: 1

View File

@ -7,28 +7,16 @@ import paginate from "./paginate";
import * as ActionTypes from "../actions";
// Updates the pagination data for different actions.
const pagination = combineReducers({
artists: paginate([
ActionTypes.ARTISTS_REQUEST,
ActionTypes.ARTISTS_SUCCESS,
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 api = paginate([
ActionTypes.API_REQUEST,
ActionTypes.API_SUCCESS,
ActionTypes.API_FAILURE
]);
const rootReducer = combineReducers({
routing,
auth,
pagination
api
});
export default rootReducer;

View File

@ -30,7 +30,8 @@ export default function paginate(types) {
return (
state
.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("nPages", payload.nPages)
.set("currentPage", payload.currentPage)

View File

@ -1,340 +1,51 @@
.body {
height: 15vh;
background: #bb71f3;
background: linear-gradient(135deg, #bb71f3 0%, #3d4d91 100%);
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;
$controlsMarginTop: 10px;
.webplayer {
margin-top: 1em;
}
/* Top Info */
.title {
.art {
opacity: 0.75;
position: absolute;
width: 100%;
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);
z-index: -10;
}
.timer {
position: absolute;
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);
.artRow {
min-height: 200px;
}
.duration {
position: absolute;
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 {
position: absolute;
width: 100%;
height: 70px;
bottom: 3%;
}
.controlsInner {
position: absolute;
width: 340px;
height: 70px;
left: 50%;
margin: 0 -170px;
.controls {
margin-top: $controlsMarginTop;
}
.btn {
position: absolute;
cursor: pointer;
opacity: 0.9;
-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;
background: transparent;
border: none;
opacity: 0.8;
}
.btn:hover {
opacity: 1;
}
.playBtn {
composes: btn;
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEUAAABGCAYAAACaGVmHAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTExIDc5LjE1ODMyNSwgMjAxNS8wOS8xMC0wMToxMDoyMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6QThERThENUFDNkJEMTFFNTk2RTBDMTRBMkYyNjVGMzQiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6QThERThENUJDNkJEMTFFNTk2RTBDMTRBMkYyNjVGMzQiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpBOERFOEQ1OEM2QkQxMUU1OTZFMEMxNEEyRjI2NUYzNCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpBOERFOEQ1OUM2QkQxMUU1OTZFMEMxNEEyRjI2NUYzNCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PnFM7yIAAAFWSURBVHja7NRJDoMwDEDRcPL2ZoVeLEUtCyoCZPBsf8mS10+Wp5zzK6X0XGdJ0bdpRcnbPgfOESUFzjmKe5wrFLc4NSjucFpQ3OD0oJjHGUExiwOBYg4HEsUMDgaKehxMFLU4FCjqcChR1OBwoIjH4UQRiyMBRRyOJBQxOBJR2HEko7DhaEAhx9GEQoajEQUdRzMKGo4FFHAcSyhgOBZRhnEso3TjeEBpxvGEUo3jEeUWxzPKKU6gFHACpYATKMfegbLDWOcRl/Jr2X7JHI+2gOEZ5RTDI8othieUagwPKM0YllG6MSyiDGNYQgHDsIACjqEZBQ1DIwo6hiYUMgwNKOQYklHYMCSisGNIQhGDIQFFHAYnilgMDhTxGJQoajAoUNRhYKKoxcBAUY8BiWIGAwLFHMYIilmMHhTzGC0objBqUNxhXKG4xSihuMfYo8yB8d9HgAEATyefBJWpw70AAAAASUVORK5CYII=');
width: 69px;
height: 70px;
left: 50%;
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;
}
.prevBtn,
.playPauseBtn,
.nextBtn,
.volumeBtn,
.repeatBtn,
.randomBtn,
.playlistBtn {
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 {
composes: btn;
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;
.playPauseBtn {
font-size: $font-size-h2;
}
/* Progress */
.waveform {
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;
.active {
color: $blue;
}

View File

@ -1,3 +1,6 @@
// 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/_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.
*/
String.prototype.rstrip = function (chars) {
var regex = new RegExp(chars + "$");
let regex = new RegExp(chars + "$");
return this.replace(regex, "");
};

View File

@ -1,5 +1,5 @@
export function getBrowserLocales () {
var langs;
let langs;
if (navigator.languages) {
// 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
var locales = langs.map(function (lang) {
var locale = lang.split("-");
let locales = langs.map(function (lang) {
let locale = lang.split("-");
return locale[1] ? `${locale[0]}-${locale[1].toUpperCase()}` : lang;
});
@ -23,7 +23,7 @@ export function getBrowserLocales () {
}
export function messagesMap(messagesDescriptorsArray) {
var messagesDescriptorsMap = {};
let messagesDescriptorsMap = {};
messagesDescriptorsArray.forEach(function (item) {
messagesDescriptorsMap[item.id] = item;

View File

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

View File

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

View File

@ -1,6 +1,7 @@
import React, { Component } from "react";
import { bindActionCreators } from "redux";
import { connect } from "react-redux";
import Immutable from "immutable";
import * as actionCreators from "../actions";
@ -19,21 +20,41 @@ export class AlbumPage extends Component {
}
render () {
const album = this.props.albums.find(
item => item.id == this.props.params.id
);
if (album) {
if (this.props.album) {
return (
<Album album={album} />
<Album album={this.props.album} songs={this.props.songs} />
);
}
return null; // Loading
return (
<div></div>
); // TODO: Loading
}
}
const mapStateToProps = (state) => ({
albums: state.pagination.albums.items
});
const mapStateToProps = (state, ownProps) => {
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) => ({
actions: bindActionCreators(actionCreators, dispatch)

View File

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

View File

@ -1,6 +1,7 @@
import React, { Component } from "react";
import { bindActionCreators } from "redux";
import { connect } from "react-redux";
import Immutable from "immutable";
import * as actionCreators from "../actions";
@ -17,21 +18,52 @@ export class ArtistPage extends Component {
}
render () {
const artist = this.props.artists.find(
item => item.id == this.props.params.id
);
if (artist) {
if (this.props.artist) {
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) => ({
artists: state.pagination.artists.items
});
const mapStateToProps = (state, ownProps) => {
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) => ({
actions: bindActionCreators(actionCreators, dispatch)

View File

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

View File

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

View File

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

View File

@ -2,6 +2,7 @@ import React, { Component, PropTypes } from "react";
import { bindActionCreators } from "redux";
import { connect } from "react-redux";
import { defineMessages, injectIntl, intlShape } from "react-intl";
import Immutable from "immutable";
import * as actionCreators from "../actions";
import { i18nRecord } from "../models/i18n";
@ -32,13 +33,13 @@ class SongsPageIntl extends Component {
render () {
const {formatMessage} = this.props.intl;
if (this.props.error) {
var errorMessage = this.props.error;
let errorMessage = this.props.error;
if (this.props.error instanceof i18nRecord) {
errorMessage = formatMessage(songsMessages[this.props.error.id], this.props.error.values);
}
alert(errorMessage);
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);
return (
@ -55,13 +56,29 @@ SongsPageIntl.propTypes = {
intl: intlShape.isRequired,
};
const mapStateToProps = (state) => ({
isFetching: state.pagination.songs.isFetching,
error: state.pagination.songs.error,
songsList: state.pagination.songs.items,
currentPage: state.pagination.songs.currentPage,
nPages: state.pagination.songs.nPages
const mapStateToProps = (state) => {
let songsList = new Immutable.List();
if (state.api.result.get("song")) {
songsList = state.api.result.get("song").map(function (id) {
let song = state.api.entities.getIn(["track", id]);
// 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) => ({
actions: bindActionCreators(actionCreators, dispatch)

View File

@ -36,6 +36,7 @@
"jquery": "^3.1.0",
"js-cookie": "^2.1.2",
"jssha": "^2.1.0",
"normalizr": "^2.2.1",
"react": "^15.3.0",
"react-addons-shallow-compare": "^15.3.0",
"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

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