[UI Work] Add Overview Artist Index View

pull/104/head
Qstick 7 years ago
parent f6fc78d927
commit 566ac1a9d3

@ -1,4 +1,4 @@
import _ from 'lodash'; /* eslint max-params: 0 */
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import createArtistSelector from 'Store/Selectors/createArtistSelector'; import createArtistSelector from 'Store/Selectors/createArtistSelector';

@ -163,6 +163,7 @@ class ArtistDetails extends Component {
foreignArtistId, foreignArtistId,
artistName, artistName,
ratings, ratings,
path,
sizeOnDisk, sizeOnDisk,
trackFileCount, trackFileCount,
qualityProfileId, qualityProfileId,
@ -343,6 +344,21 @@ class ArtistDetails extends Component {
</div> </div>
<div className={styles.detailsLabels}> <div className={styles.detailsLabels}>
<Label
className={styles.detailsLabel}
title={trackFilesCountMessage}
size={sizes.LARGE}
>
<Icon
name={icons.FOLDER}
size={17}
/>
<span className={styles.sizeOnDisk}>
{path}
</span>
</Label>
<Label <Label
className={styles.detailsLabel} className={styles.detailsLabel}
title={trackFilesCountMessage} title={trackFilesCountMessage}
@ -547,6 +563,7 @@ ArtistDetails.propTypes = {
foreignArtistId: PropTypes.string.isRequired, foreignArtistId: PropTypes.string.isRequired,
artistName: PropTypes.string.isRequired, artistName: PropTypes.string.isRequired,
ratings: PropTypes.object.isRequired, ratings: PropTypes.object.isRequired,
path: PropTypes.string.isRequired,
sizeOnDisk: PropTypes.number.isRequired, sizeOnDisk: PropTypes.number.isRequired,
trackFileCount: PropTypes.number, trackFileCount: PropTypes.number,
qualityProfileId: PropTypes.number.isRequired, qualityProfileId: PropTypes.number.isRequired,

@ -17,6 +17,8 @@ import ArtistIndexPosterOptionsModal from './Posters/Options/ArtistIndexPosterOp
import ArtistIndexPostersConnector from './Posters/ArtistIndexPostersConnector'; import ArtistIndexPostersConnector from './Posters/ArtistIndexPostersConnector';
import ArtistIndexBannerOptionsModal from './Banners/Options/ArtistIndexBannerOptionsModal'; import ArtistIndexBannerOptionsModal from './Banners/Options/ArtistIndexBannerOptionsModal';
import ArtistIndexBannersConnector from './Banners/ArtistIndexBannersConnector'; import ArtistIndexBannersConnector from './Banners/ArtistIndexBannersConnector';
import ArtistIndexOverviewOptionsModal from './Overview/Options/ArtistIndexOverviewOptionsModal';
import ArtistIndexOverviewsConnector from './Overview/ArtistIndexOverviewsConnector';
import ArtistIndexFooter from './ArtistIndexFooter'; import ArtistIndexFooter from './ArtistIndexFooter';
import ArtistIndexFilterMenu from './Menus/ArtistIndexFilterMenu'; import ArtistIndexFilterMenu from './Menus/ArtistIndexFilterMenu';
import ArtistIndexSortMenu from './Menus/ArtistIndexSortMenu'; import ArtistIndexSortMenu from './Menus/ArtistIndexSortMenu';
@ -32,6 +34,10 @@ function getViewComponent(view) {
return ArtistIndexBannersConnector; return ArtistIndexBannersConnector;
} }
if (view === 'overview') {
return ArtistIndexOverviewsConnector;
}
return ArtistIndexTableConnector; return ArtistIndexTableConnector;
} }
@ -50,6 +56,7 @@ class ArtistIndex extends Component {
jumpBarItems: [], jumpBarItems: [],
isPosterOptionsModalOpen: false, isPosterOptionsModalOpen: false,
isBannerOptionsModalOpen: false, isBannerOptionsModalOpen: false,
isOverviewOptionsModalOpen: false,
isRendered: false isRendered: false
}; };
} }
@ -137,6 +144,14 @@ class ArtistIndex extends Component {
this.setState({ isBannerOptionsModalOpen: false }); this.setState({ isBannerOptionsModalOpen: false });
} }
onOverviewOptionsPress = () => {
this.setState({ isOverviewOptionsModalOpen: true });
}
onOverviewOptionsModalClose = () => {
this.setState({ isOverviewOptionsModalOpen: false });
}
onJumpBarItemPress = (item) => { onJumpBarItemPress = (item) => {
const viewComponent = this._viewComponent.getWrappedInstance(); const viewComponent = this._viewComponent.getWrappedInstance();
viewComponent.scrollToFirstCharacter(item); viewComponent.scrollToFirstCharacter(item);
@ -193,6 +208,7 @@ class ArtistIndex extends Component {
jumpBarItems, jumpBarItems,
isPosterOptionsModalOpen, isPosterOptionsModalOpen,
isBannerOptionsModalOpen, isBannerOptionsModalOpen,
isOverviewOptionsModalOpen,
isRendered isRendered
} = this.state; } = this.state;
@ -234,11 +250,6 @@ class ArtistIndex extends Component {
/> />
} }
{
view === 'posters' &&
<PageToolbarSeparator />
}
{ {
view === 'banners' && view === 'banners' &&
<PageToolbarButton <PageToolbarButton
@ -249,7 +260,17 @@ class ArtistIndex extends Component {
} }
{ {
view === 'banners' && view === 'overview' &&
<PageToolbarButton
label="Options"
iconName={icons.OVERVIEW}
onPress={this.onOverviewOptionsPress}
/>
}
{
(view === 'posters' || view === 'banners' || view === 'overview') &&
<PageToolbarSeparator /> <PageToolbarSeparator />
} }
@ -330,6 +351,13 @@ class ArtistIndex extends Component {
<ArtistIndexBannerOptionsModal <ArtistIndexBannerOptionsModal
isOpen={isBannerOptionsModalOpen} isOpen={isBannerOptionsModalOpen}
onModalClose={this.onBannerOptionsModalClose} onModalClose={this.onBannerOptionsModalClose}
/>
<ArtistIndexOverviewOptionsModal
isOpen={isOverviewOptionsModalOpen}
onModalClose={this.onOverviewOptionsModalClose}
/> />
</PageContent> </PageContent>
); );

@ -9,7 +9,7 @@ import Link from 'Components/Link/Link';
import ArtistBanner from 'Artist/ArtistBanner'; import ArtistBanner from 'Artist/ArtistBanner';
import EditArtistModalConnector from 'Artist/Edit/EditArtistModalConnector'; import EditArtistModalConnector from 'Artist/Edit/EditArtistModalConnector';
import DeleteArtistModal from 'Artist/Delete/DeleteArtistModal'; import DeleteArtistModal from 'Artist/Delete/DeleteArtistModal';
import ArtistIndexBannerProgressBar from './ArtistIndexBannerProgressBar'; import ArtistIndexProgressBar from 'Artist/Index/ProgressBar/ArtistIndexProgressBar';
import ArtistIndexBannerInfo from './ArtistIndexBannerInfo'; import ArtistIndexBannerInfo from './ArtistIndexBannerInfo';
import styles from './ArtistIndexBanner.css'; import styles from './ArtistIndexBanner.css';
@ -135,12 +135,12 @@ class ArtistIndexBanner extends Component {
</Link> </Link>
</div> </div>
<ArtistIndexBannerProgressBar <ArtistIndexProgressBar
monitored={monitored} monitored={monitored}
status={status} status={status}
trackCount={trackCount} trackCount={trackCount}
trackFileCount={trackFileCount} trackFileCount={trackFileCount}
bannerWidth={bannerWidth} posterWidth={bannerWidth}
detailedProgressBar={detailedProgressBar} detailedProgressBar={detailedProgressBar}
/> />
@ -174,6 +174,7 @@ class ArtistIndexBanner extends Component {
<ArtistIndexBannerInfo <ArtistIndexBannerInfo
qualityProfile={qualityProfile} qualityProfile={qualityProfile}
showQualityProfile={showQualityProfile}
showRelativeDates={showRelativeDates} showRelativeDates={showRelativeDates}
shortDateFormat={shortDateFormat} shortDateFormat={shortDateFormat}
timeFormat={timeFormat} timeFormat={timeFormat}

@ -7,6 +7,7 @@ import styles from './ArtistIndexBannerInfo.css';
function ArtistIndexBannerInfo(props) { function ArtistIndexBannerInfo(props) {
const { const {
qualityProfile, qualityProfile,
showQualityProfile,
previousAiring, previousAiring,
added, added,
albumCount, albumCount,
@ -18,7 +19,7 @@ function ArtistIndexBannerInfo(props) {
timeFormat timeFormat
} = props; } = props;
if (sortKey === 'qualityProfileId') { if (sortKey === 'qualityProfileId' && !showQualityProfile) {
return ( return (
<div className={styles.info}> <div className={styles.info}>
{qualityProfile.name} {qualityProfile.name}
@ -99,6 +100,7 @@ function ArtistIndexBannerInfo(props) {
ArtistIndexBannerInfo.propTypes = { ArtistIndexBannerInfo.propTypes = {
qualityProfile: PropTypes.object.isRequired, qualityProfile: PropTypes.object.isRequired,
showQualityProfile: PropTypes.bool.isRequired,
previousAiring: PropTypes.string, previousAiring: PropTypes.string,
added: PropTypes.string, added: PropTypes.string,
albumCount: PropTypes.number.isRequired, albumCount: PropTypes.number.isRequired,

@ -1,45 +0,0 @@
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;

@ -12,8 +12,8 @@ import ArtistIndexBanner from './ArtistIndexBanner';
import styles from './ArtistIndexBanners.css'; import styles from './ArtistIndexBanners.css';
// container dimensions // container dimensions
const columnPadding = 20; const columnPadding = parseInt(dimensions.artistIndexColumnPadding);
const columnPaddingSmallScreen = 10; const columnPaddingSmallScreen = parseInt(dimensions.artistIndexColumnPaddingSmallScreen);
const progressBarHeight = parseInt(dimensions.progressBarSmallHeight); const progressBarHeight = parseInt(dimensions.progressBarSmallHeight);
const detailedProgressBarHeight = parseInt(dimensions.progressBarMediumHeight); const detailedProgressBarHeight = parseInt(dimensions.progressBarMediumHeight);
@ -178,7 +178,7 @@ class ArtistIndexBanners extends Component {
} = this.props; } = this.props;
const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding; const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding;
const columnWidth = calculateColumnWidth(width, this.props.bannerOptions.size); const columnWidth = calculateColumnWidth(width, bannerOptions.size, isSmallScreen);
const columnCount = Math.max(Math.floor(width / columnWidth), 1); const columnCount = Math.max(Math.floor(width / columnWidth), 1);
const bannerWidth = columnWidth - padding; const bannerWidth = columnWidth - padding;
const bannerHeight = calculateHeight(bannerWidth); const bannerHeight = calculateHeight(bannerWidth);
@ -300,7 +300,7 @@ class ArtistIndexBanners extends Component {
/> />
); );
} }
} }
</WindowScroller> </WindowScroller>
</Measure> </Measure>
); );

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

@ -0,0 +1,82 @@
$hoverScale: 1.05;
.container {
display: flex;
}
.poster {
position: relative;
}
.posterContainer {
position: relative;
}
.link {
composes: link from 'Components/Link/Link.css';
display: block;
color: $defaultColor;
&:hover {
color: $defaultColor;
text-decoration: none;
}
}
.ended {
position: absolute;
top: 0;
right: 0;
z-index: 1;
width: 0;
height: 0;
border-width: 0 25px 25px 0;
border-style: solid;
border-color: transparent $dangerColor transparent transparent;
color: $white;
}
.info {
flex: 1 0 1px;
overflow: hidden;
padding-left: 10px;
}
.titleRow {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
line-height: 32px;
}
.title {
@add-mixin truncate;
composes: link;
flex: 1 0 1px;
font-weight: 300;
font-size: 30px;
}
.actions {
white-space: nowrap;
}
.details {
display: flex;
justify-content: space-between;
}
.overview {
composes: link;
flex: 0 1 1000px;
overflow: hidden;
}
@media only screen and (max-width: $breakpointSmall) {
.overview {
display: none;
}
}

@ -0,0 +1,250 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Truncate from 'react-truncate';
import { icons } from 'Helpers/Props';
import dimensions from 'Styles/Variables/dimensions';
import fonts from 'Styles/Variables/fonts';
import IconButton from 'Components/Link/IconButton';
import Link from 'Components/Link/Link';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import ArtistPoster from 'Artist/ArtistPoster';
import EditArtistModalConnector from 'Artist/Edit/EditArtistModalConnector';
import DeleteArtistModal from 'Artist/Delete/DeleteArtistModal';
import ArtistIndexProgressBar from 'Artist/Index/ProgressBar/ArtistIndexProgressBar';
import ArtistIndexOverviewInfo from './ArtistIndexOverviewInfo';
import styles from './ArtistIndexOverview.css';
const columnPadding = parseInt(dimensions.artistIndexColumnPadding);
const columnPaddingSmallScreen = parseInt(dimensions.artistIndexColumnPaddingSmallScreen);
const defaultFontSize = parseInt(fonts.defaultFontSize);
const lineHeight = parseFloat(fonts.lineHeight);
function calculateHeight(rowHeight, isSmallScreen) {
let height = rowHeight - 45;
if (isSmallScreen) {
height -= columnPaddingSmallScreen;
} else {
height -= columnPadding;
}
return height;
}
class ArtistIndexOverview 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,
overview,
monitored,
status,
nameSlug,
nextAiring,
trackCount,
trackFileCount,
images,
posterWidth,
posterHeight,
qualityProfile,
overviewOptions,
showRelativeDates,
shortDateFormat,
timeFormat,
rowHeight,
isSmallScreen,
isRefreshingArtist,
onRefreshArtistPress,
...otherProps
} = this.props;
const {
isEditArtistModalOpen,
isDeleteArtistModalOpen
} = this.state;
const link = `/artist/${nameSlug}`;
const elementStyle = {
width: `${posterWidth}px`,
height: `${posterHeight}px`
};
const height = calculateHeight(rowHeight, isSmallScreen);
return (
<div className={styles.container} style={style}>
<div className={styles.poster} style={elementStyle}>
<div className={styles.posterContainer}>
{
status === 'ended' &&
<div
className={styles.ended}
title="Ended"
/>
}
<Link
className={styles.link}
style={elementStyle}
to={link}
>
<ArtistPoster
className={styles.poster}
style={elementStyle}
images={images}
size={250}
lazy={false}
overflow={true}
/>
</Link>
</div>
<ArtistIndexProgressBar
monitored={monitored}
status={status}
trackCount={trackCount}
trackFileCount={trackFileCount}
posterWidth={posterWidth}
detailedProgressBar={overviewOptions.detailedProgressBar}
/>
</div>
<div className={styles.info}>
<div className={styles.titleRow}>
<Link
className={styles.title}
to={link}
>
{artistName}
</Link>
<div className={styles.actions}>
<SpinnerIconButton
name={icons.REFRESH}
title="Refresh artist"
isSpinning={isRefreshingArtist}
onPress={onRefreshArtistPress}
/>
<IconButton
name={icons.EDIT}
title="Edit Artist"
onPress={this.onEditArtistPress}
/>
</div>
</div>
<div className={styles.details}>
<Link
className={styles.overview}
style={{
maxHeight: `${height}px`
}}
to={link}
>
<Truncate lines={Math.floor(height / (defaultFontSize * lineHeight))}>
{overview}
</Truncate>
</Link>
<ArtistIndexOverviewInfo
height={height}
nextAiring={nextAiring}
qualityProfile={qualityProfile}
showRelativeDates={showRelativeDates}
shortDateFormat={shortDateFormat}
timeFormat={timeFormat}
{...overviewOptions}
{...otherProps}
/>
</div>
</div>
<EditArtistModalConnector
isOpen={isEditArtistModalOpen}
artistId={id}
onModalClose={this.onEditArtistModalClose}
onDeleteArtistPress={this.onDeleteArtistPress}
/>
<DeleteArtistModal
isOpen={isDeleteArtistModalOpen}
artistId={id}
onModalClose={this.onDeleteArtistModalClose}
/>
</div>
);
}
}
ArtistIndexOverview.propTypes = {
style: PropTypes.object.isRequired,
id: PropTypes.number.isRequired,
artistName: PropTypes.string.isRequired,
overview: 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,
posterWidth: PropTypes.number.isRequired,
posterHeight: PropTypes.number.isRequired,
rowHeight: PropTypes.number.isRequired,
qualityProfile: PropTypes.object.isRequired,
overviewOptions: PropTypes.object.isRequired,
showRelativeDates: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired,
isSmallScreen: PropTypes.bool.isRequired,
isRefreshingArtist: PropTypes.bool.isRequired,
onRefreshArtistPress: PropTypes.func.isRequired
};
ArtistIndexOverview.defaultProps = {
trackCount: 0,
trackFileCount: 0
};
export default ArtistIndexOverview;

@ -0,0 +1,23 @@
.infos {
display: flex;
flex: 0 0 250px;
flex-direction: column;
margin-left: 10px;
}
.info {
flex: 0 0 $artistIndexOverviewInfoRowHeight;
margin: 2px 0;
}
.icon {
margin-right: 5px;
width: 25px;
text-align: center;
}
@media only screen and (max-width: $breakpointSmall) {
.infos {
margin-left: 0;
}
}

@ -0,0 +1,193 @@
import PropTypes from 'prop-types';
import React from 'react';
import getRelativeDate from 'Utilities/Date/getRelativeDate';
import formatBytes from 'Utilities/Number/formatBytes';
import { icons } from 'Helpers/Props';
import dimensions from 'Styles/Variables/dimensions';
import Icon from 'Components/Icon';
import styles from './ArtistIndexOverviewInfo.css';
const infoRowHeight = parseInt(dimensions.artistIndexOverviewInfoRowHeight);
function isVisible(name, show, value, sortKey, index) {
if (value == null) {
return false;
}
return show || sortKey === name;
}
function ArtistIndexOverviewInfo(props) {
const {
height,
showQualityProfile,
showAdded,
showAlbumCount,
showPath,
showSizeOnDisk,
nextAiring,
qualityProfile,
added,
albumCount,
path,
sizeOnDisk,
sortKey,
showRelativeDates,
shortDateFormat,
timeFormat
} = props;
let albums = '1 album';
if (albumCount === 0) {
albums = 'No albums';
} else if (albumCount > 1) {
albums = `${albumCount} albums`;
}
const maxRows = Math.floor(height / (infoRowHeight + 4));
return (
<div className={styles.infos}>
{
!!nextAiring &&
<div
className={styles.info}
title="Next Airing"
>
<Icon
className={styles.icon}
name={icons.SCHEDULED}
size={14}
/>
{
getRelativeDate(
nextAiring,
shortDateFormat,
showRelativeDates,
{
timeFormat,
timeForToday: true
}
)
}
</div>
}
{
isVisible('qualityProfileId', showQualityProfile, qualityProfile, sortKey) && maxRows > 1 &&
<div
className={styles.info}
title="Quality Profile"
>
<Icon
className={styles.icon}
name={icons.PROFILE}
size={14}
/>
{qualityProfile.name}
</div>
}
{
isVisible('added', showAdded, added, sortKey) && maxRows > 2 &&
<div
className={styles.info}
title="Date Added"
>
<Icon
className={styles.icon}
name={icons.ADD}
size={14}
/>
{
getRelativeDate(
added,
shortDateFormat,
showRelativeDates,
{
timeFormat,
timeForToday: true
}
)
}
</div>
}
{
isVisible('albumCount', showAlbumCount, albumCount, sortKey) && maxRows > 3 &&
<div
className={styles.info}
title="Album Count"
>
<Icon
className={styles.icon}
name={icons.CIRCLE}
size={14}
/>
{albums}
</div>
}
{
isVisible('path', showPath, path, sortKey) && maxRows > 4 &&
<div
className={styles.info}
title="Path"
>
<Icon
className={styles.icon}
name={icons.FOLDER}
size={14}
/>
{path}
</div>
}
{
isVisible('sizeOnDisk', showSizeOnDisk, sizeOnDisk, sortKey) && maxRows > 5 &&
<div
className={styles.info}
title="Size on Disk"
>
<Icon
className={styles.icon}
name={icons.DRIVE}
size={14}
/>
{formatBytes(sizeOnDisk)}
</div>
}
</div>
);
}
ArtistIndexOverviewInfo.propTypes = {
height: PropTypes.number.isRequired,
showNetwork: PropTypes.bool.isRequired,
showQualityProfile: PropTypes.bool.isRequired,
showAdded: PropTypes.bool.isRequired,
showAlbumCount: PropTypes.bool.isRequired,
showPath: PropTypes.bool.isRequired,
showSizeOnDisk: PropTypes.bool.isRequired,
nextAiring: PropTypes.string,
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 ArtistIndexOverviewInfo;

@ -0,0 +1,278 @@
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 ArtistIndexOverview from './ArtistIndexOverview';
import styles from './ArtistIndexOverviews.css';
// Poster container dimensions
const columnPadding = parseInt(dimensions.artistIndexColumnPadding);
const columnPaddingSmallScreen = parseInt(dimensions.artistIndexColumnPaddingSmallScreen);
const progressBarHeight = parseInt(dimensions.progressBarSmallHeight);
const detailedProgressBarHeight = parseInt(dimensions.progressBarMediumHeight);
function calculatePosterWidth(posterSize, isSmallScreen) {
const maxiumPosterWidth = isSmallScreen ? 192 : 202;
if (posterSize === 'large') {
return maxiumPosterWidth;
}
if (posterSize === 'medium') {
return Math.floor(maxiumPosterWidth * 0.75);
}
return Math.floor(maxiumPosterWidth * 0.5);
}
function calculateRowHeight(posterHeight, sortKey, isSmallScreen, overviewOptions) {
const {
detailedProgressBar
} = overviewOptions;
const heights = [
posterHeight,
detailedProgressBar ? detailedProgressBarHeight : progressBarHeight,
isSmallScreen ? columnPaddingSmallScreen : columnPadding
];
return heights.reduce((acc, height) => acc + height, 0);
}
function calculatePosterHeight(posterWidth) {
return posterWidth;
}
class ArtistIndexOverviews extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
width: 0,
columnCount: 1,
posterWidth: 238,
posterHeight: 238,
rowHeight: calculateRowHeight(238, 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,
overviewOptions
} = this.props;
const itemsChanged = hasDifferentItems(prevProps.items, items);
const overviewOptionsChanged = !_.isMatch(prevProps.overviewOptions, overviewOptions);
if (
prevProps.sortKey !== sortKey ||
prevProps.overviewOptions !== overviewOptions ||
itemsChanged
) {
this.calculateGrid();
}
if (
prevProps.filterKey !== filterKey ||
prevProps.filterValue !== filterValue ||
prevProps.sortKey !== sortKey ||
prevProps.sortDirection !== sortDirection ||
itemsChanged ||
overviewOptionsChanged
) {
this._grid.recomputeGridSize();
}
}
//
// Control
scrollToFirstCharacter(character) {
const items = this.props.items;
const {
rowHeight
} = this.state;
const index = _.findIndex(items, (item) => {
const firstCharacter = item.sortTitle.charAt(0);
if (character === '#') {
return !isNaN(firstCharacter);
}
return firstCharacter === character;
});
if (index != null) {
const scrollTop = rowHeight * index;
this.props.onScroll({ scrollTop });
}
}
setGridRef = (ref) => {
this._grid = ref;
}
calculateGrid = (width = this.state.width, isSmallScreen) => {
const {
sortKey,
overviewOptions
} = this.props;
const posterWidth = calculatePosterWidth(overviewOptions.size, isSmallScreen);
const posterHeight = calculatePosterHeight(posterWidth);
const rowHeight = calculateRowHeight(posterHeight, sortKey, isSmallScreen, overviewOptions);
this.setState({
width,
posterWidth,
posterHeight,
rowHeight
});
}
cellRenderer = ({ key, rowIndex, style }) => {
const {
items,
sortKey,
overviewOptions,
showRelativeDates,
shortDateFormat,
timeFormat,
isSmallScreen
} = this.props;
const {
posterWidth,
posterHeight,
rowHeight
} = this.state;
const artist = items[rowIndex];
if (!artist) {
return null;
}
return (
<ArtistIndexItemConnector
key={key}
component={ArtistIndexOverview}
sortKey={sortKey}
posterWidth={posterWidth}
posterHeight={posterHeight}
rowHeight={rowHeight}
overviewOptions={overviewOptions}
showRelativeDates={showRelativeDates}
shortDateFormat={shortDateFormat}
timeFormat={timeFormat}
isSmallScreen={isSmallScreen}
style={style}
{...artist}
/>
);
}
//
// 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,
rowHeight
} = this.state;
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={1}
columnWidth={width}
rowCount={items.length}
rowHeight={rowHeight}
width={width}
scrollTop={scrollTop}
overscanRowCount={2}
cellRenderer={this.cellRenderer}
onSectionRendered={this.onSectionRendered}
/>
);
}
}
</WindowScroller>
</Measure>
);
}
}
ArtistIndexOverviews.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),
overviewOptions: 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 ArtistIndexOverviews;

@ -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 ArtistIndexOverviews from './ArtistIndexOverviews';
function createMapStateToProps() {
return createSelector(
(state) => state.artistIndex.overviewOptions,
createClientSideCollectionSelector(),
createUISettingsSelector(),
createDimensionsSelector(),
(overviewOptions, artist, uiSettings, dimensions) => {
return {
overviewOptions,
showRelativeDates: uiSettings.showRelativeDates,
shortDateFormat: uiSettings.shortDateFormat,
timeFormat: uiSettings.timeFormat,
isSmallScreen: dimensions.isSmallScreen,
...artist
};
}
);
}
export default connectSection(
createMapStateToProps,
undefined,
undefined,
{ withRef: true },
{ section: 'artist', uiSection: 'artistIndex' }
)(ArtistIndexOverviews);

@ -0,0 +1,25 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import ArtistIndexOverviewOptionsModalContentConnector from './ArtistIndexOverviewOptionsModalContentConnector';
function ArtistIndexOverviewOptionsModal({ isOpen, onModalClose, ...otherProps }) {
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<ArtistIndexOverviewOptionsModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
ArtistIndexOverviewOptionsModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default ArtistIndexOverviewOptionsModal;

@ -0,0 +1,247 @@
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 posterSizeOptions = [
{ key: 'small', value: 'Small' },
{ key: 'medium', value: 'Medium' },
{ key: 'large', value: 'Large' }
];
class ArtistIndexOverviewOptionsModalContent extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
detailedProgressBar: props.detailedProgressBar,
size: props.size,
showQualityProfile: props.showQualityProfile,
showPreviousAiring: props.showPreviousAiring,
showAdded: props.showAdded,
showAlbumCount: props.showAlbumCount,
showPath: props.showPath,
showSizeOnDisk: props.showSizeOnDisk
};
}
componentDidUpdate(prevProps) {
const {
detailedProgressBar,
size,
showQualityProfile,
showPreviousAiring,
showAdded,
showAlbumCount,
showPath,
showSizeOnDisk
} = this.props;
const state = {};
if (detailedProgressBar !== prevProps.detailedProgressBar) {
state.detailedProgressBar = detailedProgressBar;
}
if (size !== prevProps.size) {
state.size = size;
}
if (showQualityProfile !== prevProps.showQualityProfile) {
state.showQualityProfile = showQualityProfile;
}
if (showPreviousAiring !== prevProps.showPreviousAiring) {
state.showPreviousAiring = showPreviousAiring;
}
if (showAdded !== prevProps.showAdded) {
state.showAdded = showAdded;
}
if (showAlbumCount !== prevProps.showAlbumCount) {
state.showAlbumCount = showAlbumCount;
}
if (showPath !== prevProps.showPath) {
state.showPath = showPath;
}
if (showSizeOnDisk !== prevProps.showSizeOnDisk) {
state.showSizeOnDisk = showSizeOnDisk;
}
if (!_.isEmpty(state)) {
this.setState(state);
}
}
//
// Listeners
onChangeOverviewOption = ({ name, value }) => {
this.setState({
[name]: value
}, () => {
this.props.onChangeOverviewOption({ [name]: value });
});
}
//
// Render
render() {
const {
onModalClose
} = this.props;
const {
detailedProgressBar,
size,
showQualityProfile,
showPreviousAiring,
showAdded,
showAlbumCount,
showPath,
showSizeOnDisk
} = this.state;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Overview Options
</ModalHeader>
<ModalBody>
<Form>
<FormGroup>
<FormLabel>Poster Size</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="size"
value={size}
values={posterSizeOptions}
onChange={this.onChangeOverviewOption}
/>
</FormGroup>
<FormGroup>
<FormLabel>Detailed Progress Bar</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="detailedProgressBar"
value={detailedProgressBar}
helpText="Show text on progess bar"
onChange={this.onChangeOverviewOption}
/>
</FormGroup>
<FormGroup>
<FormLabel>Show Quality Profile</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="showQualityProfile"
value={showQualityProfile}
onChange={this.onChangeOverviewOption}
/>
</FormGroup>
<FormGroup>
<FormLabel>Show Previous Airing</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="showPreviousAiring"
value={showPreviousAiring}
onChange={this.onChangeOverviewOption}
/>
</FormGroup>
<FormGroup>
<FormLabel>Show Date Added</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="showAdded"
value={showAdded}
onChange={this.onChangeOverviewOption}
/>
</FormGroup>
<FormGroup>
<FormLabel>Show Season Count</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="showAlbumCount"
value={showAlbumCount}
onChange={this.onChangeOverviewOption}
/>
</FormGroup>
<FormGroup>
<FormLabel>Show Path</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="showPath"
value={showPath}
onChange={this.onChangeOverviewOption}
/>
</FormGroup>
<FormGroup>
<FormLabel>Show Size on Disk</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="showSizeOnDisk"
value={showSizeOnDisk}
onChange={this.onChangeOverviewOption}
/>
</FormGroup>
</Form>
</ModalBody>
<ModalFooter>
<Button
onPress={onModalClose}
>
Close
</Button>
</ModalFooter>
</ModalContent>
);
}
}
ArtistIndexOverviewOptionsModalContent.propTypes = {
size: PropTypes.string.isRequired,
showQualityProfile: PropTypes.bool.isRequired,
showPreviousAiring: PropTypes.bool.isRequired,
showAdded: PropTypes.bool.isRequired,
showAlbumCount: PropTypes.bool.isRequired,
showPath: PropTypes.bool.isRequired,
showSizeOnDisk: PropTypes.bool.isRequired,
detailedProgressBar: PropTypes.bool.isRequired,
onChangeOverviewOption: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default ArtistIndexOverviewOptionsModalContent;

@ -0,0 +1,23 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { setArtistOverviewOption } from 'Store/Actions/artistIndexActions';
import ArtistIndexOverviewOptionsModalContent from './ArtistIndexOverviewOptionsModalContent';
function createMapStateToProps() {
return createSelector(
(state) => state.artistIndex,
(artistIndex) => {
return artistIndex.overviewOptions;
}
);
}
function createMapDispatchToProps(dispatch, props) {
return {
onChangeOverviewOption(payload) {
dispatch(setArtistOverviewOption(payload));
}
};
}
export default connect(createMapStateToProps, createMapDispatchToProps)(ArtistIndexOverviewOptionsModalContent);

@ -9,7 +9,7 @@ import Link from 'Components/Link/Link';
import ArtistPoster from 'Artist/ArtistPoster'; import ArtistPoster from 'Artist/ArtistPoster';
import EditArtistModalConnector from 'Artist/Edit/EditArtistModalConnector'; import EditArtistModalConnector from 'Artist/Edit/EditArtistModalConnector';
import DeleteArtistModal from 'Artist/Delete/DeleteArtistModal'; import DeleteArtistModal from 'Artist/Delete/DeleteArtistModal';
import ArtistIndexPosterProgressBar from './ArtistIndexPosterProgressBar'; import ArtistIndexProgressBar from 'Artist/Index/ProgressBar/ArtistIndexProgressBar';
import ArtistIndexPosterInfo from './ArtistIndexPosterInfo'; import ArtistIndexPosterInfo from './ArtistIndexPosterInfo';
import styles from './ArtistIndexPoster.css'; import styles from './ArtistIndexPoster.css';
@ -135,7 +135,7 @@ class ArtistIndexPoster extends Component {
</Link> </Link>
</div> </div>
<ArtistIndexPosterProgressBar <ArtistIndexProgressBar
monitored={monitored} monitored={monitored}
status={status} status={status}
trackCount={trackCount} trackCount={trackCount}
@ -174,6 +174,7 @@ class ArtistIndexPoster extends Component {
<ArtistIndexPosterInfo <ArtistIndexPosterInfo
qualityProfile={qualityProfile} qualityProfile={qualityProfile}
showQualityProfile={showQualityProfile}
showRelativeDates={showRelativeDates} showRelativeDates={showRelativeDates}
shortDateFormat={shortDateFormat} shortDateFormat={shortDateFormat}
timeFormat={timeFormat} timeFormat={timeFormat}

@ -7,6 +7,7 @@ import styles from './ArtistIndexPosterInfo.css';
function ArtistIndexPosterInfo(props) { function ArtistIndexPosterInfo(props) {
const { const {
qualityProfile, qualityProfile,
showQualityProfile,
previousAiring, previousAiring,
added, added,
albumCount, albumCount,
@ -18,7 +19,7 @@ function ArtistIndexPosterInfo(props) {
timeFormat timeFormat
} = props; } = props;
if (sortKey === 'qualityProfileId') { if (sortKey === 'qualityProfileId' && !showQualityProfile) {
return ( return (
<div className={styles.info}> <div className={styles.info}>
{qualityProfile.name} {qualityProfile.name}
@ -99,6 +100,7 @@ function ArtistIndexPosterInfo(props) {
ArtistIndexPosterInfo.propTypes = { ArtistIndexPosterInfo.propTypes = {
qualityProfile: PropTypes.object.isRequired, qualityProfile: PropTypes.object.isRequired,
showQualityProfile: PropTypes.bool.isRequired,
previousAiring: PropTypes.string, previousAiring: PropTypes.string,
added: PropTypes.string, added: PropTypes.string,
albumCount: PropTypes.number.isRequired, albumCount: PropTypes.number.isRequired,

@ -1,14 +0,0 @@
.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;
}

@ -12,8 +12,8 @@ import ArtistIndexPoster from './ArtistIndexPoster';
import styles from './ArtistIndexPosters.css'; import styles from './ArtistIndexPosters.css';
// Poster container dimensions // Poster container dimensions
const columnPadding = 20; const columnPadding = parseInt(dimensions.artistIndexColumnPadding);
const columnPaddingSmallScreen = 10; const columnPaddingSmallScreen = parseInt(dimensions.artistIndexColumnPaddingSmallScreen);
const progressBarHeight = parseInt(dimensions.progressBarSmallHeight); const progressBarHeight = parseInt(dimensions.progressBarSmallHeight);
const detailedProgressBarHeight = parseInt(dimensions.progressBarMediumHeight); const detailedProgressBarHeight = parseInt(dimensions.progressBarMediumHeight);
@ -178,7 +178,7 @@ class ArtistIndexPosters extends Component {
} = this.props; } = this.props;
const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding; const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding;
const columnWidth = calculateColumnWidth(width, this.props.posterOptions.size); const columnWidth = calculateColumnWidth(width, posterOptions.size, isSmallScreen);
const columnCount = Math.max(Math.floor(width / columnWidth), 1); const columnCount = Math.max(Math.floor(width / columnWidth), 1);
const posterWidth = columnWidth - padding; const posterWidth = columnWidth - padding;
const posterHeight = calculatePosterHeight(posterWidth); const posterHeight = calculatePosterHeight(posterWidth);

@ -3,9 +3,9 @@ import React from 'react';
import getProgressBarKind from 'Utilities/Series/getProgressBarKind'; import getProgressBarKind from 'Utilities/Series/getProgressBarKind';
import { sizes } from 'Helpers/Props'; import { sizes } from 'Helpers/Props';
import ProgressBar from 'Components/ProgressBar'; import ProgressBar from 'Components/ProgressBar';
import styles from './ArtistIndexPosterProgressBar.css'; import styles from './ArtistIndexProgressBar.css';
function ArtistIndexPosterProgressBar(props) { function ArtistIndexProgressBar(props) {
const { const {
monitored, monitored,
status, status,
@ -33,7 +33,7 @@ function ArtistIndexPosterProgressBar(props) {
); );
} }
ArtistIndexPosterProgressBar.propTypes = { ArtistIndexProgressBar.propTypes = {
monitored: PropTypes.bool.isRequired, monitored: PropTypes.bool.isRequired,
status: PropTypes.string.isRequired, status: PropTypes.string.isRequired,
trackCount: PropTypes.number.isRequired, trackCount: PropTypes.number.isRequired,
@ -42,4 +42,4 @@ ArtistIndexPosterProgressBar.propTypes = {
detailedProgressBar: PropTypes.bool.isRequired detailedProgressBar: PropTypes.bool.isRequired
}; };
export default ArtistIndexPosterProgressBar; export default ArtistIndexProgressBar;

@ -35,11 +35,10 @@ class CalendarPage extends Component {
// Listeners // Listeners
onMeasure = ({ width }) => { onMeasure = ({ width }) => {
this.setState({ width }, () => { this.setState({ width });
const days = Math.max(3, Math.min(7, Math.floor(width / MINIMUM_DAY_WIDTH))); const days = Math.max(3, Math.min(7, Math.floor(width / MINIMUM_DAY_WIDTH)));
console.log(`${width} || ${days}`);
this.props.onDaysCountChange(days); this.props.onDaysCountChange(days);
});
} }
onFilterMenuItemPress = (filterKey, unmonitored) => { onFilterMenuItemPress = (filterKey, unmonitored) => {
@ -109,7 +108,11 @@ class CalendarPage extends Component {
whitelist={['width']} whitelist={['width']}
onMeasure={this.onMeasure} onMeasure={this.onMeasure}
> >
<CalendarConnector /> {
this.state.width > 0 ?
<CalendarConnector /> :
<div />
}
</Measure> </Measure>
<Legend colorImpairedMode={colorImpairedMode} /> <Legend colorImpairedMode={colorImpairedMode} />

@ -1,5 +1,5 @@
.description { .description {
line-height: 1.528571429; line-height: $lineHeight;
} }
.description { .description {

@ -1,5 +1,5 @@
.title { .title {
line-height: 1.528571429; line-height: $lineHeight;
} }
.title { .title {

@ -75,7 +75,6 @@ class PathInput extends Component {
onInputBlur = () => { onInputBlur = () => {
this.props.onClearPaths(); this.props.onClearPaths();
this.props.onFetchPaths('');
} }
onSuggestionsFetchRequested = ({ value }) => { onSuggestionsFetchRequested = ({ value }) => {
@ -85,7 +84,6 @@ class PathInput extends Component {
onSuggestionsClearRequested = () => { onSuggestionsClearRequested = () => {
// Required because props aren't always rendered, but no-op // Required because props aren't always rendered, but no-op
// because we don't want to reset the paths after a path is selected. // because we don't want to reset the paths after a path is selected.
// `onInputBlur` will handle clearing when the user leaves the input.
} }
onSuggestionSelected = (event, { suggestionValue }) => { onSuggestionSelected = (event, { suggestionValue }) => {

@ -69,7 +69,7 @@ class SignalRConnector extends Component {
} }
componentDidMount() { componentDidMount() {
console.log('starting signalR'); console.log('Starting signalR');
this.signalRconnection = $.connection('/signalr', { apiKey: window.Sonarr.apiKey }); this.signalRconnection = $.connection('/signalr', { apiKey: window.Sonarr.apiKey });
@ -281,7 +281,7 @@ class SignalRConnector extends Component {
onStateChanged = (change) => { onStateChanged = (change) => {
const state = getState(change.newState); const state = getState(change.newState);
console.log(`SignalR: [${state}]`); console.log(`SignalR: ${state}`);
if (state === 'connected') { if (state === 'connected') {
// Repopulate the page (if a repopulator is set) to ensure things // Repopulate the page (if a repopulator is set) to ensure things

@ -13,6 +13,7 @@ export const CARET_DOWN = 'fa fa-caret-down';
export const CHECK = 'fa fa-check'; export const CHECK = 'fa fa-check';
export const CHECK_INDETERMINATE = 'fa fa-minus'; export const CHECK_INDETERMINATE = 'fa fa-minus';
export const CHECK_CIRCLE = 'fa fa-check-circle'; export const CHECK_CIRCLE = 'fa fa-check-circle';
export const CIRCLE = 'fa fa-circle';
export const CIRCLE_OUTLINE = 'fa fa-circle-o'; export const CIRCLE_OUTLINE = 'fa fa-circle-o';
export const CLEAR = 'fa fa-trash'; export const CLEAR = 'fa fa-trash';
export const CLIPBOARD = 'fa fa-clipboard'; export const CLIPBOARD = 'fa fa-clipboard';
@ -48,6 +49,7 @@ export const NAVBAR_COLLAPSE = 'fa fa-bars';
export const NOT_AIRED = 'fa fa-clock-o'; export const NOT_AIRED = 'fa fa-clock-o';
export const ORGANIZE = 'fa fa-sitemap'; export const ORGANIZE = 'fa fa-sitemap';
export const OVERFLOW = 'fa fa-ellipsis-h'; export const OVERFLOW = 'fa fa-ellipsis-h';
export const OVERVIEW = 'fa fa-th-list';
export const PAGE_FIRST = 'fa fa-fast-backward'; export const PAGE_FIRST = 'fa fa-fast-backward';
export const PAGE_PREVIOUS = 'fa fa-backward'; export const PAGE_PREVIOUS = 'fa fa-backward';
export const PAGE_NEXT = 'fa fa-forward'; export const PAGE_NEXT = 'fa fa-forward';

@ -33,24 +33,24 @@ class UISettings extends Component {
]; ];
const weekColumnOptions = [ const weekColumnOptions = [
{ key: 'ddd M/D', value: 'Tue 3/5' }, { key: 'ddd M/D', value: 'Tue 3/25' },
{ key: 'ddd MM/DD', value: 'Tue 03/05' }, { key: 'ddd MM/DD', value: 'Tue 03/25' },
{ key: 'ddd D/M', value: 'Tue 5/3' }, { key: 'ddd D/M', value: 'Tue 25/3' },
{ key: 'ddd DD/MM', value: 'Tue 05/03' } { key: 'ddd DD/MM', value: 'Tue 25/03' }
]; ];
const shortDateFormatOptions = [ const shortDateFormatOptions = [
{ key: 'MMM D YYYY', value: 'Mar 5 2014' }, { key: 'MMM D YYYY', value: 'Mar 25 2014' },
{ key: 'DD MMM YYYY', value: '5 Mar 2014' }, { key: 'DD MMM YYYY', value: '25 Mar 2014' },
{ key: 'MM/D/YYYY', value: '03/5/2014' }, { key: 'MM/D/YYYY', value: '03/25/2014' },
{ key: 'MM/DD/YYYY', value: '03/05/2014' }, { key: 'MM/DD/YYYY', value: '03/25/2014' },
{ key: 'DD/MM/YYYY', value: '05/03/2014' }, { key: 'DD/MM/YYYY', value: '25/03/2014' },
{ key: 'YYYY-MM-DD', value: '2014-03-05' } { key: 'YYYY-MM-DD', value: '2014-03-25' }
]; ];
const longDateFormatOptions = [ const longDateFormatOptions = [
{ key: 'dddd, MMMM D YYYY', value: 'Tuesday, March 5, 2014' }, { key: 'dddd, MMMM D YYYY', value: 'Tuesday, March 25, 2014' },
{ key: 'dddd, D MMMM YYYY', value: 'Tuesday, 5 March, 2014' } { key: 'dddd, D MMMM YYYY', value: 'Tuesday, 25 March, 2014' }
]; ];
const timeFormatOptions = [ const timeFormatOptions = [

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

@ -7,3 +7,4 @@ export const setArtistView = createAction(types.SET_ARTIST_VIEW);
export const setArtistTableOption = createAction(types.SET_ARTIST_TABLE_OPTION); export const setArtistTableOption = createAction(types.SET_ARTIST_TABLE_OPTION);
export const setArtistPosterOption = createAction(types.SET_ARTIST_POSTER_OPTION); export const setArtistPosterOption = createAction(types.SET_ARTIST_POSTER_OPTION);
export const setArtistBannerOption = createAction(types.SET_ARTIST_BANNER_OPTION); export const setArtistBannerOption = createAction(types.SET_ARTIST_BANNER_OPTION);
export const setArtistOverviewOption = createAction(types.SET_ARTIST_OVERVIEW_OPTION);

@ -176,6 +176,10 @@ const calendarActionHandlers = {
[types.SET_CALENDAR_DAYS_COUNT]: function(payload) { [types.SET_CALENDAR_DAYS_COUNT]: function(payload) {
return function(dispatch, getState) { return function(dispatch, getState) {
if (payload.dayCount === getState().calendar.dayCount) {
return;
}
dispatch(set({ dispatch(set({
section, section,
dayCount: payload.dayCount dayCount: payload.dayCount
@ -184,10 +188,7 @@ const calendarActionHandlers = {
const state = getState(); const state = getState();
const { time, view } = state.calendar; const { time, view } = state.calendar;
dispatch(fetchCalendar({ dispatch(fetchCalendar({ time, view }));
time,
view
}));
}; };
}, },
@ -201,10 +202,7 @@ const calendarActionHandlers = {
const state = getState(); const state = getState();
const { time, view } = state.calendar; const { time, view } = state.calendar;
dispatch(fetchCalendar({ dispatch(fetchCalendar({ time, view }));
time,
view
}));
}; };
}, },
@ -212,7 +210,9 @@ const calendarActionHandlers = {
return function(dispatch, getState) { return function(dispatch, getState) {
const state = getState(); const state = getState();
const view = payload.view; const view = payload.view;
const time = view === calendarViews.FORECAST ? moment() : state.calendar.time; const time = view === calendarViews.FORECAST ?
moment() :
state.calendar.time;
dispatch(fetchCalendar({ time, view })); dispatch(fetchCalendar({ time, view }));
}; };

@ -206,7 +206,10 @@ const queueActionHandlers = {
}); });
promise.done((data) => { promise.done((data) => {
dispatch(fetchQueue()); dispatch(batchActions([
set({ section, isRemoving: false }),
fetchQueue()
]));
}); });
promise.fail((xhr) => { promise.fail((xhr) => {

@ -21,14 +21,26 @@ export const defaultState = {
detailedProgressBar: false, detailedProgressBar: false,
size: 'large', size: 'large',
showTitle: false, showTitle: false,
showQualityProfile: false showQualityProfile: true
}, },
bannerOptions: { bannerOptions: {
detailedProgressBar: false, detailedProgressBar: false,
size: 'large', size: 'large',
showTitle: false, showTitle: false,
showQualityProfile: false showQualityProfile: true
},
overviewOptions: {
detailedProgressBar: false,
size: 'medium',
showNetwork: true,
showQualityProfile: true,
showPreviousAiring: false,
showAdded: false,
showAlbumCount: true,
showPath: false,
showSizeOnDisk: false
}, },
columns: [ columns: [
@ -175,7 +187,8 @@ export const persistState = [
'artistIndex.view', 'artistIndex.view',
'artistIndex.columns', 'artistIndex.columns',
'artistIndex.posterOptions', 'artistIndex.posterOptions',
'artistIndex.bannerOptions' 'artistIndex.bannerOptions',
'artistIndex.overviewOptions'
]; ];
const reducerSection = 'artistIndex'; const reducerSection = 'artistIndex';
@ -215,6 +228,18 @@ const artistIndexReducers = handleActions({
...payload ...payload
} }
}; };
},
[types.SET_ARTIST_OVERVIEW_OPTION]: function(state, { payload }) {
const overviewOptions = state.overviewOptions;
return {
...state,
overviewOptions: {
...overviewOptions,
...payload
}
};
} }
}, defaultState); }, defaultState);

@ -36,8 +36,10 @@ module.exports = {
progressBarLargeHeight: '20px', progressBarLargeHeight: '20px',
// Jump Bar // Jump Bar
jumpBarItemHeight: '25px' jumpBarItemHeight: '25px',
// Series
// Artist
artistIndexColumnPadding: '20px',
artistIndexColumnPaddingSmallScreen: '10px',
artistIndexOverviewInfoRowHeight: '21px'
}; };

@ -7,5 +7,7 @@ module.exports = {
extraSmallFontSize: '11px', extraSmallFontSize: '11px',
smallFontSize: '12px', smallFontSize: '12px',
defaultFontSize: '14px', defaultFontSize: '14px',
largeFontSize: '16px' largeFontSize: '16px',
lineHeight: '1.528571429'
}; };

@ -77,8 +77,8 @@
"react-async-script": "0.9.1", "react-async-script": "0.9.1",
"react-autosuggest": "9.3.2", "react-autosuggest": "9.3.2",
"react-custom-scrollbars": "4.1.2", "react-custom-scrollbars": "4.1.2",
"react-dnd": "2.5.3", "react-dnd": "2.5.4",
"react-dnd-html5-backend": "2.5.3", "react-dnd-html5-backend": "2.5.4",
"react-document-title": "2.0.3", "react-document-title": "2.0.3",
"react-dom": "15.6.0", "react-dom": "15.6.0",
"react-google-recaptcha": "0.9.7", "react-google-recaptcha": "0.9.7",
@ -92,6 +92,7 @@
"react-tabs": "2.1.0", "react-tabs": "2.1.0",
"react-tag-autocomplete": "5.4.1", "react-tag-autocomplete": "5.4.1",
"react-tether": "0.5.7", "react-tether": "0.5.7",
"react-truncate": "2.1.5",
"react-virtualized": "9.10.1", "react-virtualized": "9.10.1",
"redux": "3.7.2", "redux": "3.7.2",
"redux-actions": "2.2.1", "redux-actions": "2.2.1",

@ -81,7 +81,7 @@ namespace Lidarr.Api.V3.Calendar
//occurrence.Description = album.Overview; //occurrence.Description = album.Overview;
//occurrence.Categories = new List<string>() { album.Series.Network }; //occurrence.Categories = new List<string>() { album.Series.Network };
occurrence.Start = new CalDateTime(album.ReleaseDate.Value) { HasTime = false }; occurrence.Start = new CalDateTime(album.ReleaseDate.Value.ToLocalTime()) { HasTime = false };
occurrence.Summary = $"{album.Artist.Name} - {album.Title}"; occurrence.Summary = $"{album.Artist.Name} - {album.Title}";
} }

@ -1921,9 +1921,9 @@ disposables@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/disposables/-/disposables-1.0.1.tgz#064727a25b54f502bd82b89aa2dfb8df9f1b39e3" resolved "https://registry.yarnpkg.com/disposables/-/disposables-1.0.1.tgz#064727a25b54f502bd82b89aa2dfb8df9f1b39e3"
dnd-core@^2.5.3: dnd-core@^2.5.4:
version "2.5.3" version "2.5.4"
resolved "https://registry.yarnpkg.com/dnd-core/-/dnd-core-2.5.3.tgz#a4163d59a01779860d2edbdadab5183ce321147c" resolved "https://registry.yarnpkg.com/dnd-core/-/dnd-core-2.5.4.tgz#0c70a8dcbb609c0b222e275fcae9fa83e5897397"
dependencies: dependencies:
asap "^2.0.6" asap "^2.0.6"
invariant "^2.0.0" invariant "^2.0.0"
@ -5246,18 +5246,18 @@ react-custom-scrollbars@4.1.2:
prop-types "^15.5.10" prop-types "^15.5.10"
raf "^3.1.0" raf "^3.1.0"
react-dnd-html5-backend@2.5.3: react-dnd-html5-backend@2.5.4:
version "2.5.3" version "2.5.4"
resolved "https://registry.yarnpkg.com/react-dnd-html5-backend/-/react-dnd-html5-backend-2.5.3.tgz#1158112e8129dfe672d4c72222f266dd05321f03" resolved "https://registry.yarnpkg.com/react-dnd-html5-backend/-/react-dnd-html5-backend-2.5.4.tgz#974ad083f67b12d56977a5b171f5ffeb29d78352"
dependencies: dependencies:
lodash "^4.2.0" lodash "^4.2.0"
react-dnd@2.5.3: react-dnd@2.5.4:
version "2.5.3" version "2.5.4"
resolved "https://registry.yarnpkg.com/react-dnd/-/react-dnd-2.5.3.tgz#ee5357da19d4ebd0e0da0a070a862bbc133e3241" resolved "https://registry.yarnpkg.com/react-dnd/-/react-dnd-2.5.4.tgz#0b6dc5e9d0dfc2909f4f4fe736e5534f3afd1bd9"
dependencies: dependencies:
disposables "^1.0.1" disposables "^1.0.1"
dnd-core "^2.5.3" dnd-core "^2.5.4"
hoist-non-react-statics "^2.1.0" hoist-non-react-statics "^2.1.0"
invariant "^2.1.0" invariant "^2.1.0"
lodash "^4.2.0" lodash "^4.2.0"
@ -5382,6 +5382,10 @@ react-themeable@^1.1.0:
dependencies: dependencies:
object-assign "^3.0.0" object-assign "^3.0.0"
react-truncate@2.1.5:
version "2.1.5"
resolved "https://registry.yarnpkg.com/react-truncate/-/react-truncate-2.1.5.tgz#c33c1bf7053c2800d4012fefab09a18013019721"
react-virtualized@9.10.1: react-virtualized@9.10.1:
version "9.10.1" version "9.10.1"
resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-9.10.1.tgz#d32365d0edf49debbe25fbfe73b5f55f6d9d8c72" resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-9.10.1.tgz#d32365d0edf49debbe25fbfe73b5f55f6d9d8c72"

Loading…
Cancel
Save