From 25eef250a40344bb98a0895a9d2d01ef28a5b89a Mon Sep 17 00:00:00 2001 From: Qstick Date: Sun, 17 Sep 2017 02:24:15 -0400 Subject: [PATCH] [UI] Add Banner View to Artist Index --- frontend/src/Artist/ArtistBanner.js | 160 +++++++++ frontend/src/Artist/ArtistLogo.js | 160 +++++++++ frontend/src/Artist/Details/ArtistDetails.js | 10 + frontend/src/Artist/Index/ArtistIndex.css | 15 + frontend/src/Artist/Index/ArtistIndex.js | 35 ++ .../src/Artist/Index/ArtistIndexConnector.js | 6 + .../Index/Banners/ArtistIndexBanner.css | 89 +++++ .../Artist/Index/Banners/ArtistIndexBanner.js | 230 +++++++++++++ .../Index/Banners/ArtistIndexBannerInfo.css | 6 + .../Index/Banners/ArtistIndexBannerInfo.js | 113 ++++++ .../Banners/ArtistIndexBannerProgressBar.css | 14 + .../Banners/ArtistIndexBannerProgressBar.js | 45 +++ .../Index/Banners/ArtistIndexBanners.css | 3 + .../Index/Banners/ArtistIndexBanners.js | 325 ++++++++++++++++++ .../Banners/ArtistIndexBannersConnector.js | 33 ++ .../Options/ArtistIndexBannerOptionsModal.js | 25 ++ .../ArtistIndexBannerOptionsModalContent.js | 173 ++++++++++ ...IndexBannerOptionsModalContentConnector.js | 23 ++ .../Artist/Index/Menus/ArtistIndexViewMenu.js | 8 + frontend/src/Store/Actions/actionTypes.js | 1 + .../src/Store/Actions/artistIndexActions.js | 1 + .../src/Store/Reducers/artistIndexReducers.js | 22 +- 22 files changed, 1496 insertions(+), 1 deletion(-) create mode 100644 frontend/src/Artist/ArtistBanner.js create mode 100644 frontend/src/Artist/ArtistLogo.js create mode 100644 frontend/src/Artist/Index/Banners/ArtistIndexBanner.css create mode 100644 frontend/src/Artist/Index/Banners/ArtistIndexBanner.js create mode 100644 frontend/src/Artist/Index/Banners/ArtistIndexBannerInfo.css create mode 100644 frontend/src/Artist/Index/Banners/ArtistIndexBannerInfo.js create mode 100644 frontend/src/Artist/Index/Banners/ArtistIndexBannerProgressBar.css create mode 100644 frontend/src/Artist/Index/Banners/ArtistIndexBannerProgressBar.js create mode 100644 frontend/src/Artist/Index/Banners/ArtistIndexBanners.css create mode 100644 frontend/src/Artist/Index/Banners/ArtistIndexBanners.js create mode 100644 frontend/src/Artist/Index/Banners/ArtistIndexBannersConnector.js create mode 100644 frontend/src/Artist/Index/Banners/Options/ArtistIndexBannerOptionsModal.js create mode 100644 frontend/src/Artist/Index/Banners/Options/ArtistIndexBannerOptionsModalContent.js create mode 100644 frontend/src/Artist/Index/Banners/Options/ArtistIndexBannerOptionsModalContentConnector.js 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 + + + +
+ + Size + + + + + + Detailed Progress Bar + + + + + + Show Name + + + + + + Show Quality Profile + + + +
+
+ + + + +
+ ); + } +} + +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);