1
0
mirror of https://github.com/Radarr/Radarr.git synced 2024-10-26 22:52:40 +02:00

New: Add and Edit People Lists from Movie Details Page

This commit is contained in:
Qstick 2019-12-29 01:21:01 -05:00
parent 8021381de2
commit b3caa87b78
17 changed files with 377 additions and 444 deletions

View File

@ -8,7 +8,7 @@ import { saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions';
import { fetchCustomFilters } from 'Store/Actions/customFilterActions'; import { fetchCustomFilters } from 'Store/Actions/customFilterActions';
import { fetchMovies } from 'Store/Actions/movieActions'; import { fetchMovies } from 'Store/Actions/movieActions';
import { fetchTags } from 'Store/Actions/tagActions'; import { fetchTags } from 'Store/Actions/tagActions';
import { fetchQualityProfiles, fetchUISettings, fetchLanguages } from 'Store/Actions/settingsActions'; import { fetchQualityProfiles, fetchUISettings, fetchLanguages, fetchNetImports } from 'Store/Actions/settingsActions';
import { fetchStatus } from 'Store/Actions/systemActions'; import { fetchStatus } from 'Store/Actions/systemActions';
import ErrorPage from './ErrorPage'; import ErrorPage from './ErrorPage';
import LoadingPage from './LoadingPage'; import LoadingPage from './LoadingPage';
@ -48,6 +48,7 @@ const selectIsPopulated = createSelector(
(state) => state.settings.ui.isPopulated, (state) => state.settings.ui.isPopulated,
(state) => state.settings.qualityProfiles.isPopulated, (state) => state.settings.qualityProfiles.isPopulated,
(state) => state.settings.languages.isPopulated, (state) => state.settings.languages.isPopulated,
(state) => state.settings.netImports.isPopulated,
(state) => state.system.status.isPopulated, (state) => state.system.status.isPopulated,
( (
customFiltersIsPopulated, customFiltersIsPopulated,
@ -55,6 +56,7 @@ const selectIsPopulated = createSelector(
uiSettingsIsPopulated, uiSettingsIsPopulated,
qualityProfilesIsPopulated, qualityProfilesIsPopulated,
languagesIsPopulated, languagesIsPopulated,
netImportsIsPopulated,
systemStatusIsPopulated systemStatusIsPopulated
) => { ) => {
return ( return (
@ -63,6 +65,7 @@ const selectIsPopulated = createSelector(
uiSettingsIsPopulated && uiSettingsIsPopulated &&
qualityProfilesIsPopulated && qualityProfilesIsPopulated &&
languagesIsPopulated && languagesIsPopulated &&
netImportsIsPopulated &&
systemStatusIsPopulated systemStatusIsPopulated
); );
} }
@ -74,6 +77,7 @@ const selectErrors = createSelector(
(state) => state.settings.ui.error, (state) => state.settings.ui.error,
(state) => state.settings.qualityProfiles.error, (state) => state.settings.qualityProfiles.error,
(state) => state.settings.languages.error, (state) => state.settings.languages.error,
(state) => state.settings.netImports.error,
(state) => state.system.status.error, (state) => state.system.status.error,
( (
customFiltersError, customFiltersError,
@ -81,6 +85,7 @@ const selectErrors = createSelector(
uiSettingsError, uiSettingsError,
qualityProfilesError, qualityProfilesError,
languagesError, languagesError,
netImportsError,
systemStatusError systemStatusError
) => { ) => {
const hasError = !!( const hasError = !!(
@ -89,6 +94,7 @@ const selectErrors = createSelector(
uiSettingsError || uiSettingsError ||
qualityProfilesError || qualityProfilesError ||
languagesError || languagesError ||
netImportsError ||
systemStatusError systemStatusError
); );
@ -99,6 +105,7 @@ const selectErrors = createSelector(
uiSettingsError, uiSettingsError,
qualityProfilesError, qualityProfilesError,
languagesError, languagesError,
netImportsError,
systemStatusError systemStatusError
}; };
} }
@ -146,6 +153,9 @@ function createMapDispatchToProps(dispatch, props) {
dispatchFetchLanguages() { dispatchFetchLanguages() {
dispatch(fetchLanguages()); dispatch(fetchLanguages());
}, },
dispatchFetchNetImports() {
dispatch(fetchNetImports());
},
dispatchFetchUISettings() { dispatchFetchUISettings() {
dispatch(fetchUISettings()); dispatch(fetchUISettings());
}, },
@ -181,6 +191,7 @@ class PageConnector extends Component {
this.props.dispatchFetchTags(); this.props.dispatchFetchTags();
this.props.dispatchFetchQualityProfiles(); this.props.dispatchFetchQualityProfiles();
this.props.dispatchFetchLanguages(); this.props.dispatchFetchLanguages();
this.props.dispatchFetchNetImports();
this.props.dispatchFetchUISettings(); this.props.dispatchFetchUISettings();
this.props.dispatchFetchStatus(); this.props.dispatchFetchStatus();
} }
@ -204,6 +215,7 @@ class PageConnector extends Component {
dispatchFetchTags, dispatchFetchTags,
dispatchFetchQualityProfiles, dispatchFetchQualityProfiles,
dispatchFetchLanguages, dispatchFetchLanguages,
dispatchFetchNetImports,
dispatchFetchUISettings, dispatchFetchUISettings,
dispatchFetchStatus, dispatchFetchStatus,
...otherProps ...otherProps
@ -242,6 +254,7 @@ PageConnector.propTypes = {
dispatchFetchTags: PropTypes.func.isRequired, dispatchFetchTags: PropTypes.func.isRequired,
dispatchFetchQualityProfiles: PropTypes.func.isRequired, dispatchFetchQualityProfiles: PropTypes.func.isRequired,
dispatchFetchLanguages: PropTypes.func.isRequired, dispatchFetchLanguages: PropTypes.func.isRequired,
dispatchFetchNetImports: PropTypes.func.isRequired,
dispatchFetchUISettings: PropTypes.func.isRequired, dispatchFetchUISettings: PropTypes.func.isRequired,
dispatchFetchStatus: PropTypes.func.isRequired, dispatchFetchStatus: PropTypes.func.isRequired,
onSidebarVisibleChange: PropTypes.func.isRequired onSidebarVisibleChange: PropTypes.func.isRequired

View File

@ -1,25 +0,0 @@
import _ from 'lodash';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import MovieCastPosters from './MovieCastPosters';
function createMapStateToProps() {
return createSelector(
(state) => state.moviePeople.items,
(people) => {
const cast = _.reduce(people, (acc, person) => {
if (person.type === 'cast') {
acc.push(person);
}
return acc;
}, []);
return {
cast
};
}
);
}
export default connect(createMapStateToProps)(MovieCastPosters);

View File

@ -4,7 +4,8 @@ import { icons } from 'Helpers/Props';
import IconButton from 'Components/Link/IconButton'; import IconButton from 'Components/Link/IconButton';
import Label from 'Components/Label'; import Label from 'Components/Label';
import MovieHeadshot from 'Movie/MovieHeadshot'; import MovieHeadshot from 'Movie/MovieHeadshot';
import styles from './MovieCastPoster.css'; import EditNetImportModalConnector from 'Settings/NetImport/NetImport/EditNetImportModalConnector';
import styles from '../MovieCreditPoster.css';
class MovieCastPoster extends Component { class MovieCastPoster extends Component {
@ -16,19 +17,24 @@ class MovieCastPoster extends Component {
this.state = { this.state = {
hasPosterError: false, hasPosterError: false,
isEditMovieModalOpen: false isEditNetImportModalOpen: false
}; };
} }
// //
// Listeners // Listeners
onEditMoviePress = () => { onEditNetImportPress = () => {
this.setState({ isEditMovieModalOpen: true }); this.setState({ isEditNetImportModalOpen: true });
} }
onEditMovieModalClose = () => { onAddNetImportPress = () => {
this.setState({ isEditMovieModalOpen: false }); this.props.onNetImportSelect();
this.setState({ isEditNetImportModalOpen: true });
}
onEditNetImportModalClose = () => {
this.setState({ isEditNetImportModalOpen: false });
} }
onPosterLoad = () => { onPosterLoad = () => {
@ -48,11 +54,12 @@ class MovieCastPoster extends Component {
render() { render() {
const { const {
castName, personName,
character, character,
images, images,
posterWidth, posterWidth,
posterHeight posterHeight,
netImportId
} = this.props; } = this.props;
const { const {
@ -68,12 +75,21 @@ class MovieCastPoster extends Component {
<div className={styles.content}> <div className={styles.content}>
<div className={styles.posterContainer}> <div className={styles.posterContainer}>
<Label className={styles.controls}> <Label className={styles.controls}>
<IconButton {
className={styles.action} netImportId > 0 ?
name={icons.EDIT} <IconButton
title="Edit movie" className={styles.action}
onPress={this.onEditMoviePress} name={icons.EDIT}
/> title="Edit Person"
onPress={this.onEditNetImportPress}
/> :
<IconButton
className={styles.action}
name={icons.ADD}
title="Follow Person"
onPress={this.onAddNetImportPress}
/>
}
</Label> </Label>
<div <div
@ -94,30 +110,43 @@ class MovieCastPoster extends Component {
{ {
hasPosterError && hasPosterError &&
<div className={styles.overlayTitle}> <div className={styles.overlayTitle}>
{castName} {personName}
</div> </div>
} }
</div> </div>
</div> </div>
<div className={styles.title}> <div className={styles.title}>
{castName} {personName}
</div> </div>
<div className={styles.title}> <div className={styles.title}>
{character} {character}
</div> </div>
<EditNetImportModalConnector
id={netImportId}
isOpen={this.state.isEditNetImportModalOpen}
onModalClose={this.onEditNetImportModalClose}
onDeleteNetImportPress={this.onDeleteNetImportPress}
/>
</div> </div>
); );
} }
} }
MovieCastPoster.propTypes = { MovieCastPoster.propTypes = {
castId: PropTypes.number.isRequired, tmdbId: PropTypes.number.isRequired,
castName: PropTypes.string.isRequired, personName: PropTypes.string.isRequired,
character: PropTypes.string.isRequired, character: PropTypes.string.isRequired,
images: PropTypes.arrayOf(PropTypes.object).isRequired, images: PropTypes.arrayOf(PropTypes.object).isRequired,
posterWidth: PropTypes.number.isRequired, posterWidth: PropTypes.number.isRequired,
posterHeight: PropTypes.number.isRequired posterHeight: PropTypes.number.isRequired,
netImportId: PropTypes.number.isRequired,
onNetImportSelect: PropTypes.func.isRequired
};
MovieCastPoster.defaultProps = {
netImportId: 0
}; };
export default MovieCastPoster; export default MovieCastPoster;

View File

@ -0,0 +1,60 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import MovieCreditPosters from '../MovieCreditPosters';
import MovieCastPoster from './MovieCastPoster';
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
function createMapStateToProps() {
return createSelector(
(state) => state.movieCredits.items,
(credits) => {
const cast = _.reduce(credits, (acc, credit) => {
if (credit.type === 'cast') {
acc.push(credit);
}
return acc;
}, []);
return {
items: cast
};
}
);
}
const mapDispatchToProps = {
fetchRootFolders
};
class MovieCastPostersConnector extends Component {
//
// Lifecycle
componentDidMount() {
this.props.fetchRootFolders();
}
//
// Render
render() {
return (
<MovieCreditPosters
{...this.props}
itemComponent={MovieCastPoster}
/>
);
}
}
MovieCastPostersConnector.propTypes = {
fetchRootFolders: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(MovieCastPostersConnector);

View File

@ -4,7 +4,8 @@ import { icons } from 'Helpers/Props';
import IconButton from 'Components/Link/IconButton'; import IconButton from 'Components/Link/IconButton';
import Label from 'Components/Label'; import Label from 'Components/Label';
import MovieHeadshot from 'Movie/MovieHeadshot'; import MovieHeadshot from 'Movie/MovieHeadshot';
import styles from './MovieCrewPoster.css'; import EditNetImportModalConnector from 'Settings/NetImport/NetImport/EditNetImportModalConnector';
import styles from '../MovieCreditPoster.css';
class MovieCrewPoster extends Component { class MovieCrewPoster extends Component {
@ -16,19 +17,24 @@ class MovieCrewPoster extends Component {
this.state = { this.state = {
hasPosterError: false, hasPosterError: false,
isEditMovieModalOpen: false isEditNetImportModalOpen: false
}; };
} }
// //
// Listeners // Listeners
onEditMoviePress = () => { onEditNetImportPress = () => {
this.setState({ isEditMovieModalOpen: true }); this.setState({ isEditNetImportModalOpen: true });
} }
onEditMovieModalClose = () => { onAddNetImportPress = () => {
this.setState({ isEditMovieModalOpen: false }); this.props.onNetImportSelect();
this.setState({ isEditNetImportModalOpen: true });
}
onEditNetImportModalClose = () => {
this.setState({ isEditNetImportModalOpen: false });
} }
onPosterLoad = () => { onPosterLoad = () => {
@ -48,11 +54,12 @@ class MovieCrewPoster extends Component {
render() { render() {
const { const {
crewName, personName,
job, job,
images, images,
posterWidth, posterWidth,
posterHeight posterHeight,
netImportId
} = this.props; } = this.props;
const { const {
@ -68,12 +75,21 @@ class MovieCrewPoster extends Component {
<div className={styles.content}> <div className={styles.content}>
<div className={styles.posterContainer}> <div className={styles.posterContainer}>
<Label className={styles.controls}> <Label className={styles.controls}>
<IconButton {
className={styles.action} netImportId > 0 ?
name={icons.EDIT} <IconButton
title="Edit movie" className={styles.action}
onPress={this.onEditMoviePress} name={icons.EDIT}
/> title="Edit Person"
onPress={this.onEditNetImportPress}
/> :
<IconButton
className={styles.action}
name={icons.ADD}
title="Follow Person"
onPress={this.onAddNetImportPress}
/>
}
</Label> </Label>
<div <div
@ -94,30 +110,43 @@ class MovieCrewPoster extends Component {
{ {
hasPosterError && hasPosterError &&
<div className={styles.overlayTitle}> <div className={styles.overlayTitle}>
{crewName} {personName}
</div> </div>
} }
</div> </div>
</div> </div>
<div className={styles.title}> <div className={styles.title}>
{crewName} {personName}
</div> </div>
<div className={styles.title}> <div className={styles.title}>
{job} {job}
</div> </div>
<EditNetImportModalConnector
id={netImportId}
isOpen={this.state.isEditNetImportModalOpen}
onModalClose={this.onEditNetImportModalClose}
onDeleteNetImportPress={this.onDeleteNetImportPress}
/>
</div> </div>
); );
} }
} }
MovieCrewPoster.propTypes = { MovieCrewPoster.propTypes = {
crewId: PropTypes.number.isRequired, tmdbId: PropTypes.number.isRequired,
crewName: PropTypes.string.isRequired, personName: PropTypes.string.isRequired,
job: PropTypes.string.isRequired, job: PropTypes.string.isRequired,
images: PropTypes.arrayOf(PropTypes.object).isRequired, images: PropTypes.arrayOf(PropTypes.object).isRequired,
posterWidth: PropTypes.number.isRequired, posterWidth: PropTypes.number.isRequired,
posterHeight: PropTypes.number.isRequired posterHeight: PropTypes.number.isRequired,
netImportId: PropTypes.number.isRequired,
onNetImportSelect: PropTypes.func.isRequired
};
MovieCrewPoster.defaultProps = {
netImportId: 0
}; };
export default MovieCrewPoster; export default MovieCrewPoster;

View File

@ -0,0 +1,60 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import MovieCreditPosters from '../MovieCreditPosters';
import MovieCrewPoster from './MovieCrewPoster';
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
function createMapStateToProps() {
return createSelector(
(state) => state.movieCredits.items,
(credits) => {
const crew = _.reduce(credits, (acc, credit) => {
if (credit.type === 'crew') {
acc.push(credit);
}
return acc;
}, []);
return {
items: crew
};
}
);
}
const mapDispatchToProps = {
fetchRootFolders
};
class MovieCrewPostersConnector extends Component {
//
// Lifecycle
componentDidMount() {
this.props.fetchRootFolders();
}
//
// Render
render() {
return (
<MovieCreditPosters
{...this.props}
itemComponent={MovieCrewPoster}
/>
);
}
}
MovieCrewPostersConnector.propTypes = {
fetchRootFolders: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(MovieCrewPostersConnector);

View File

@ -0,0 +1,58 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import createMovieCreditListSelector from 'Store/Selectors/createMovieCreditListSelector';
import { selectNetImportSchema, setNetImportValue, setNetImportFieldValue } from 'Store/Actions/settingsActions';
function createMapStateToProps() {
return createMovieCreditListSelector();
}
const mapDispatchToProps = {
selectNetImportSchema,
setNetImportFieldValue,
setNetImportValue
};
class MovieCreditPosterConnector extends Component {
//
// Listeners
onNetImportSelect = () => {
this.props.selectNetImportSchema({ implementation: 'TMDbPersonImport', presetName: undefined });
this.props.setNetImportFieldValue({ name: 'personId', value: this.props.tmdbId.toString() });
this.props.setNetImportValue({ name: 'name', value: `${this.props.personName} - ${this.props.tmdbId}` });
}
//
// Render
render() {
const {
tmdbId,
component: ItemComponent,
personName
} = this.props;
return (
<ItemComponent
{...this.props}
tmdbId={tmdbId}
personName={personName}
onNetImportSelect={this.onNetImportSelect}
/>
);
}
}
MovieCreditPosterConnector.propTypes = {
tmdbId: PropTypes.number.isRequired,
personName: PropTypes.string.isRequired,
component: PropTypes.elementType.isRequired,
selectNetImportSchema: PropTypes.func.isRequired,
setNetImportFieldValue: PropTypes.func.isRequired,
setNetImportValue: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(MovieCreditPosterConnector);

View File

@ -4,8 +4,8 @@ import { Grid, WindowScroller } from 'react-virtualized';
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder'; import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
import dimensions from 'Styles/Variables/dimensions'; import dimensions from 'Styles/Variables/dimensions';
import Measure from 'Components/Measure'; import Measure from 'Components/Measure';
import MovieCastPoster from './MovieCastPoster'; import MovieCreditPosterConnector from './MovieCreditPosterConnector';
import styles from './MovieCastPosters.css'; import styles from './MovieCreditPosters.css';
// Poster container dimensions // Poster container dimensions
const columnPadding = parseInt(dimensions.movieIndexColumnPadding); const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
@ -47,7 +47,7 @@ function calculatePosterHeight(posterWidth) {
return Math.ceil((250 / 170) * posterWidth); return Math.ceil((250 / 170) * posterWidth);
} }
class MovieCastPosters extends Component { class MovieCreditPosters extends Component {
// //
// Lifecycle // Lifecycle
@ -70,7 +70,7 @@ class MovieCastPosters extends Component {
componentDidUpdate(prevProps, prevState) { componentDidUpdate(prevProps, prevState) {
const { const {
cast items
} = this.props; } = this.props;
const { const {
@ -85,7 +85,7 @@ class MovieCastPosters extends Component {
prevState.columnWidth !== columnWidth || prevState.columnWidth !== columnWidth ||
prevState.columnCount !== columnCount || prevState.columnCount !== columnCount ||
prevState.rowHeight !== rowHeight || prevState.rowHeight !== rowHeight ||
hasDifferentItemsOrOrder(prevProps.cast, cast))) { hasDifferentItemsOrOrder(prevProps.items, items))) {
// recomputeGridSize also forces Grid to discard its cache of rendered cells // recomputeGridSize also forces Grid to discard its cache of rendered cells
this._grid.recomputeGridSize(); this._grid.recomputeGridSize();
} }
@ -119,7 +119,8 @@ class MovieCastPosters extends Component {
cellRenderer = ({ key, rowIndex, columnIndex, style }) => { cellRenderer = ({ key, rowIndex, columnIndex, style }) => {
const { const {
cast items,
itemComponent
} = this.props; } = this.props;
const { const {
@ -129,7 +130,7 @@ class MovieCastPosters extends Component {
} = this.state; } = this.state;
const movieIdx = rowIndex * columnCount + columnIndex; const movieIdx = rowIndex * columnCount + columnIndex;
const movie = cast[movieIdx]; const movie = items[movieIdx];
if (!movie) { if (!movie) {
return null; return null;
@ -141,12 +142,14 @@ class MovieCastPosters extends Component {
key={key} key={key}
style={style} style={style}
> >
<MovieCastPoster <MovieCreditPosterConnector
key={movie.order} key={movie.order}
component={itemComponent}
posterWidth={posterWidth} posterWidth={posterWidth}
posterHeight={posterHeight} posterHeight={posterHeight}
castId={movie.tmdbId} tmdbId={movie.personTmdbId}
castName={movie.name} personName={movie.personName}
job={movie.job}
character={movie.character} character={movie.character}
images={movie.images} images={movie.images}
/> />
@ -166,7 +169,7 @@ class MovieCastPosters extends Component {
render() { render() {
const { const {
cast items
} = this.props; } = this.props;
const { const {
@ -176,7 +179,7 @@ class MovieCastPosters extends Component {
rowHeight rowHeight
} = this.state; } = this.state;
const rowCount = Math.ceil(cast.length / columnCount); const rowCount = Math.ceil(items.length / columnCount);
return ( return (
<Measure <Measure
@ -220,9 +223,10 @@ class MovieCastPosters extends Component {
} }
} }
MovieCastPosters.propTypes = { MovieCreditPosters.propTypes = {
cast: PropTypes.arrayOf(PropTypes.object).isRequired, items: PropTypes.arrayOf(PropTypes.object).isRequired,
itemComponent: PropTypes.elementType.isRequired,
isSmallScreen: PropTypes.bool.isRequired isSmallScreen: PropTypes.bool.isRequired
}; };
export default MovieCastPosters; export default MovieCreditPosters;

View File

@ -1,76 +0,0 @@
$hoverScale: 1.05;
.content {
transition: all 200ms ease-in;
&:hover {
z-index: 2;
box-shadow: 0 0 12px $black;
transition: all 200ms ease-in;
.controls {
opacity: 0.9;
transition: opacity 200ms linear 150ms;
}
}
}
.posterContainer {
position: relative;
}
.poster {
position: relative;
display: block;
background-color: $defaultColor;
}
.overlayTitle {
position: absolute;
top: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 5px;
width: 100%;
height: 100%;
color: $offWhite;
text-align: center;
font-size: 20px;
}
.title {
@add-mixin truncate;
background-color: #fafbfc;
text-align: center;
font-size: $smallFontSize;
}
.controls {
position: absolute;
bottom: 10px;
left: 10px;
z-index: 3;
border-radius: 4px;
background-color: #707070;
color: $white;
font-size: $smallFontSize;
opacity: 0;
transition: opacity 0;
}
.action {
composes: button from '~Components/Link/IconButton.css';
&:hover {
color: $radarrYellow;
}
}
@media only screen and (max-width: $breakpointSmall) {
.container {
padding: 5px;
}
}

View File

@ -1,7 +0,0 @@
.grid {
flex: 1 0 auto;
}
.container {
padding: 10px;
}

View File

@ -1,228 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { Grid, WindowScroller } from 'react-virtualized';
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
import dimensions from 'Styles/Variables/dimensions';
import Measure from 'Components/Measure';
import MovieCrewPoster from './MovieCrewPoster';
import styles from './MovieCrewPosters.css';
// Poster container dimensions
const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
const columnPaddingSmallScreen = parseInt(dimensions.movieIndexColumnPaddingSmallScreen);
const additionalColumnCount = {
small: 3,
medium: 2,
large: 1
};
function calculateColumnWidth(width, posterSize, isSmallScreen) {
const maxiumColumnWidth = isSmallScreen ? 172 : 182;
const columns = Math.floor(width / maxiumColumnWidth);
const remainder = width % maxiumColumnWidth;
if (remainder === 0 && posterSize === 'large') {
return maxiumColumnWidth;
}
return Math.floor(width / (columns + additionalColumnCount[posterSize]));
}
function calculateRowHeight(posterHeight, isSmallScreen) {
const titleHeight = 19;
const characterHeight = 19;
const heights = [
posterHeight,
titleHeight,
characterHeight,
isSmallScreen ? columnPaddingSmallScreen : columnPadding
];
return heights.reduce((acc, height) => acc + height, 0);
}
function calculatePosterHeight(posterWidth) {
return Math.ceil((250 / 170) * posterWidth);
}
class MovieCrewPosters extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
width: 0,
columnWidth: 182,
columnCount: 1,
posterWidth: 162,
posterHeight: 238,
rowHeight: calculateRowHeight(238, props.isSmallScreen)
};
this._isInitialized = false;
this._grid = null;
}
componentDidUpdate(prevProps, prevState) {
const {
crew
} = this.props;
const {
width,
columnWidth,
columnCount,
rowHeight
} = this.state;
if (this._grid &&
(prevState.width !== width ||
prevState.columnWidth !== columnWidth ||
prevState.columnCount !== columnCount ||
prevState.rowHeight !== rowHeight ||
hasDifferentItemsOrOrder(prevProps.crew, crew))) {
// recomputeGridSize also forces Grid to discard its cache of rendered cells
this._grid.recomputeGridSize();
}
}
//
// Control
setGridRef = (ref) => {
this._grid = ref;
}
calculateGrid = (width = this.state.width, isSmallScreen) => {
const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding;
const columnWidth = calculateColumnWidth(width, 'small', isSmallScreen);
const columnCount = Math.max(Math.floor(width / columnWidth), 1);
const posterWidth = columnWidth - padding;
const posterHeight = calculatePosterHeight(posterWidth);
const rowHeight = calculateRowHeight(posterHeight, isSmallScreen);
this.setState({
width,
columnWidth,
columnCount,
posterWidth,
posterHeight,
rowHeight
});
}
cellRenderer = ({ key, rowIndex, columnIndex, style }) => {
const {
crew
} = this.props;
const {
posterWidth,
posterHeight,
columnCount
} = this.state;
const movieIdx = rowIndex * columnCount + columnIndex;
const movie = crew[movieIdx];
if (!movie) {
return null;
}
return (
<div
className={styles.container}
key={key}
style={style}
>
<MovieCrewPoster
key={movie.order}
posterWidth={posterWidth}
posterHeight={posterHeight}
crewId={movie.tmdbId}
crewName={movie.name}
job={movie.job}
images={movie.images}
/>
</div>
);
}
//
// Listeners
onMeasure = ({ width }) => {
this.calculateGrid(width, this.props.isSmallScreen);
}
//
// Render
render() {
const {
crew
} = this.props;
const {
width,
columnWidth,
columnCount,
rowHeight
} = this.state;
const rowCount = Math.ceil(crew.length / columnCount);
return (
<Measure
whitelist={['width']}
onMeasure={this.onMeasure}
>
<WindowScroller
scrollElement={undefined}
>
{({ height, registerChild, onChildScroll, scrollTop }) => {
if (!height) {
return <div />;
}
return (
<div ref={registerChild}>
<Grid
ref={this.setGridRef}
className={styles.grid}
autoHeight={true}
height={height}
columnCount={columnCount}
columnWidth={columnWidth}
rowCount={rowCount}
rowHeight={rowHeight}
width={width}
onScroll={onChildScroll}
scrollTop={scrollTop}
overscanRowCount={2}
cellRenderer={this.cellRenderer}
scrollToAlignment={'start'}
isScrollingOptOut={true}
/>
</div>
);
}
}
</WindowScroller>
</Measure>
);
}
}
MovieCrewPosters.propTypes = {
crew: PropTypes.arrayOf(PropTypes.object).isRequired,
isSmallScreen: PropTypes.bool.isRequired
};
export default MovieCrewPosters;

View File

@ -1,25 +0,0 @@
import _ from 'lodash';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import MovieCrewPosters from './MovieCrewPosters';
function createMapStateToProps() {
return createSelector(
(state) => state.moviePeople.items,
(people) => {
const crew = _.reduce(people, (acc, person) => {
if (person.type === 'crew') {
acc.push(person);
}
return acc;
}, []);
return {
crew
};
}
);
}
export default connect(createMapStateToProps)(MovieCrewPosters);

View File

@ -31,8 +31,8 @@ import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal'; import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
import MovieHistoryTable from 'Movie/History/MovieHistoryTable'; import MovieHistoryTable from 'Movie/History/MovieHistoryTable';
import MovieTitlesTable from './Titles/MovieTitlesTable'; import MovieTitlesTable from './Titles/MovieTitlesTable';
import MovieCastPostersConnector from './Cast/MovieCastPostersConnector'; import MovieCastPostersConnector from './Credits/Cast/MovieCastPostersConnector';
import MovieCrewPostersConnector from './Crew/MovieCrewPostersConnector'; import MovieCrewPostersConnector from './Credits/Crew/MovieCrewPostersConnector';
import MovieAlternateTitles from './MovieAlternateTitles'; import MovieAlternateTitles from './MovieAlternateTitles';
import MovieDetailsLinks from './MovieDetailsLinks'; import MovieDetailsLinks from './MovieDetailsLinks';
import InteractiveSearchTable from 'InteractiveSearch/InteractiveSearchTable'; import InteractiveSearchTable from 'InteractiveSearch/InteractiveSearchTable';
@ -181,7 +181,7 @@ class MovieDetails extends Component {
isPopulated, isPopulated,
isSmallScreen, isSmallScreen,
movieFilesError, movieFilesError,
moviePeopleError, movieCreditsError,
hasMovieFiles, hasMovieFiles,
previousMovie, previousMovie,
nextMovie, nextMovie,
@ -464,12 +464,12 @@ class MovieDetails extends Component {
<div className={styles.contentContainer}> <div className={styles.contentContainer}>
{ {
!isPopulated && !movieFilesError && !moviePeopleError && !isPopulated && !movieFilesError && !movieCreditsError &&
<LoadingIndicator /> <LoadingIndicator />
} }
{ {
!isFetching && movieFilesError && !moviePeopleError && !isFetching && movieFilesError && !movieCreditsError &&
<div>Loading movie files failed</div> <div>Loading movie files failed</div>
} }
@ -629,7 +629,7 @@ MovieDetails.propTypes = {
isPopulated: PropTypes.bool.isRequired, isPopulated: PropTypes.bool.isRequired,
isSmallScreen: PropTypes.bool.isRequired, isSmallScreen: PropTypes.bool.isRequired,
movieFilesError: PropTypes.object, movieFilesError: PropTypes.object,
moviePeopleError: PropTypes.object, movieCreditsError: PropTypes.object,
hasMovieFiles: PropTypes.bool.isRequired, hasMovieFiles: PropTypes.bool.isRequired,
previousMovie: PropTypes.object.isRequired, previousMovie: PropTypes.object.isRequired,
nextMovie: PropTypes.object.isRequired, nextMovie: PropTypes.object.isRequired,

View File

@ -9,10 +9,11 @@ import createAllMoviesSelector from 'Store/Selectors/createAllMoviesSelector';
import createCommandsSelector from 'Store/Selectors/createCommandsSelector'; import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import { fetchMovieFiles, clearMovieFiles } from 'Store/Actions/movieFileActions'; import { fetchMovieFiles, clearMovieFiles } from 'Store/Actions/movieFileActions';
import { fetchMoviePeople, clearMoviePeople } from 'Store/Actions/moviePeopleActions'; import { fetchMovieCredits, clearMovieCredits } from 'Store/Actions/movieCreditsActions';
import { toggleMovieMonitored } from 'Store/Actions/movieActions'; import { toggleMovieMonitored } from 'Store/Actions/movieActions';
import { fetchQueueDetails, clearQueueDetails } from 'Store/Actions/queueActions'; import { fetchQueueDetails, clearQueueDetails } from 'Store/Actions/queueActions';
import { clearReleases, cancelFetchReleases } from 'Store/Actions/releaseActions'; import { clearReleases, cancelFetchReleases } from 'Store/Actions/releaseActions';
import { fetchNetImportSchema } from 'Store/Actions/settingsActions';
import { executeCommand } from 'Store/Actions/commandActions'; import { executeCommand } from 'Store/Actions/commandActions';
import * as commandNames from 'Commands/commandNames'; import * as commandNames from 'Commands/commandNames';
import MovieDetails from './MovieDetails'; import MovieDetails from './MovieDetails';
@ -41,19 +42,19 @@ const selectMovieFiles = createSelector(
} }
); );
const selectMoviePeople = createSelector( const selectMovieCredits = createSelector(
(state) => state.moviePeople, (state) => state.movieCredits,
(moviePeople) => { (movieCredits) => {
const { const {
isFetching, isFetching,
isPopulated, isPopulated,
error error
} = moviePeople; } = movieCredits;
return { return {
isMoviePeopleFetching: isFetching, isMovieCreditsFetching: isFetching,
isMoviePeoplePopulated: isPopulated, isMovieCreditsPopulated: isPopulated,
moviePeopleError: error movieCreditsError: error
}; };
} }
); );
@ -62,11 +63,11 @@ function createMapStateToProps() {
return createSelector( return createSelector(
(state, { titleSlug }) => titleSlug, (state, { titleSlug }) => titleSlug,
selectMovieFiles, selectMovieFiles,
selectMoviePeople, selectMovieCredits,
createAllMoviesSelector(), createAllMoviesSelector(),
createCommandsSelector(), createCommandsSelector(),
createDimensionsSelector(), createDimensionsSelector(),
(titleSlug, movieFiles, moviePeople, allMovies, commands, dimensions) => { (titleSlug, movieFiles, movieCredits, allMovies, commands, dimensions) => {
const sortedMovies = _.orderBy(allMovies, 'sortTitle'); const sortedMovies = _.orderBy(allMovies, 'sortTitle');
const movieIndex = _.findIndex(sortedMovies, { titleSlug }); const movieIndex = _.findIndex(sortedMovies, { titleSlug });
const movie = sortedMovies[movieIndex]; const movie = sortedMovies[movieIndex];
@ -84,10 +85,10 @@ function createMapStateToProps() {
} = movieFiles; } = movieFiles;
const { const {
isMoviePeopleFetching, isMovieCreditsFetching,
isMoviePeoplePopulated, isMovieCreditsPopulated,
moviePeopleError movieCreditsError
} = moviePeople; } = movieCredits;
const previousMovie = sortedMovies[movieIndex - 1] || _.last(sortedMovies); const previousMovie = sortedMovies[movieIndex - 1] || _.last(sortedMovies);
const nextMovie = sortedMovies[movieIndex + 1] || _.first(sortedMovies); const nextMovie = sortedMovies[movieIndex + 1] || _.first(sortedMovies);
@ -106,8 +107,8 @@ function createMapStateToProps() {
isRenamingMovieCommand.body.movieIds.indexOf(movie.id) > -1 isRenamingMovieCommand.body.movieIds.indexOf(movie.id) > -1
); );
const isFetching = isMovieFilesFetching && isMoviePeopleFetching; const isFetching = isMovieFilesFetching && isMovieCreditsFetching;
const isPopulated = isMovieFilesPopulated && isMoviePeoplePopulated; const isPopulated = isMovieFilesPopulated && isMovieCreditsPopulated;
const alternateTitles = _.reduce(movie.alternateTitles, (acc, alternateTitle) => { const alternateTitles = _.reduce(movie.alternateTitles, (acc, alternateTitle) => {
acc.push(alternateTitle.title); acc.push(alternateTitle.title);
return acc; return acc;
@ -125,7 +126,7 @@ function createMapStateToProps() {
isFetching, isFetching,
isPopulated, isPopulated,
movieFilesError, movieFilesError,
moviePeopleError, movieCreditsError,
hasMovieFiles, hasMovieFiles,
sizeOnDisk, sizeOnDisk,
previousMovie, previousMovie,
@ -139,10 +140,11 @@ function createMapStateToProps() {
const mapDispatchToProps = { const mapDispatchToProps = {
fetchMovieFiles, fetchMovieFiles,
clearMovieFiles, clearMovieFiles,
fetchMoviePeople, fetchMovieCredits,
clearMoviePeople, clearMovieCredits,
clearReleases, clearReleases,
cancelFetchReleases, cancelFetchReleases,
fetchNetImportSchema,
toggleMovieMonitored, toggleMovieMonitored,
fetchQueueDetails, fetchQueueDetails,
clearQueueDetails, clearQueueDetails,
@ -198,14 +200,15 @@ class MovieDetailsConnector extends Component {
const movieId = this.props.id; const movieId = this.props.id;
this.props.fetchMovieFiles({ movieId }); this.props.fetchMovieFiles({ movieId });
this.props.fetchMoviePeople({ movieId }); this.props.fetchMovieCredits({ movieId });
this.props.fetchQueueDetails({ movieId }); this.props.fetchQueueDetails({ movieId });
this.props.fetchNetImportSchema();
} }
unpopulate = () => { unpopulate = () => {
this.props.cancelFetchReleases(); this.props.cancelFetchReleases();
this.props.clearMovieFiles(); this.props.clearMovieFiles();
this.props.clearMoviePeople(); this.props.clearMovieCredits();
this.props.clearQueueDetails(); this.props.clearQueueDetails();
this.props.clearReleases(); this.props.clearReleases();
} }
@ -260,13 +263,14 @@ MovieDetailsConnector.propTypes = {
isSmallScreen: PropTypes.bool.isRequired, isSmallScreen: PropTypes.bool.isRequired,
fetchMovieFiles: PropTypes.func.isRequired, fetchMovieFiles: PropTypes.func.isRequired,
clearMovieFiles: PropTypes.func.isRequired, clearMovieFiles: PropTypes.func.isRequired,
fetchMoviePeople: PropTypes.func.isRequired, fetchMovieCredits: PropTypes.func.isRequired,
clearMoviePeople: PropTypes.func.isRequired, clearMovieCredits: PropTypes.func.isRequired,
clearReleases: PropTypes.func.isRequired, clearReleases: PropTypes.func.isRequired,
cancelFetchReleases: PropTypes.func.isRequired, cancelFetchReleases: PropTypes.func.isRequired,
toggleMovieMonitored: PropTypes.func.isRequired, toggleMovieMonitored: PropTypes.func.isRequired,
fetchQueueDetails: PropTypes.func.isRequired, fetchQueueDetails: PropTypes.func.isRequired,
clearQueueDetails: PropTypes.func.isRequired, clearQueueDetails: PropTypes.func.isRequired,
fetchNetImportSchema: PropTypes.func.isRequired,
executeCommand: PropTypes.func.isRequired executeCommand: PropTypes.func.isRequired
}; };

View File

@ -0,0 +1,37 @@
import _ from 'lodash';
import { createSelector } from 'reselect';
function createMovieCreditListSelector() {
return createSelector(
(state, { tmdbId }) => tmdbId,
(state) => state.settings.netImports.items,
(tmdbId, netImports) => {
const netImportIds = _.reduce(netImports, (acc, list) => {
if (list.implementation === 'TMDbPersonImport') {
const personIdField = list.fields.find((field) => {
return field.name === 'personId';
});
if (personIdField && parseInt(personIdField.value) === tmdbId) {
acc.push(list);
return acc;
}
}
return acc;
}, []);
let netImportId = 0;
if (netImportIds.length > 0) {
netImportId = netImportIds[0].id;
}
return {
netImportId
};
}
);
}
export default createMovieCreditListSelector;