Finish API refactor, use normalizr + immutable
This commit is contained in:
parent
288039e732
commit
b73b4ba200
6
TODO
6
TODO
@ -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
|
|
||||||
|
@ -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
|
||||||
],
|
],
|
||||||
|
@ -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
|
||||||
|
@ -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";
|
||||||
|
@ -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
|
||||||
};
|
};
|
||||||
|
@ -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);
|
||||||
|
@ -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} />
|
||||||
|
@ -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 = (
|
||||||
|
@ -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">
|
||||||
|
@ -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>;
|
||||||
|
@ -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>
|
||||||
|
<span className="text-capitalize">
|
||||||
|
<FormattedMessage {...webplayerMessages["app.webplayer.by"]} />
|
||||||
|
</span> {this.props.song.artist}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{ /* Controls */ }
|
<div className="row text-center" styleName="controls">
|
||||||
<div styleName="controlsOuter">
|
<div className="col-xs-12">
|
||||||
<div styleName="controlsInner">
|
<button styleName="prevBtn" aria-label={formatMessage(webplayerMessages["app.webplayer.previous"])} title={formatMessage(webplayerMessages["app.webplayer.previous"])}>
|
||||||
<div id="loading" styleName="loading"></div>
|
<FontAwesome name="step-backward" />
|
||||||
<div id="playBtn" styleName="playBtn"></div>
|
</button>
|
||||||
<div id="pauseBtn" styleName="pauseBtn"></div>
|
<button className="play" styleName="playPauseBtn" aria-label={formatMessage(webplayerMessages["app.common." + playPause])} title={formatMessage(webplayerMessages["app.common." + playPause])}>
|
||||||
<div id="prevBtn" styleName="prevBtn"></div>
|
<FontAwesome name={playPause} />
|
||||||
<div id="nextBtn" styleName="nextBtn"></div>
|
</button>
|
||||||
|
<button styleName="nextBtn" aria-label={formatMessage(webplayerMessages["app.webplayer.next"])} title={formatMessage(webplayerMessages["app.webplayer.next"])}>
|
||||||
|
<FontAwesome name="step-forward" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="playlistBtn" styleName="playlistBtn"></div>
|
<div className="col-xs-12">
|
||||||
<div id="volumeBtn" styleName="volumeBtn"></div>
|
<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>
|
</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>
|
</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 }));
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
21
app/containers/WebPlayer.jsx
Normal file
21
app/containers/WebPlayer.jsx
Normal 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} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
};
|
};
|
||||||
|
@ -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
|
||||||
};
|
};
|
||||||
|
@ -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;
|
||||||
|
39
app/locales/messagesDescriptors/elements/WebPlayer.js
Normal file
39
app/locales/messagesDescriptors/elements/WebPlayer.js
Normal 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;
|
@ -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
22
app/models/api.js
Normal 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
|
||||||
|
});
|
@ -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
|
||||||
|
@ -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;
|
||||||
|
@ -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)
|
||||||
|
@ -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;
|
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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, "");
|
||||||
};
|
};
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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])) {
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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;
|
||||||
|
@ -11,7 +11,7 @@ export class LogoutPage extends Component {
|
|||||||
|
|
||||||
render () {
|
render () {
|
||||||
return (
|
return (
|
||||||
null
|
<div></div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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
@ -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
Loading…
Reference in New Issue
Block a user