[UI] Add Banner View to Artist Index

pull/84/head
Qstick 7 years ago
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;

@ -45,6 +45,16 @@ const albumTypes = [
name: 'ep',
label: 'EP',
isVisible: true
},
{
name: 'broadcast',
label: 'Broadcast',
isVisible: true
},
{
name: 'other',
label: 'Other',
isVisible: true
}
];

@ -22,6 +22,17 @@
padding: calc($pageContentBodyPadding - 5px);
}
.bannersInnerContentBody {
composes: innerContentBody from 'Components/Page/PageContentBody.css';
display: flex;
flex-direction: column;
flex-grow: 1;
/* 5px less padding than normal to handle poster's 5px margin */
padding: calc($pageContentBodyPadding - 5px);
}
.tableInnerContentBody {
composes: innerContentBody from 'Components/Page/PageContentBody.css';
@ -48,4 +59,8 @@
.postersInnerContentBody {
padding: calc($pageContentBodyPaddingSmallScreen - 5px);
}
.bannersInnerContentBody {
padding: calc($pageContentBodyPaddingSmallScreen - 5px);
}
}

@ -15,6 +15,8 @@ import NoArtist from 'Artist/NoArtist';
import ArtistIndexTableConnector from './Table/ArtistIndexTableConnector';
import ArtistIndexPosterOptionsModal from './Posters/Options/ArtistIndexPosterOptionsModal';
import ArtistIndexPostersConnector from './Posters/ArtistIndexPostersConnector';
import ArtistIndexBannerOptionsModal from './Banners/Options/ArtistIndexBannerOptionsModal';
import ArtistIndexBannersConnector from './Banners/ArtistIndexBannersConnector';
import ArtistIndexFooter from './ArtistIndexFooter';
import ArtistIndexFilterMenu from './Menus/ArtistIndexFilterMenu';
import ArtistIndexSortMenu from './Menus/ArtistIndexSortMenu';
@ -26,6 +28,10 @@ function getViewComponent(view) {
return ArtistIndexPostersConnector;
}
if (view === 'banners') {
return ArtistIndexBannersConnector;
}
return ArtistIndexTableConnector;
}
@ -43,6 +49,7 @@ class ArtistIndex extends Component {
contentBody: null,
jumpBarItems: [],
isPosterOptionsModalOpen: false,
isBannerOptionsModalOpen: false,
isRendered: false
};
}
@ -122,6 +129,14 @@ class ArtistIndex extends Component {
this.setState({ isPosterOptionsModalOpen: false });
}
onBannerOptionsPress = () => {
this.setState({ isBannerOptionsModalOpen: true });
}
onBannerOptionsModalClose = () => {
this.setState({ isBannerOptionsModalOpen: false });
}
onJumpBarItemPress = (item) => {
const viewComponent = this._viewComponent.getWrappedInstance();
viewComponent.scrollToFirstCharacter(item);
@ -177,6 +192,7 @@ class ArtistIndex extends Component {
contentBody,
jumpBarItems,
isPosterOptionsModalOpen,
isBannerOptionsModalOpen,
isRendered
} = this.state;
@ -223,6 +239,20 @@ class ArtistIndex extends Component {
<PageToolbarSeparator />
}
{
view === 'banners' &&
<PageToolbarButton
label="Options"
iconName={icons.POSTER}
onPress={this.onBannerOptionsPress}
/>
}
{
view === 'banners' &&
<PageToolbarSeparator />
}
<ArtistIndexViewMenu
view={view}
onViewSelect={onViewSelect}
@ -296,6 +326,11 @@ class ArtistIndex extends Component {
isOpen={isPosterOptionsModalOpen}
onModalClose={this.onPosterOptionsModalClose}
/>
<ArtistIndexBannerOptionsModal
isOpen={isBannerOptionsModalOpen}
onModalClose={this.onBannerOptionsModalClose}
/>
</PageContent>
);
}

@ -16,6 +16,8 @@ import ArtistIndex from './ArtistIndex';
const POSTERS_PADDING = 15;
const POSTERS_PADDING_SMALL_SCREEN = 5;
const BANNERS_PADDING = 15;
const BANNERS_PADDING_SMALL_SCREEN = 5;
const TABLE_PADDING = parseInt(dimensions.pageContentBodyPadding);
const TABLE_PADDING_SMALL_SCREEN = parseInt(dimensions.pageContentBodyPaddingSmallScreen);
@ -34,6 +36,10 @@ function getScrollTop(view, scrollTop, isSmallScreen) {
padding = isSmallScreen ? POSTERS_PADDING_SMALL_SCREEN : POSTERS_PADDING;
}
if (view === 'banners') {
padding = isSmallScreen ? BANNERS_PADDING_SMALL_SCREEN : BANNERS_PADDING;
}
return scrollTop + padding;
}

@ -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,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);

@ -29,6 +29,14 @@ function ArtistIndexViewMenu(props) {
>
Posters
</ViewMenuItem>
<ViewMenuItem
name="banners"
selectedView={view}
onPress={onViewSelect}
>
Banners
</ViewMenuItem>
</MenuContent>
</ViewMenu>
);

@ -54,6 +54,7 @@ export const SET_ARTIST_FILTER = 'SET_ARTIST_FILTER';
export const SET_ARTIST_VIEW = 'SET_ARTIST_VIEW';
export const SET_ARTIST_TABLE_OPTION = 'SET_ARTIST_TABLE_OPTION';
export const SET_ARTIST_POSTER_OPTION = 'SET_ARTIST_POSTER_OPTION';
export const SET_ARTIST_BANNER_OPTION = 'SET_ARTIST_BANNER_OPTION';
export const TOGGLE_ARTIST_MONITORED = 'TOGGLE_ARTIST_MONITORED';
export const TOGGLE_ALBUM_MONITORED = 'TOGGLE_ALBUM_MONITORED';

@ -6,3 +6,4 @@ export const setArtistFilter = createAction(types.SET_ARTIST_FILTER);
export const setArtistView = createAction(types.SET_ARTIST_VIEW);
export const setArtistTableOption = createAction(types.SET_ARTIST_TABLE_OPTION);
export const setArtistPosterOption = createAction(types.SET_ARTIST_POSTER_OPTION);
export const setArtistBannerOption = createAction(types.SET_ARTIST_BANNER_OPTION);

@ -24,6 +24,13 @@ export const defaultState = {
showQualityProfile: false
},
bannerOptions: {
detailedProgressBar: false,
size: 'large',
showTitle: false,
showQualityProfile: false
},
columns: [
{
name: 'status',
@ -160,7 +167,8 @@ export const persistState = [
'artistIndex.filterType',
'artistIndex.view',
'artistIndex.columns',
'artistIndex.posterOptions'
'artistIndex.posterOptions',
'artistIndex.bannerOptions'
];
const reducerSection = 'artistIndex';
@ -188,6 +196,18 @@ const artistIndexReducers = handleActions({
...payload
}
};
},
[types.SET_ARTIST_BANNER_OPTION]: function(state, { payload }) {
const bannerOptions = state.bannerOptions;
return {
...state,
bannerOptions: {
...bannerOptions,
...payload
}
};
}
}, defaultState);

Loading…
Cancel
Save