parent
07b37c1575
commit
25eef250a4
@ -0,0 +1,160 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import LazyLoad from 'react-lazyload';
|
||||||
|
|
||||||
|
const bannerPlaceholder = 'data:image/png;base64,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';
|
||||||
|
|
||||||
|
function findBanner(images) {
|
||||||
|
return _.find(images, { coverType: 'banner' });
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBannerUrl(banner, size) {
|
||||||
|
if (banner) {
|
||||||
|
// Remove protocol
|
||||||
|
let url = banner.url.replace(/^https?:/, '');
|
||||||
|
url = url.replace('banner.jpg', `banner-${size}.jpg`);
|
||||||
|
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ArtistBanner extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
const pixelRatio = Math.floor(window.devicePixelRatio);
|
||||||
|
|
||||||
|
const {
|
||||||
|
images,
|
||||||
|
size
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const banner = findBanner(images);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
pixelRatio,
|
||||||
|
banner,
|
||||||
|
bannerUrl: getBannerUrl(banner, pixelRatio * size),
|
||||||
|
hasError: false,
|
||||||
|
isLoaded: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
const {
|
||||||
|
images,
|
||||||
|
size
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
pixelRatio
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const banner = findBanner(images);
|
||||||
|
|
||||||
|
if (banner && banner.url !== this.state.banner.url) {
|
||||||
|
this.setState({
|
||||||
|
banner,
|
||||||
|
bannerUrl: getBannerUrl(banner, pixelRatio * size),
|
||||||
|
hasError: false,
|
||||||
|
isLoaded: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onError = () => {
|
||||||
|
this.setState({ hasError: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
onLoad = () => {
|
||||||
|
this.setState({ isLoaded: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
className,
|
||||||
|
style,
|
||||||
|
size,
|
||||||
|
lazy,
|
||||||
|
overflow
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
bannerUrl,
|
||||||
|
hasError,
|
||||||
|
isLoaded
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
if (hasError || !bannerUrl) {
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
className={className}
|
||||||
|
style={style}
|
||||||
|
src={bannerPlaceholder}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lazy) {
|
||||||
|
return (
|
||||||
|
<LazyLoad
|
||||||
|
height={size}
|
||||||
|
offset={100}
|
||||||
|
overflow={overflow}
|
||||||
|
placeholder={
|
||||||
|
<img
|
||||||
|
className={className}
|
||||||
|
style={style}
|
||||||
|
src={bannerPlaceholder}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
className={className}
|
||||||
|
style={style}
|
||||||
|
src={bannerUrl}
|
||||||
|
onError={this.onError}
|
||||||
|
/>
|
||||||
|
</LazyLoad>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
className={className}
|
||||||
|
style={style}
|
||||||
|
src={isLoaded ? bannerUrl : bannerPlaceholder}
|
||||||
|
onError={this.onError}
|
||||||
|
onLoad={this.onLoad}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ArtistBanner.propTypes = {
|
||||||
|
className: PropTypes.string,
|
||||||
|
style: PropTypes.object,
|
||||||
|
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
size: PropTypes.number.isRequired,
|
||||||
|
lazy: PropTypes.bool.isRequired,
|
||||||
|
overflow: PropTypes.bool.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
ArtistBanner.defaultProps = {
|
||||||
|
size: 70,
|
||||||
|
lazy: true,
|
||||||
|
overflow: false
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ArtistBanner;
|
@ -0,0 +1,160 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import LazyLoad from 'react-lazyload';
|
||||||
|
|
||||||
|
const logoPlaceholder = 'data:image/png;base64,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';
|
||||||
|
|
||||||
|
function findLogo(images) {
|
||||||
|
return _.find(images, { coverType: 'logo' });
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLogoUrl(logo, size) {
|
||||||
|
if (logo) {
|
||||||
|
// Remove protocol
|
||||||
|
let url = logo.url.replace(/^https?:/, '');
|
||||||
|
url = url.replace('logo.jpg', `logo-${size}.jpg`);
|
||||||
|
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ArtistLogo extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
const pixelRatio = Math.floor(window.devicePixelRatio);
|
||||||
|
|
||||||
|
const {
|
||||||
|
images,
|
||||||
|
size
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const logo = findLogo(images);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
pixelRatio,
|
||||||
|
logo,
|
||||||
|
logoUrl: getLogoUrl(logo, pixelRatio * size),
|
||||||
|
hasError: false,
|
||||||
|
isLoaded: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
const {
|
||||||
|
images,
|
||||||
|
size
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
pixelRatio
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const logo = findLogo(images);
|
||||||
|
|
||||||
|
if (logo && logo.url !== this.state.logo.url) {
|
||||||
|
this.setState({
|
||||||
|
logo,
|
||||||
|
logoUrl: getLogoUrl(logo, pixelRatio * size),
|
||||||
|
hasError: false,
|
||||||
|
isLoaded: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onError = () => {
|
||||||
|
this.setState({ hasError: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
onLoad = () => {
|
||||||
|
this.setState({ isLoaded: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
className,
|
||||||
|
style,
|
||||||
|
size,
|
||||||
|
lazy,
|
||||||
|
overflow
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
logoUrl,
|
||||||
|
hasError,
|
||||||
|
isLoaded
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
if (hasError || !logoUrl) {
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
className={className}
|
||||||
|
style={style}
|
||||||
|
src={logoPlaceholder}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lazy) {
|
||||||
|
return (
|
||||||
|
<LazyLoad
|
||||||
|
height={size}
|
||||||
|
offset={100}
|
||||||
|
overflow={overflow}
|
||||||
|
placeholder={
|
||||||
|
<img
|
||||||
|
className={className}
|
||||||
|
style={style}
|
||||||
|
src={logoPlaceholder}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
className={className}
|
||||||
|
style={style}
|
||||||
|
src={logoUrl}
|
||||||
|
onError={this.onError}
|
||||||
|
/>
|
||||||
|
</LazyLoad>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
className={className}
|
||||||
|
style={style}
|
||||||
|
src={isLoaded ? logoUrl : logoPlaceholder}
|
||||||
|
onError={this.onError}
|
||||||
|
onLoad={this.onLoad}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ArtistLogo.propTypes = {
|
||||||
|
className: PropTypes.string,
|
||||||
|
style: PropTypes.object,
|
||||||
|
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
size: PropTypes.number.isRequired,
|
||||||
|
lazy: PropTypes.bool.isRequired,
|
||||||
|
overflow: PropTypes.bool.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
ArtistLogo.defaultProps = {
|
||||||
|
size: 250,
|
||||||
|
lazy: true,
|
||||||
|
overflow: false
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ArtistLogo;
|
@ -0,0 +1,89 @@
|
|||||||
|
$hoverScale: 1.05;
|
||||||
|
|
||||||
|
.container {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
transition: all 200ms ease-in;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
z-index: 2;
|
||||||
|
box-shadow: 0 0 12px $black;
|
||||||
|
transition: all 200ms ease-in;
|
||||||
|
|
||||||
|
// Transforming causes the content to shift slightly
|
||||||
|
// transform: scale($hoverScale);
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
opacity: 0.9;
|
||||||
|
transition: opacity 200ms linear 150ms;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bannerContainer {
|
||||||
|
position: relative;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link {
|
||||||
|
composes: link from 'Components/Link/Link.css';
|
||||||
|
|
||||||
|
display: block;
|
||||||
|
background-color: $defaultColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nextAiring {
|
||||||
|
background-color: $defaultColor;
|
||||||
|
color: $white;
|
||||||
|
text-align: center;
|
||||||
|
font-size: $smallFontSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
composes: truncate from 'Styles/mixins/truncate.css';
|
||||||
|
|
||||||
|
background-color: $defaultColor;
|
||||||
|
color: $white;
|
||||||
|
text-align: center;
|
||||||
|
font-size: $smallFontSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ended {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-width: 0 25px 25px 0;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: transparent $dangerColor transparent transparent;
|
||||||
|
color: $white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 10px;
|
||||||
|
left: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: #216044;
|
||||||
|
color: $white;
|
||||||
|
font-size: $smallFontSize;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action {
|
||||||
|
composes: button from 'Components/Link/IconButton.css';
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: $breakpointSmall) {
|
||||||
|
.container {
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,230 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import IconButton from 'Components/Link/IconButton';
|
||||||
|
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||||
|
import Label from 'Components/Label';
|
||||||
|
import Link from 'Components/Link/Link';
|
||||||
|
import ArtistBanner from 'Artist/ArtistBanner';
|
||||||
|
import EditArtistModalConnector from 'Artist/Edit/EditArtistModalConnector';
|
||||||
|
import DeleteArtistModal from 'Artist/Delete/DeleteArtistModal';
|
||||||
|
import ArtistIndexBannerProgressBar from './ArtistIndexBannerProgressBar';
|
||||||
|
import ArtistIndexBannerInfo from './ArtistIndexBannerInfo';
|
||||||
|
import styles from './ArtistIndexBanner.css';
|
||||||
|
|
||||||
|
class ArtistIndexBanner extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isEditArtistModalOpen: false,
|
||||||
|
isDeleteArtistModalOpen: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onEditArtistPress = () => {
|
||||||
|
this.setState({ isEditArtistModalOpen: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
onEditArtistModalClose = () => {
|
||||||
|
this.setState({ isEditArtistModalOpen: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
onDeleteArtistPress = () => {
|
||||||
|
this.setState({
|
||||||
|
isEditArtistModalOpen: false,
|
||||||
|
isDeleteArtistModalOpen: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onDeleteArtistModalClose = () => {
|
||||||
|
this.setState({ isDeleteArtistModalOpen: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
style,
|
||||||
|
id,
|
||||||
|
artistName,
|
||||||
|
monitored,
|
||||||
|
status,
|
||||||
|
nameSlug,
|
||||||
|
nextAiring,
|
||||||
|
trackCount,
|
||||||
|
trackFileCount,
|
||||||
|
images,
|
||||||
|
bannerWidth,
|
||||||
|
bannerHeight,
|
||||||
|
detailedProgressBar,
|
||||||
|
showTitle,
|
||||||
|
showQualityProfile,
|
||||||
|
qualityProfile,
|
||||||
|
showRelativeDates,
|
||||||
|
shortDateFormat,
|
||||||
|
timeFormat,
|
||||||
|
isRefreshingArtist,
|
||||||
|
onRefreshArtistPress,
|
||||||
|
...otherProps
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
isEditArtistModalOpen,
|
||||||
|
isDeleteArtistModalOpen
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const link = `/artist/${nameSlug}`;
|
||||||
|
|
||||||
|
const elementStyle = {
|
||||||
|
width: `${bannerWidth}px`,
|
||||||
|
height: `${bannerHeight}px`
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container} style={style}>
|
||||||
|
<div className={styles.content}>
|
||||||
|
<div className={styles.bannerContainer}>
|
||||||
|
<Label className={styles.controls}>
|
||||||
|
<SpinnerIconButton
|
||||||
|
className={styles.action}
|
||||||
|
name={icons.REFRESH}
|
||||||
|
title="Refresh Artist"
|
||||||
|
isSpinning={isRefreshingArtist}
|
||||||
|
onPress={onRefreshArtistPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
className={styles.action}
|
||||||
|
name={icons.EDIT}
|
||||||
|
title="Edit Artist"
|
||||||
|
onPress={this.onEditArtistPress}
|
||||||
|
/>
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
{
|
||||||
|
status === 'ended' &&
|
||||||
|
<div
|
||||||
|
className={styles.ended}
|
||||||
|
title="Ended"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
<Link
|
||||||
|
className={styles.link}
|
||||||
|
style={elementStyle}
|
||||||
|
to={link}
|
||||||
|
>
|
||||||
|
<ArtistBanner
|
||||||
|
className={styles.banner}
|
||||||
|
style={elementStyle}
|
||||||
|
images={images}
|
||||||
|
size={70}
|
||||||
|
lazy={false}
|
||||||
|
overflow={true}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ArtistIndexBannerProgressBar
|
||||||
|
monitored={monitored}
|
||||||
|
status={status}
|
||||||
|
trackCount={trackCount}
|
||||||
|
trackFileCount={trackFileCount}
|
||||||
|
bannerWidth={bannerWidth}
|
||||||
|
detailedProgressBar={detailedProgressBar}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{
|
||||||
|
showTitle &&
|
||||||
|
<div className={styles.title}>
|
||||||
|
{artistName}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
showQualityProfile &&
|
||||||
|
<div className={styles.title}>
|
||||||
|
{qualityProfile.name}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div className={styles.nextAiring}>
|
||||||
|
{
|
||||||
|
getRelativeDate(
|
||||||
|
nextAiring,
|
||||||
|
shortDateFormat,
|
||||||
|
showRelativeDates,
|
||||||
|
{
|
||||||
|
timeFormat,
|
||||||
|
timeForToday: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ArtistIndexBannerInfo
|
||||||
|
qualityProfile={qualityProfile}
|
||||||
|
showRelativeDates={showRelativeDates}
|
||||||
|
shortDateFormat={shortDateFormat}
|
||||||
|
timeFormat={timeFormat}
|
||||||
|
{...otherProps}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<EditArtistModalConnector
|
||||||
|
isOpen={isEditArtistModalOpen}
|
||||||
|
artistId={id}
|
||||||
|
onModalClose={this.onEditArtistModalClose}
|
||||||
|
onDeleteArtistPress={this.onDeleteArtistPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DeleteArtistModal
|
||||||
|
isOpen={isDeleteArtistModalOpen}
|
||||||
|
artistId={id}
|
||||||
|
onModalClose={this.onDeleteArtistModalClose}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ArtistIndexBanner.propTypes = {
|
||||||
|
style: PropTypes.object.isRequired,
|
||||||
|
id: PropTypes.number.isRequired,
|
||||||
|
artistName: PropTypes.string.isRequired,
|
||||||
|
monitored: PropTypes.bool.isRequired,
|
||||||
|
status: PropTypes.string.isRequired,
|
||||||
|
nameSlug: PropTypes.string.isRequired,
|
||||||
|
nextAiring: PropTypes.string,
|
||||||
|
trackCount: PropTypes.number,
|
||||||
|
trackFileCount: PropTypes.number,
|
||||||
|
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
bannerWidth: PropTypes.number.isRequired,
|
||||||
|
bannerHeight: PropTypes.number.isRequired,
|
||||||
|
detailedProgressBar: PropTypes.bool.isRequired,
|
||||||
|
showTitle: PropTypes.bool.isRequired,
|
||||||
|
showQualityProfile: PropTypes.bool.isRequired,
|
||||||
|
qualityProfile: PropTypes.object.isRequired,
|
||||||
|
showRelativeDates: PropTypes.bool.isRequired,
|
||||||
|
shortDateFormat: PropTypes.string.isRequired,
|
||||||
|
timeFormat: PropTypes.string.isRequired,
|
||||||
|
isRefreshingArtist: PropTypes.bool.isRequired,
|
||||||
|
onRefreshArtistPress: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
ArtistIndexBanner.defaultProps = {
|
||||||
|
trackCount: 0,
|
||||||
|
trackFileCount: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ArtistIndexBanner;
|
@ -0,0 +1,6 @@
|
|||||||
|
.info {
|
||||||
|
background-color: $defaultColor;
|
||||||
|
color: $white;
|
||||||
|
text-align: center;
|
||||||
|
font-size: $smallFontSize;
|
||||||
|
}
|
@ -0,0 +1,113 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||||
|
import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
|
import styles from './ArtistIndexBannerInfo.css';
|
||||||
|
|
||||||
|
function ArtistIndexBannerInfo(props) {
|
||||||
|
const {
|
||||||
|
qualityProfile,
|
||||||
|
previousAiring,
|
||||||
|
added,
|
||||||
|
albumCount,
|
||||||
|
path,
|
||||||
|
sizeOnDisk,
|
||||||
|
sortKey,
|
||||||
|
showRelativeDates,
|
||||||
|
shortDateFormat,
|
||||||
|
timeFormat
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
if (sortKey === 'qualityProfileId') {
|
||||||
|
return (
|
||||||
|
<div className={styles.info}>
|
||||||
|
{qualityProfile.name}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sortKey === 'previousAiring' && previousAiring) {
|
||||||
|
return (
|
||||||
|
<div className={styles.info}>
|
||||||
|
{
|
||||||
|
getRelativeDate(
|
||||||
|
previousAiring,
|
||||||
|
shortDateFormat,
|
||||||
|
showRelativeDates,
|
||||||
|
{
|
||||||
|
timeFormat,
|
||||||
|
timeForToday: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sortKey === 'added' && added) {
|
||||||
|
const addedDate = getRelativeDate(
|
||||||
|
added,
|
||||||
|
shortDateFormat,
|
||||||
|
showRelativeDates,
|
||||||
|
{
|
||||||
|
timeFormat,
|
||||||
|
timeForToday: false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.info}>
|
||||||
|
{`Added ${addedDate}`}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sortKey === 'albumCount') {
|
||||||
|
let albums = '1 album';
|
||||||
|
|
||||||
|
if (albumCount === 0) {
|
||||||
|
albums = 'No albums';
|
||||||
|
} else if (albumCount > 1) {
|
||||||
|
albums = `${albumCount} albums`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.info}>
|
||||||
|
{albums}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sortKey === 'path') {
|
||||||
|
return (
|
||||||
|
<div className={styles.info}>
|
||||||
|
{path}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sortKey === 'sizeOnDisk') {
|
||||||
|
return (
|
||||||
|
<div className={styles.info}>
|
||||||
|
{formatBytes(sizeOnDisk)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
ArtistIndexBannerInfo.propTypes = {
|
||||||
|
qualityProfile: PropTypes.object.isRequired,
|
||||||
|
previousAiring: PropTypes.string,
|
||||||
|
added: PropTypes.string,
|
||||||
|
albumCount: PropTypes.number.isRequired,
|
||||||
|
path: PropTypes.string.isRequired,
|
||||||
|
sizeOnDisk: PropTypes.number,
|
||||||
|
sortKey: PropTypes.string.isRequired,
|
||||||
|
showRelativeDates: PropTypes.bool.isRequired,
|
||||||
|
shortDateFormat: PropTypes.string.isRequired,
|
||||||
|
timeFormat: PropTypes.string.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ArtistIndexBannerInfo;
|
@ -0,0 +1,14 @@
|
|||||||
|
.progress {
|
||||||
|
composes: container from 'Components/ProgressBar.css';
|
||||||
|
|
||||||
|
border-radius: 0;
|
||||||
|
background-color: #5b5b5b;
|
||||||
|
color: $white;
|
||||||
|
transition: width 200ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progressBar {
|
||||||
|
composes: progressBar from 'Components/ProgressBar.css';
|
||||||
|
|
||||||
|
transition: width 200ms ease;
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import getProgressBarKind from 'Utilities/Series/getProgressBarKind';
|
||||||
|
import { sizes } from 'Helpers/Props';
|
||||||
|
import ProgressBar from 'Components/ProgressBar';
|
||||||
|
import styles from './ArtistIndexBannerProgressBar.css';
|
||||||
|
|
||||||
|
function ArtistIndexBannerProgressBar(props) {
|
||||||
|
const {
|
||||||
|
monitored,
|
||||||
|
status,
|
||||||
|
trackCount,
|
||||||
|
trackFileCount,
|
||||||
|
bannerWidth,
|
||||||
|
detailedProgressBar
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const progress = trackCount ? trackFileCount / trackCount * 100 : 100;
|
||||||
|
const text = `${trackFileCount} / ${trackCount}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ProgressBar
|
||||||
|
className={styles.progressBar}
|
||||||
|
containerClassName={styles.progress}
|
||||||
|
progress={progress}
|
||||||
|
kind={getProgressBarKind(status, monitored, progress)}
|
||||||
|
size={detailedProgressBar ? sizes.MEDIUM : sizes.SMALL}
|
||||||
|
showText={detailedProgressBar}
|
||||||
|
text={text}
|
||||||
|
title={detailedProgressBar ? null : text}
|
||||||
|
width={bannerWidth}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ArtistIndexBannerProgressBar.propTypes = {
|
||||||
|
monitored: PropTypes.bool.isRequired,
|
||||||
|
status: PropTypes.string.isRequired,
|
||||||
|
trackCount: PropTypes.number.isRequired,
|
||||||
|
trackFileCount: PropTypes.number.isRequired,
|
||||||
|
bannerWidth: PropTypes.number.isRequired,
|
||||||
|
detailedProgressBar: PropTypes.bool.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ArtistIndexBannerProgressBar;
|
@ -0,0 +1,3 @@
|
|||||||
|
.grid {
|
||||||
|
flex: 1 0 auto;
|
||||||
|
}
|
@ -0,0 +1,325 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import Measure from 'react-measure';
|
||||||
|
import { Grid, WindowScroller } from 'react-virtualized';
|
||||||
|
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||||
|
import dimensions from 'Styles/Variables/dimensions';
|
||||||
|
import { sortDirections } from 'Helpers/Props';
|
||||||
|
import ArtistIndexItemConnector from 'Artist/Index/ArtistIndexItemConnector';
|
||||||
|
import ArtistIndexBanner from './ArtistIndexBanner';
|
||||||
|
import styles from './ArtistIndexBanners.css';
|
||||||
|
|
||||||
|
// container dimensions
|
||||||
|
const columnPadding = 20;
|
||||||
|
const columnPaddingSmallScreen = 10;
|
||||||
|
const progressBarHeight = parseInt(dimensions.progressBarSmallHeight);
|
||||||
|
const detailedProgressBarHeight = parseInt(dimensions.progressBarMediumHeight);
|
||||||
|
|
||||||
|
const additionalColumnCount = {
|
||||||
|
small: 3,
|
||||||
|
medium: 2,
|
||||||
|
large: 1
|
||||||
|
};
|
||||||
|
|
||||||
|
function calculateColumnWidth(width, bannerSize, isSmallScreen) {
|
||||||
|
const maxiumColumnWidth = isSmallScreen ? 344 : 364;
|
||||||
|
const columns = Math.floor(width / maxiumColumnWidth);
|
||||||
|
const remainder = width % maxiumColumnWidth;
|
||||||
|
|
||||||
|
if (remainder === 0 && bannerSize === 'large') {
|
||||||
|
return maxiumColumnWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.floor(width / (columns + additionalColumnCount[bannerSize]));
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateRowHeight(bannerHeight, sortKey, isSmallScreen, bannerOptions) {
|
||||||
|
const {
|
||||||
|
detailedProgressBar,
|
||||||
|
showTitle,
|
||||||
|
showQualityProfile
|
||||||
|
} = bannerOptions;
|
||||||
|
|
||||||
|
const nextAiringHeight = 19;
|
||||||
|
|
||||||
|
const heights = [
|
||||||
|
bannerHeight,
|
||||||
|
detailedProgressBar ? detailedProgressBarHeight : progressBarHeight,
|
||||||
|
nextAiringHeight,
|
||||||
|
isSmallScreen ? columnPaddingSmallScreen : columnPadding
|
||||||
|
];
|
||||||
|
|
||||||
|
if (showTitle) {
|
||||||
|
heights.push(19);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showQualityProfile) {
|
||||||
|
heights.push(19);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (sortKey) {
|
||||||
|
case 'seasons':
|
||||||
|
case 'previousAiring':
|
||||||
|
case 'added':
|
||||||
|
case 'path':
|
||||||
|
case 'sizeOnDisk':
|
||||||
|
heights.push(19);
|
||||||
|
break;
|
||||||
|
case 'qualityProfileId':
|
||||||
|
if (!showQualityProfile) {
|
||||||
|
heights.push(19);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return heights.reduce((acc, height) => acc + height, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateHeight(bannerWidth) {
|
||||||
|
return Math.ceil((88/476) * bannerWidth);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ArtistIndexBanners extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
width: 0,
|
||||||
|
columnWidth: 364,
|
||||||
|
columnCount: 1,
|
||||||
|
bannerWidth: 476,
|
||||||
|
bannerHeight: 88,
|
||||||
|
rowHeight: calculateRowHeight(88, null, props.isSmallScreen, {})
|
||||||
|
};
|
||||||
|
|
||||||
|
this._isInitialized = false;
|
||||||
|
this._grid = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this._contentBodyNode = ReactDOM.findDOMNode(this.props.contentBody);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
const {
|
||||||
|
items,
|
||||||
|
filterKey,
|
||||||
|
filterValue,
|
||||||
|
sortKey,
|
||||||
|
sortDirection,
|
||||||
|
bannerOptions
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const itemsChanged = hasDifferentItems(prevProps.items, items);
|
||||||
|
|
||||||
|
if (
|
||||||
|
prevProps.sortKey !== sortKey ||
|
||||||
|
prevProps.bannerOptions !== bannerOptions ||
|
||||||
|
itemsChanged
|
||||||
|
) {
|
||||||
|
this.calculateGrid();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
prevProps.filterKey !== filterKey ||
|
||||||
|
prevProps.filterValue !== filterValue ||
|
||||||
|
prevProps.sortKey !== sortKey ||
|
||||||
|
prevProps.sortDirection !== sortDirection ||
|
||||||
|
itemsChanged
|
||||||
|
) {
|
||||||
|
this._grid.recomputeGridSize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Control
|
||||||
|
|
||||||
|
scrollToFirstCharacter(character) {
|
||||||
|
const items = this.props.items;
|
||||||
|
const {
|
||||||
|
columnCount,
|
||||||
|
rowHeight
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const index = _.findIndex(items, (item) => {
|
||||||
|
const firstCharacter = item.sortName.charAt(0);
|
||||||
|
|
||||||
|
if (character === '#') {
|
||||||
|
return !isNaN(firstCharacter);
|
||||||
|
}
|
||||||
|
|
||||||
|
return firstCharacter === character;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (index != null) {
|
||||||
|
const row = Math.floor(index / columnCount);
|
||||||
|
const scrollTop = rowHeight * row;
|
||||||
|
|
||||||
|
this.props.onScroll({ scrollTop });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setGridRef = (ref) => {
|
||||||
|
this._grid = ref;
|
||||||
|
}
|
||||||
|
|
||||||
|
calculateGrid = (width = this.state.width, isSmallScreen) => {
|
||||||
|
const {
|
||||||
|
sortKey,
|
||||||
|
bannerOptions
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding;
|
||||||
|
const columnWidth = calculateColumnWidth(width, this.props.bannerOptions.size);
|
||||||
|
const columnCount = Math.max(Math.floor(width / columnWidth), 1);
|
||||||
|
const bannerWidth = columnWidth - padding;
|
||||||
|
const bannerHeight = calculateHeight(bannerWidth);
|
||||||
|
const rowHeight = calculateRowHeight(bannerHeight, sortKey, isSmallScreen, bannerOptions);
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
width,
|
||||||
|
columnWidth,
|
||||||
|
columnCount,
|
||||||
|
bannerWidth,
|
||||||
|
bannerHeight,
|
||||||
|
rowHeight
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
cellRenderer = ({ key, rowIndex, columnIndex, style }) => {
|
||||||
|
const {
|
||||||
|
items,
|
||||||
|
sortKey,
|
||||||
|
bannerOptions,
|
||||||
|
showRelativeDates,
|
||||||
|
shortDateFormat,
|
||||||
|
timeFormat
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
bannerWidth,
|
||||||
|
bannerHeight,
|
||||||
|
columnCount
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const {
|
||||||
|
detailedProgressBar,
|
||||||
|
showTitle,
|
||||||
|
showQualityProfile
|
||||||
|
} = bannerOptions;
|
||||||
|
|
||||||
|
const series = items[rowIndex * columnCount + columnIndex];
|
||||||
|
|
||||||
|
if (!series) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ArtistIndexItemConnector
|
||||||
|
key={key}
|
||||||
|
component={ArtistIndexBanner}
|
||||||
|
sortKey={sortKey}
|
||||||
|
bannerWidth={bannerWidth}
|
||||||
|
bannerHeight={bannerHeight}
|
||||||
|
detailedProgressBar={detailedProgressBar}
|
||||||
|
showTitle={showTitle}
|
||||||
|
showQualityProfile={showQualityProfile}
|
||||||
|
showRelativeDates={showRelativeDates}
|
||||||
|
shortDateFormat={shortDateFormat}
|
||||||
|
timeFormat={timeFormat}
|
||||||
|
style={style}
|
||||||
|
{...series}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onMeasure = ({ width }) => {
|
||||||
|
this.calculateGrid(width, this.props.isSmallScreen);
|
||||||
|
}
|
||||||
|
|
||||||
|
onSectionRendered = () => {
|
||||||
|
if (!this._isInitialized && this._contentBodyNode) {
|
||||||
|
this.props.onRender();
|
||||||
|
this._isInitialized = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
items,
|
||||||
|
scrollTop,
|
||||||
|
isSmallScreen,
|
||||||
|
onScroll
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
width,
|
||||||
|
columnWidth,
|
||||||
|
columnCount,
|
||||||
|
rowHeight
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const rowCount = Math.ceil(items.length / columnCount);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Measure onMeasure={this.onMeasure}>
|
||||||
|
<WindowScroller
|
||||||
|
scrollElement={isSmallScreen ? null : this._contentBodyNode}
|
||||||
|
onScroll={onScroll}
|
||||||
|
>
|
||||||
|
{({ height, isScrolling }) => {
|
||||||
|
return (
|
||||||
|
<Grid
|
||||||
|
ref={this.setGridRef}
|
||||||
|
className={styles.grid}
|
||||||
|
autoHeight={true}
|
||||||
|
height={height}
|
||||||
|
columnCount={columnCount}
|
||||||
|
columnWidth={columnWidth}
|
||||||
|
rowCount={rowCount}
|
||||||
|
rowHeight={rowHeight}
|
||||||
|
width={width}
|
||||||
|
scrollTop={scrollTop}
|
||||||
|
overscanRowCount={2}
|
||||||
|
cellRenderer={this.cellRenderer}
|
||||||
|
onSectionRendered={this.onSectionRendered}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</WindowScroller>
|
||||||
|
</Measure>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ArtistIndexBanners.propTypes = {
|
||||||
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
filterKey: PropTypes.string,
|
||||||
|
filterValue: PropTypes.oneOfType([PropTypes.bool, PropTypes.number, PropTypes.string]),
|
||||||
|
sortKey: PropTypes.string,
|
||||||
|
sortDirection: PropTypes.oneOf(sortDirections.all),
|
||||||
|
bannerOptions: PropTypes.object.isRequired,
|
||||||
|
scrollTop: PropTypes.number.isRequired,
|
||||||
|
contentBody: PropTypes.object.isRequired,
|
||||||
|
showRelativeDates: PropTypes.bool.isRequired,
|
||||||
|
shortDateFormat: PropTypes.string.isRequired,
|
||||||
|
isSmallScreen: PropTypes.bool.isRequired,
|
||||||
|
timeFormat: PropTypes.string.isRequired,
|
||||||
|
onRender: PropTypes.func.isRequired,
|
||||||
|
onScroll: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ArtistIndexBanners;
|
@ -0,0 +1,33 @@
|
|||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import connectSection from 'Store/connectSection';
|
||||||
|
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||||
|
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||||
|
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||||
|
import ArtistIndexBanners from './ArtistIndexBanners';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.artistIndex.bannerOptions,
|
||||||
|
createClientSideCollectionSelector(),
|
||||||
|
createUISettingsSelector(),
|
||||||
|
createDimensionsSelector(),
|
||||||
|
(bannerOptions, series, uiSettings, dimensions) => {
|
||||||
|
return {
|
||||||
|
bannerOptions,
|
||||||
|
showRelativeDates: uiSettings.showRelativeDates,
|
||||||
|
shortDateFormat: uiSettings.shortDateFormat,
|
||||||
|
timeFormat: uiSettings.timeFormat,
|
||||||
|
isSmallScreen: dimensions.isSmallScreen,
|
||||||
|
...series
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connectSection(
|
||||||
|
createMapStateToProps,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
{ withRef: true },
|
||||||
|
{ section: 'series', uiSection: 'artistIndex' }
|
||||||
|
)(ArtistIndexBanners);
|
@ -0,0 +1,25 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import ArtistIndexBannerOptionsModalContentConnector from './ArtistIndexBannerOptionsModalContentConnector';
|
||||||
|
|
||||||
|
function ArtistIndexBannerOptionsModal({ isOpen, onModalClose, ...otherProps }) {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
>
|
||||||
|
<ArtistIndexBannerOptionsModalContentConnector
|
||||||
|
{...otherProps}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ArtistIndexBannerOptionsModal.propTypes = {
|
||||||
|
isOpen: PropTypes.bool.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ArtistIndexBannerOptionsModal;
|
@ -0,0 +1,173 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { inputTypes } from 'Helpers/Props';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
import Form from 'Components/Form/Form';
|
||||||
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
|
|
||||||
|
const bannerSizeOptions = [
|
||||||
|
{ key: 'small', value: 'Small' },
|
||||||
|
{ key: 'medium', value: 'Medium' },
|
||||||
|
{ key: 'large', value: 'Large' }
|
||||||
|
];
|
||||||
|
|
||||||
|
class ArtistIndexBannerOptionsModalContent extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
detailedProgressBar: props.detailedProgressBar,
|
||||||
|
size: props.size,
|
||||||
|
showTitle: props.showTitle,
|
||||||
|
showQualityProfile: props.showQualityProfile
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
const {
|
||||||
|
detailedProgressBar,
|
||||||
|
size,
|
||||||
|
showTitle,
|
||||||
|
showQualityProfile
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const state = {};
|
||||||
|
|
||||||
|
if (detailedProgressBar !== prevProps.detailedProgressBar) {
|
||||||
|
state.detailedProgressBar = detailedProgressBar;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (size !== prevProps.size) {
|
||||||
|
state.size = size;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showTitle !== prevProps.showTitle) {
|
||||||
|
state.showTitle = showTitle;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showQualityProfile !== prevProps.showQualityProfile) {
|
||||||
|
state.showQualityProfile = showQualityProfile;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_.isEmpty(state)) {
|
||||||
|
this.setState(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onChangeOption = ({ name, value }) => {
|
||||||
|
this.setState({
|
||||||
|
[name]: value
|
||||||
|
}, () => {
|
||||||
|
this.props.onChangeOption({ [name]: value });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
onModalClose
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
detailedProgressBar,
|
||||||
|
size,
|
||||||
|
showTitle,
|
||||||
|
showQualityProfile
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>
|
||||||
|
Options
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<Form>
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel> Size</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.SELECT}
|
||||||
|
name="size"
|
||||||
|
value={size}
|
||||||
|
values={bannerSizeOptions}
|
||||||
|
onChange={this.onChangeOption}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>Detailed Progress Bar</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="detailedProgressBar"
|
||||||
|
value={detailedProgressBar}
|
||||||
|
helpText="Show text on progess bar"
|
||||||
|
onChange={this.onChangeOption}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>Show Name</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="showTitle"
|
||||||
|
value={showTitle}
|
||||||
|
helpText="Show artist name under banner"
|
||||||
|
onChange={this.onChangeOption}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>Show Quality Profile</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="showQualityProfile"
|
||||||
|
value={showQualityProfile}
|
||||||
|
helpText="Show quality profile under banner"
|
||||||
|
onChange={this.onChangeOption}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</Form>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button
|
||||||
|
onPress={onModalClose}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ArtistIndexBannerOptionsModalContent.propTypes = {
|
||||||
|
size: PropTypes.string.isRequired,
|
||||||
|
showTitle: PropTypes.bool.isRequired,
|
||||||
|
showQualityProfile: PropTypes.bool.isRequired,
|
||||||
|
detailedProgressBar: PropTypes.bool.isRequired,
|
||||||
|
onChangeOption: PropTypes.func.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ArtistIndexBannerOptionsModalContent;
|
@ -0,0 +1,23 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { setArtistBannerOption } from 'Store/Actions/artistIndexActions';
|
||||||
|
import ArtistIndexBannerOptionsModalContent from './ArtistIndexBannerOptionsModalContent';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.artistIndex,
|
||||||
|
(artistIndex) => {
|
||||||
|
return artistIndex.bannerOptions;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMapDispatchToProps(dispatch, props) {
|
||||||
|
return {
|
||||||
|
onChangeOption(payload) {
|
||||||
|
dispatch(setArtistBannerOption(payload));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, createMapDispatchToProps)(ArtistIndexBannerOptionsModalContent);
|
Loading…
Reference in new issue