diff --git a/frontend/src/Artist/ArtistBanner.js b/frontend/src/Artist/ArtistBanner.js
new file mode 100644
index 000000000..3edd1b0bc
--- /dev/null
+++ b/frontend/src/Artist/ArtistBanner.js
@@ -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 (
+
+ );
+ }
+
+ if (lazy) {
+ return (
+
+ }
+ >
+
+
+ );
+ }
+
+ return (
+
+ );
+ }
+}
+
+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;
diff --git a/frontend/src/Artist/ArtistLogo.js b/frontend/src/Artist/ArtistLogo.js
new file mode 100644
index 000000000..05e665186
--- /dev/null
+++ b/frontend/src/Artist/ArtistLogo.js
@@ -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 (
+
+ );
+ }
+
+ if (lazy) {
+ return (
+
+ }
+ >
+
+
+ );
+ }
+
+ return (
+
+ );
+ }
+}
+
+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;
diff --git a/frontend/src/Artist/Details/ArtistDetails.js b/frontend/src/Artist/Details/ArtistDetails.js
index 77a0a6c83..2fe9d732c 100644
--- a/frontend/src/Artist/Details/ArtistDetails.js
+++ b/frontend/src/Artist/Details/ArtistDetails.js
@@ -45,6 +45,16 @@ const albumTypes = [
name: 'ep',
label: 'EP',
isVisible: true
+ },
+ {
+ name: 'broadcast',
+ label: 'Broadcast',
+ isVisible: true
+ },
+ {
+ name: 'other',
+ label: 'Other',
+ isVisible: true
}
];
diff --git a/frontend/src/Artist/Index/ArtistIndex.css b/frontend/src/Artist/Index/ArtistIndex.css
index 443372a73..9e67ef538 100644
--- a/frontend/src/Artist/Index/ArtistIndex.css
+++ b/frontend/src/Artist/Index/ArtistIndex.css
@@ -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);
+ }
}
diff --git a/frontend/src/Artist/Index/ArtistIndex.js b/frontend/src/Artist/Index/ArtistIndex.js
index b4d646636..5bf32a7c3 100644
--- a/frontend/src/Artist/Index/ArtistIndex.js
+++ b/frontend/src/Artist/Index/ArtistIndex.js
@@ -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 {
}
+ {
+ view === 'banners' &&
+
+ }
+
+ {
+ view === 'banners' &&
+
+ }
+
+
+
);
}
diff --git a/frontend/src/Artist/Index/ArtistIndexConnector.js b/frontend/src/Artist/Index/ArtistIndexConnector.js
index 8687c7b3c..719609c89 100644
--- a/frontend/src/Artist/Index/ArtistIndexConnector.js
+++ b/frontend/src/Artist/Index/ArtistIndexConnector.js
@@ -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;
}
diff --git a/frontend/src/Artist/Index/Banners/ArtistIndexBanner.css b/frontend/src/Artist/Index/Banners/ArtistIndexBanner.css
new file mode 100644
index 000000000..6e2dd7812
--- /dev/null
+++ b/frontend/src/Artist/Index/Banners/ArtistIndexBanner.css
@@ -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;
+ }
+}
diff --git a/frontend/src/Artist/Index/Banners/ArtistIndexBanner.js b/frontend/src/Artist/Index/Banners/ArtistIndexBanner.js
new file mode 100644
index 000000000..4a89f1a94
--- /dev/null
+++ b/frontend/src/Artist/Index/Banners/ArtistIndexBanner.js
@@ -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 (
+
+
+
+
+
+ {
+ status === 'ended' &&
+
+ }
+
+
+
+
+
+
+
+
+ {
+ showTitle &&
+
+ {artistName}
+
+ }
+
+ {
+ showQualityProfile &&
+
+ {qualityProfile.name}
+
+ }
+
+
+ {
+ getRelativeDate(
+ nextAiring,
+ shortDateFormat,
+ showRelativeDates,
+ {
+ timeFormat,
+ timeForToday: true
+ }
+ )
+ }
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+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;
diff --git a/frontend/src/Artist/Index/Banners/ArtistIndexBannerInfo.css b/frontend/src/Artist/Index/Banners/ArtistIndexBannerInfo.css
new file mode 100644
index 000000000..cab3dec61
--- /dev/null
+++ b/frontend/src/Artist/Index/Banners/ArtistIndexBannerInfo.css
@@ -0,0 +1,6 @@
+.info {
+ background-color: $defaultColor;
+ color: $white;
+ text-align: center;
+ font-size: $smallFontSize;
+}
diff --git a/frontend/src/Artist/Index/Banners/ArtistIndexBannerInfo.js b/frontend/src/Artist/Index/Banners/ArtistIndexBannerInfo.js
new file mode 100644
index 000000000..f800c5e27
--- /dev/null
+++ b/frontend/src/Artist/Index/Banners/ArtistIndexBannerInfo.js
@@ -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 (
+
+ {qualityProfile.name}
+
+ );
+ }
+
+ if (sortKey === 'previousAiring' && previousAiring) {
+ return (
+
+ {
+ getRelativeDate(
+ previousAiring,
+ shortDateFormat,
+ showRelativeDates,
+ {
+ timeFormat,
+ timeForToday: true
+ }
+ )
+ }
+
+ );
+ }
+
+ if (sortKey === 'added' && added) {
+ const addedDate = getRelativeDate(
+ added,
+ shortDateFormat,
+ showRelativeDates,
+ {
+ timeFormat,
+ timeForToday: false
+ }
+ );
+
+ return (
+
+ {`Added ${addedDate}`}
+
+ );
+ }
+
+ if (sortKey === 'albumCount') {
+ let albums = '1 album';
+
+ if (albumCount === 0) {
+ albums = 'No albums';
+ } else if (albumCount > 1) {
+ albums = `${albumCount} albums`;
+ }
+
+ return (
+
+ {albums}
+
+ );
+ }
+
+ if (sortKey === 'path') {
+ return (
+
+ {path}
+
+ );
+ }
+
+ if (sortKey === 'sizeOnDisk') {
+ return (
+
+ {formatBytes(sizeOnDisk)}
+
+ );
+ }
+
+ 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;
diff --git a/frontend/src/Artist/Index/Banners/ArtistIndexBannerProgressBar.css b/frontend/src/Artist/Index/Banners/ArtistIndexBannerProgressBar.css
new file mode 100644
index 000000000..dbf3499ab
--- /dev/null
+++ b/frontend/src/Artist/Index/Banners/ArtistIndexBannerProgressBar.css
@@ -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;
+}
diff --git a/frontend/src/Artist/Index/Banners/ArtistIndexBannerProgressBar.js b/frontend/src/Artist/Index/Banners/ArtistIndexBannerProgressBar.js
new file mode 100644
index 000000000..fa9931a02
--- /dev/null
+++ b/frontend/src/Artist/Index/Banners/ArtistIndexBannerProgressBar.js
@@ -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 (
+
+ );
+}
+
+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;
diff --git a/frontend/src/Artist/Index/Banners/ArtistIndexBanners.css b/frontend/src/Artist/Index/Banners/ArtistIndexBanners.css
new file mode 100644
index 000000000..9c6520fb5
--- /dev/null
+++ b/frontend/src/Artist/Index/Banners/ArtistIndexBanners.css
@@ -0,0 +1,3 @@
+.grid {
+ flex: 1 0 auto;
+}
diff --git a/frontend/src/Artist/Index/Banners/ArtistIndexBanners.js b/frontend/src/Artist/Index/Banners/ArtistIndexBanners.js
new file mode 100644
index 000000000..908f5e4da
--- /dev/null
+++ b/frontend/src/Artist/Index/Banners/ArtistIndexBanners.js
@@ -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 (
+
+ );
+ }
+
+ //
+ // 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 (
+
+
+ {({ height, isScrolling }) => {
+ return (
+
+ );
+ }
+ }
+
+
+ );
+ }
+}
+
+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;
diff --git a/frontend/src/Artist/Index/Banners/ArtistIndexBannersConnector.js b/frontend/src/Artist/Index/Banners/ArtistIndexBannersConnector.js
new file mode 100644
index 000000000..83063eed8
--- /dev/null
+++ b/frontend/src/Artist/Index/Banners/ArtistIndexBannersConnector.js
@@ -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);
diff --git a/frontend/src/Artist/Index/Banners/Options/ArtistIndexBannerOptionsModal.js b/frontend/src/Artist/Index/Banners/Options/ArtistIndexBannerOptionsModal.js
new file mode 100644
index 000000000..34c8abfcf
--- /dev/null
+++ b/frontend/src/Artist/Index/Banners/Options/ArtistIndexBannerOptionsModal.js
@@ -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 (
+
+
+
+ );
+}
+
+ArtistIndexBannerOptionsModal.propTypes = {
+ isOpen: PropTypes.bool.isRequired,
+ onModalClose: PropTypes.func.isRequired
+};
+
+export default ArtistIndexBannerOptionsModal;
diff --git a/frontend/src/Artist/Index/Banners/Options/ArtistIndexBannerOptionsModalContent.js b/frontend/src/Artist/Index/Banners/Options/ArtistIndexBannerOptionsModalContent.js
new file mode 100644
index 000000000..d320acea5
--- /dev/null
+++ b/frontend/src/Artist/Index/Banners/Options/ArtistIndexBannerOptionsModalContent.js
@@ -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 (
+
+
+ Options
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+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;
diff --git a/frontend/src/Artist/Index/Banners/Options/ArtistIndexBannerOptionsModalContentConnector.js b/frontend/src/Artist/Index/Banners/Options/ArtistIndexBannerOptionsModalContentConnector.js
new file mode 100644
index 000000000..0ea742781
--- /dev/null
+++ b/frontend/src/Artist/Index/Banners/Options/ArtistIndexBannerOptionsModalContentConnector.js
@@ -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);
diff --git a/frontend/src/Artist/Index/Menus/ArtistIndexViewMenu.js b/frontend/src/Artist/Index/Menus/ArtistIndexViewMenu.js
index 7e3e35764..a8f0cd3ee 100644
--- a/frontend/src/Artist/Index/Menus/ArtistIndexViewMenu.js
+++ b/frontend/src/Artist/Index/Menus/ArtistIndexViewMenu.js
@@ -29,6 +29,14 @@ function ArtistIndexViewMenu(props) {
>
Posters
+
+
+ Banners
+
);
diff --git a/frontend/src/Store/Actions/actionTypes.js b/frontend/src/Store/Actions/actionTypes.js
index 509491d6d..22ee4d2b2 100644
--- a/frontend/src/Store/Actions/actionTypes.js
+++ b/frontend/src/Store/Actions/actionTypes.js
@@ -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';
diff --git a/frontend/src/Store/Actions/artistIndexActions.js b/frontend/src/Store/Actions/artistIndexActions.js
index 0f7ba43bf..e657dd5f7 100644
--- a/frontend/src/Store/Actions/artistIndexActions.js
+++ b/frontend/src/Store/Actions/artistIndexActions.js
@@ -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);
diff --git a/frontend/src/Store/Reducers/artistIndexReducers.js b/frontend/src/Store/Reducers/artistIndexReducers.js
index 6288e3377..ec26db1c3 100644
--- a/frontend/src/Store/Reducers/artistIndexReducers.js
+++ b/frontend/src/Store/Reducers/artistIndexReducers.js
@@ -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);