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:
parent
8021381de2
commit
b3caa87b78
@ -8,7 +8,7 @@ import { saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions';
|
||||
import { fetchCustomFilters } from 'Store/Actions/customFilterActions';
|
||||
import { fetchMovies } from 'Store/Actions/movieActions';
|
||||
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 ErrorPage from './ErrorPage';
|
||||
import LoadingPage from './LoadingPage';
|
||||
@ -48,6 +48,7 @@ const selectIsPopulated = createSelector(
|
||||
(state) => state.settings.ui.isPopulated,
|
||||
(state) => state.settings.qualityProfiles.isPopulated,
|
||||
(state) => state.settings.languages.isPopulated,
|
||||
(state) => state.settings.netImports.isPopulated,
|
||||
(state) => state.system.status.isPopulated,
|
||||
(
|
||||
customFiltersIsPopulated,
|
||||
@ -55,6 +56,7 @@ const selectIsPopulated = createSelector(
|
||||
uiSettingsIsPopulated,
|
||||
qualityProfilesIsPopulated,
|
||||
languagesIsPopulated,
|
||||
netImportsIsPopulated,
|
||||
systemStatusIsPopulated
|
||||
) => {
|
||||
return (
|
||||
@ -63,6 +65,7 @@ const selectIsPopulated = createSelector(
|
||||
uiSettingsIsPopulated &&
|
||||
qualityProfilesIsPopulated &&
|
||||
languagesIsPopulated &&
|
||||
netImportsIsPopulated &&
|
||||
systemStatusIsPopulated
|
||||
);
|
||||
}
|
||||
@ -74,6 +77,7 @@ const selectErrors = createSelector(
|
||||
(state) => state.settings.ui.error,
|
||||
(state) => state.settings.qualityProfiles.error,
|
||||
(state) => state.settings.languages.error,
|
||||
(state) => state.settings.netImports.error,
|
||||
(state) => state.system.status.error,
|
||||
(
|
||||
customFiltersError,
|
||||
@ -81,6 +85,7 @@ const selectErrors = createSelector(
|
||||
uiSettingsError,
|
||||
qualityProfilesError,
|
||||
languagesError,
|
||||
netImportsError,
|
||||
systemStatusError
|
||||
) => {
|
||||
const hasError = !!(
|
||||
@ -89,6 +94,7 @@ const selectErrors = createSelector(
|
||||
uiSettingsError ||
|
||||
qualityProfilesError ||
|
||||
languagesError ||
|
||||
netImportsError ||
|
||||
systemStatusError
|
||||
);
|
||||
|
||||
@ -99,6 +105,7 @@ const selectErrors = createSelector(
|
||||
uiSettingsError,
|
||||
qualityProfilesError,
|
||||
languagesError,
|
||||
netImportsError,
|
||||
systemStatusError
|
||||
};
|
||||
}
|
||||
@ -146,6 +153,9 @@ function createMapDispatchToProps(dispatch, props) {
|
||||
dispatchFetchLanguages() {
|
||||
dispatch(fetchLanguages());
|
||||
},
|
||||
dispatchFetchNetImports() {
|
||||
dispatch(fetchNetImports());
|
||||
},
|
||||
dispatchFetchUISettings() {
|
||||
dispatch(fetchUISettings());
|
||||
},
|
||||
@ -181,6 +191,7 @@ class PageConnector extends Component {
|
||||
this.props.dispatchFetchTags();
|
||||
this.props.dispatchFetchQualityProfiles();
|
||||
this.props.dispatchFetchLanguages();
|
||||
this.props.dispatchFetchNetImports();
|
||||
this.props.dispatchFetchUISettings();
|
||||
this.props.dispatchFetchStatus();
|
||||
}
|
||||
@ -204,6 +215,7 @@ class PageConnector extends Component {
|
||||
dispatchFetchTags,
|
||||
dispatchFetchQualityProfiles,
|
||||
dispatchFetchLanguages,
|
||||
dispatchFetchNetImports,
|
||||
dispatchFetchUISettings,
|
||||
dispatchFetchStatus,
|
||||
...otherProps
|
||||
@ -242,6 +254,7 @@ PageConnector.propTypes = {
|
||||
dispatchFetchTags: PropTypes.func.isRequired,
|
||||
dispatchFetchQualityProfiles: PropTypes.func.isRequired,
|
||||
dispatchFetchLanguages: PropTypes.func.isRequired,
|
||||
dispatchFetchNetImports: PropTypes.func.isRequired,
|
||||
dispatchFetchUISettings: PropTypes.func.isRequired,
|
||||
dispatchFetchStatus: PropTypes.func.isRequired,
|
||||
onSidebarVisibleChange: PropTypes.func.isRequired
|
||||
|
@ -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);
|
@ -4,7 +4,8 @@ import { icons } from 'Helpers/Props';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import Label from 'Components/Label';
|
||||
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 {
|
||||
|
||||
@ -16,19 +17,24 @@ class MovieCastPoster extends Component {
|
||||
|
||||
this.state = {
|
||||
hasPosterError: false,
|
||||
isEditMovieModalOpen: false
|
||||
isEditNetImportModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onEditMoviePress = () => {
|
||||
this.setState({ isEditMovieModalOpen: true });
|
||||
onEditNetImportPress = () => {
|
||||
this.setState({ isEditNetImportModalOpen: true });
|
||||
}
|
||||
|
||||
onEditMovieModalClose = () => {
|
||||
this.setState({ isEditMovieModalOpen: false });
|
||||
onAddNetImportPress = () => {
|
||||
this.props.onNetImportSelect();
|
||||
this.setState({ isEditNetImportModalOpen: true });
|
||||
}
|
||||
|
||||
onEditNetImportModalClose = () => {
|
||||
this.setState({ isEditNetImportModalOpen: false });
|
||||
}
|
||||
|
||||
onPosterLoad = () => {
|
||||
@ -48,11 +54,12 @@ class MovieCastPoster extends Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
castName,
|
||||
personName,
|
||||
character,
|
||||
images,
|
||||
posterWidth,
|
||||
posterHeight
|
||||
posterHeight,
|
||||
netImportId
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
@ -68,12 +75,21 @@ class MovieCastPoster extends Component {
|
||||
<div className={styles.content}>
|
||||
<div className={styles.posterContainer}>
|
||||
<Label className={styles.controls}>
|
||||
{
|
||||
netImportId > 0 ?
|
||||
<IconButton
|
||||
className={styles.action}
|
||||
name={icons.EDIT}
|
||||
title="Edit movie"
|
||||
onPress={this.onEditMoviePress}
|
||||
title="Edit Person"
|
||||
onPress={this.onEditNetImportPress}
|
||||
/> :
|
||||
<IconButton
|
||||
className={styles.action}
|
||||
name={icons.ADD}
|
||||
title="Follow Person"
|
||||
onPress={this.onAddNetImportPress}
|
||||
/>
|
||||
}
|
||||
</Label>
|
||||
|
||||
<div
|
||||
@ -94,30 +110,43 @@ class MovieCastPoster extends Component {
|
||||
{
|
||||
hasPosterError &&
|
||||
<div className={styles.overlayTitle}>
|
||||
{castName}
|
||||
{personName}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.title}>
|
||||
{castName}
|
||||
{personName}
|
||||
</div>
|
||||
<div className={styles.title}>
|
||||
{character}
|
||||
</div>
|
||||
|
||||
<EditNetImportModalConnector
|
||||
id={netImportId}
|
||||
isOpen={this.state.isEditNetImportModalOpen}
|
||||
onModalClose={this.onEditNetImportModalClose}
|
||||
onDeleteNetImportPress={this.onDeleteNetImportPress}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MovieCastPoster.propTypes = {
|
||||
castId: PropTypes.number.isRequired,
|
||||
castName: PropTypes.string.isRequired,
|
||||
tmdbId: PropTypes.number.isRequired,
|
||||
personName: PropTypes.string.isRequired,
|
||||
character: PropTypes.string.isRequired,
|
||||
images: PropTypes.arrayOf(PropTypes.object).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;
|
@ -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);
|
@ -4,7 +4,8 @@ import { icons } from 'Helpers/Props';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import Label from 'Components/Label';
|
||||
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 {
|
||||
|
||||
@ -16,19 +17,24 @@ class MovieCrewPoster extends Component {
|
||||
|
||||
this.state = {
|
||||
hasPosterError: false,
|
||||
isEditMovieModalOpen: false
|
||||
isEditNetImportModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onEditMoviePress = () => {
|
||||
this.setState({ isEditMovieModalOpen: true });
|
||||
onEditNetImportPress = () => {
|
||||
this.setState({ isEditNetImportModalOpen: true });
|
||||
}
|
||||
|
||||
onEditMovieModalClose = () => {
|
||||
this.setState({ isEditMovieModalOpen: false });
|
||||
onAddNetImportPress = () => {
|
||||
this.props.onNetImportSelect();
|
||||
this.setState({ isEditNetImportModalOpen: true });
|
||||
}
|
||||
|
||||
onEditNetImportModalClose = () => {
|
||||
this.setState({ isEditNetImportModalOpen: false });
|
||||
}
|
||||
|
||||
onPosterLoad = () => {
|
||||
@ -48,11 +54,12 @@ class MovieCrewPoster extends Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
crewName,
|
||||
personName,
|
||||
job,
|
||||
images,
|
||||
posterWidth,
|
||||
posterHeight
|
||||
posterHeight,
|
||||
netImportId
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
@ -68,12 +75,21 @@ class MovieCrewPoster extends Component {
|
||||
<div className={styles.content}>
|
||||
<div className={styles.posterContainer}>
|
||||
<Label className={styles.controls}>
|
||||
{
|
||||
netImportId > 0 ?
|
||||
<IconButton
|
||||
className={styles.action}
|
||||
name={icons.EDIT}
|
||||
title="Edit movie"
|
||||
onPress={this.onEditMoviePress}
|
||||
title="Edit Person"
|
||||
onPress={this.onEditNetImportPress}
|
||||
/> :
|
||||
<IconButton
|
||||
className={styles.action}
|
||||
name={icons.ADD}
|
||||
title="Follow Person"
|
||||
onPress={this.onAddNetImportPress}
|
||||
/>
|
||||
}
|
||||
</Label>
|
||||
|
||||
<div
|
||||
@ -94,30 +110,43 @@ class MovieCrewPoster extends Component {
|
||||
{
|
||||
hasPosterError &&
|
||||
<div className={styles.overlayTitle}>
|
||||
{crewName}
|
||||
{personName}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.title}>
|
||||
{crewName}
|
||||
{personName}
|
||||
</div>
|
||||
<div className={styles.title}>
|
||||
{job}
|
||||
</div>
|
||||
|
||||
<EditNetImportModalConnector
|
||||
id={netImportId}
|
||||
isOpen={this.state.isEditNetImportModalOpen}
|
||||
onModalClose={this.onEditNetImportModalClose}
|
||||
onDeleteNetImportPress={this.onDeleteNetImportPress}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MovieCrewPoster.propTypes = {
|
||||
crewId: PropTypes.number.isRequired,
|
||||
crewName: PropTypes.string.isRequired,
|
||||
tmdbId: PropTypes.number.isRequired,
|
||||
personName: PropTypes.string.isRequired,
|
||||
job: PropTypes.string.isRequired,
|
||||
images: PropTypes.arrayOf(PropTypes.object).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;
|
@ -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);
|
@ -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);
|
@ -4,8 +4,8 @@ import { Grid, WindowScroller } from 'react-virtualized';
|
||||
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
|
||||
import dimensions from 'Styles/Variables/dimensions';
|
||||
import Measure from 'Components/Measure';
|
||||
import MovieCastPoster from './MovieCastPoster';
|
||||
import styles from './MovieCastPosters.css';
|
||||
import MovieCreditPosterConnector from './MovieCreditPosterConnector';
|
||||
import styles from './MovieCreditPosters.css';
|
||||
|
||||
// Poster container dimensions
|
||||
const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
|
||||
@ -47,7 +47,7 @@ function calculatePosterHeight(posterWidth) {
|
||||
return Math.ceil((250 / 170) * posterWidth);
|
||||
}
|
||||
|
||||
class MovieCastPosters extends Component {
|
||||
class MovieCreditPosters extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
@ -70,7 +70,7 @@ class MovieCastPosters extends Component {
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const {
|
||||
cast
|
||||
items
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
@ -85,7 +85,7 @@ class MovieCastPosters extends Component {
|
||||
prevState.columnWidth !== columnWidth ||
|
||||
prevState.columnCount !== columnCount ||
|
||||
prevState.rowHeight !== rowHeight ||
|
||||
hasDifferentItemsOrOrder(prevProps.cast, cast))) {
|
||||
hasDifferentItemsOrOrder(prevProps.items, items))) {
|
||||
// recomputeGridSize also forces Grid to discard its cache of rendered cells
|
||||
this._grid.recomputeGridSize();
|
||||
}
|
||||
@ -119,7 +119,8 @@ class MovieCastPosters extends Component {
|
||||
|
||||
cellRenderer = ({ key, rowIndex, columnIndex, style }) => {
|
||||
const {
|
||||
cast
|
||||
items,
|
||||
itemComponent
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
@ -129,7 +130,7 @@ class MovieCastPosters extends Component {
|
||||
} = this.state;
|
||||
|
||||
const movieIdx = rowIndex * columnCount + columnIndex;
|
||||
const movie = cast[movieIdx];
|
||||
const movie = items[movieIdx];
|
||||
|
||||
if (!movie) {
|
||||
return null;
|
||||
@ -141,12 +142,14 @@ class MovieCastPosters extends Component {
|
||||
key={key}
|
||||
style={style}
|
||||
>
|
||||
<MovieCastPoster
|
||||
<MovieCreditPosterConnector
|
||||
key={movie.order}
|
||||
component={itemComponent}
|
||||
posterWidth={posterWidth}
|
||||
posterHeight={posterHeight}
|
||||
castId={movie.tmdbId}
|
||||
castName={movie.name}
|
||||
tmdbId={movie.personTmdbId}
|
||||
personName={movie.personName}
|
||||
job={movie.job}
|
||||
character={movie.character}
|
||||
images={movie.images}
|
||||
/>
|
||||
@ -166,7 +169,7 @@ class MovieCastPosters extends Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
cast
|
||||
items
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
@ -176,7 +179,7 @@ class MovieCastPosters extends Component {
|
||||
rowHeight
|
||||
} = this.state;
|
||||
|
||||
const rowCount = Math.ceil(cast.length / columnCount);
|
||||
const rowCount = Math.ceil(items.length / columnCount);
|
||||
|
||||
return (
|
||||
<Measure
|
||||
@ -220,9 +223,10 @@ class MovieCastPosters extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
MovieCastPosters.propTypes = {
|
||||
cast: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
MovieCreditPosters.propTypes = {
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
itemComponent: PropTypes.elementType.isRequired,
|
||||
isSmallScreen: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
export default MovieCastPosters;
|
||||
export default MovieCreditPosters;
|
@ -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;
|
||||
}
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
.grid {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 10px;
|
||||
}
|
@ -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;
|
@ -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);
|
@ -31,8 +31,8 @@ import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
|
||||
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
|
||||
import MovieHistoryTable from 'Movie/History/MovieHistoryTable';
|
||||
import MovieTitlesTable from './Titles/MovieTitlesTable';
|
||||
import MovieCastPostersConnector from './Cast/MovieCastPostersConnector';
|
||||
import MovieCrewPostersConnector from './Crew/MovieCrewPostersConnector';
|
||||
import MovieCastPostersConnector from './Credits/Cast/MovieCastPostersConnector';
|
||||
import MovieCrewPostersConnector from './Credits/Crew/MovieCrewPostersConnector';
|
||||
import MovieAlternateTitles from './MovieAlternateTitles';
|
||||
import MovieDetailsLinks from './MovieDetailsLinks';
|
||||
import InteractiveSearchTable from 'InteractiveSearch/InteractiveSearchTable';
|
||||
@ -181,7 +181,7 @@ class MovieDetails extends Component {
|
||||
isPopulated,
|
||||
isSmallScreen,
|
||||
movieFilesError,
|
||||
moviePeopleError,
|
||||
movieCreditsError,
|
||||
hasMovieFiles,
|
||||
previousMovie,
|
||||
nextMovie,
|
||||
@ -464,12 +464,12 @@ class MovieDetails extends Component {
|
||||
|
||||
<div className={styles.contentContainer}>
|
||||
{
|
||||
!isPopulated && !movieFilesError && !moviePeopleError &&
|
||||
!isPopulated && !movieFilesError && !movieCreditsError &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && movieFilesError && !moviePeopleError &&
|
||||
!isFetching && movieFilesError && !movieCreditsError &&
|
||||
<div>Loading movie files failed</div>
|
||||
}
|
||||
|
||||
@ -629,7 +629,7 @@ MovieDetails.propTypes = {
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
movieFilesError: PropTypes.object,
|
||||
moviePeopleError: PropTypes.object,
|
||||
movieCreditsError: PropTypes.object,
|
||||
hasMovieFiles: PropTypes.bool.isRequired,
|
||||
previousMovie: PropTypes.object.isRequired,
|
||||
nextMovie: PropTypes.object.isRequired,
|
||||
|
@ -9,10 +9,11 @@ import createAllMoviesSelector from 'Store/Selectors/createAllMoviesSelector';
|
||||
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||
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 { fetchQueueDetails, clearQueueDetails } from 'Store/Actions/queueActions';
|
||||
import { clearReleases, cancelFetchReleases } from 'Store/Actions/releaseActions';
|
||||
import { fetchNetImportSchema } from 'Store/Actions/settingsActions';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import MovieDetails from './MovieDetails';
|
||||
@ -41,19 +42,19 @@ const selectMovieFiles = createSelector(
|
||||
}
|
||||
);
|
||||
|
||||
const selectMoviePeople = createSelector(
|
||||
(state) => state.moviePeople,
|
||||
(moviePeople) => {
|
||||
const selectMovieCredits = createSelector(
|
||||
(state) => state.movieCredits,
|
||||
(movieCredits) => {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error
|
||||
} = moviePeople;
|
||||
} = movieCredits;
|
||||
|
||||
return {
|
||||
isMoviePeopleFetching: isFetching,
|
||||
isMoviePeoplePopulated: isPopulated,
|
||||
moviePeopleError: error
|
||||
isMovieCreditsFetching: isFetching,
|
||||
isMovieCreditsPopulated: isPopulated,
|
||||
movieCreditsError: error
|
||||
};
|
||||
}
|
||||
);
|
||||
@ -62,11 +63,11 @@ function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state, { titleSlug }) => titleSlug,
|
||||
selectMovieFiles,
|
||||
selectMoviePeople,
|
||||
selectMovieCredits,
|
||||
createAllMoviesSelector(),
|
||||
createCommandsSelector(),
|
||||
createDimensionsSelector(),
|
||||
(titleSlug, movieFiles, moviePeople, allMovies, commands, dimensions) => {
|
||||
(titleSlug, movieFiles, movieCredits, allMovies, commands, dimensions) => {
|
||||
const sortedMovies = _.orderBy(allMovies, 'sortTitle');
|
||||
const movieIndex = _.findIndex(sortedMovies, { titleSlug });
|
||||
const movie = sortedMovies[movieIndex];
|
||||
@ -84,10 +85,10 @@ function createMapStateToProps() {
|
||||
} = movieFiles;
|
||||
|
||||
const {
|
||||
isMoviePeopleFetching,
|
||||
isMoviePeoplePopulated,
|
||||
moviePeopleError
|
||||
} = moviePeople;
|
||||
isMovieCreditsFetching,
|
||||
isMovieCreditsPopulated,
|
||||
movieCreditsError
|
||||
} = movieCredits;
|
||||
|
||||
const previousMovie = sortedMovies[movieIndex - 1] || _.last(sortedMovies);
|
||||
const nextMovie = sortedMovies[movieIndex + 1] || _.first(sortedMovies);
|
||||
@ -106,8 +107,8 @@ function createMapStateToProps() {
|
||||
isRenamingMovieCommand.body.movieIds.indexOf(movie.id) > -1
|
||||
);
|
||||
|
||||
const isFetching = isMovieFilesFetching && isMoviePeopleFetching;
|
||||
const isPopulated = isMovieFilesPopulated && isMoviePeoplePopulated;
|
||||
const isFetching = isMovieFilesFetching && isMovieCreditsFetching;
|
||||
const isPopulated = isMovieFilesPopulated && isMovieCreditsPopulated;
|
||||
const alternateTitles = _.reduce(movie.alternateTitles, (acc, alternateTitle) => {
|
||||
acc.push(alternateTitle.title);
|
||||
return acc;
|
||||
@ -125,7 +126,7 @@ function createMapStateToProps() {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
movieFilesError,
|
||||
moviePeopleError,
|
||||
movieCreditsError,
|
||||
hasMovieFiles,
|
||||
sizeOnDisk,
|
||||
previousMovie,
|
||||
@ -139,10 +140,11 @@ function createMapStateToProps() {
|
||||
const mapDispatchToProps = {
|
||||
fetchMovieFiles,
|
||||
clearMovieFiles,
|
||||
fetchMoviePeople,
|
||||
clearMoviePeople,
|
||||
fetchMovieCredits,
|
||||
clearMovieCredits,
|
||||
clearReleases,
|
||||
cancelFetchReleases,
|
||||
fetchNetImportSchema,
|
||||
toggleMovieMonitored,
|
||||
fetchQueueDetails,
|
||||
clearQueueDetails,
|
||||
@ -198,14 +200,15 @@ class MovieDetailsConnector extends Component {
|
||||
const movieId = this.props.id;
|
||||
|
||||
this.props.fetchMovieFiles({ movieId });
|
||||
this.props.fetchMoviePeople({ movieId });
|
||||
this.props.fetchMovieCredits({ movieId });
|
||||
this.props.fetchQueueDetails({ movieId });
|
||||
this.props.fetchNetImportSchema();
|
||||
}
|
||||
|
||||
unpopulate = () => {
|
||||
this.props.cancelFetchReleases();
|
||||
this.props.clearMovieFiles();
|
||||
this.props.clearMoviePeople();
|
||||
this.props.clearMovieCredits();
|
||||
this.props.clearQueueDetails();
|
||||
this.props.clearReleases();
|
||||
}
|
||||
@ -260,13 +263,14 @@ MovieDetailsConnector.propTypes = {
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
fetchMovieFiles: PropTypes.func.isRequired,
|
||||
clearMovieFiles: PropTypes.func.isRequired,
|
||||
fetchMoviePeople: PropTypes.func.isRequired,
|
||||
clearMoviePeople: PropTypes.func.isRequired,
|
||||
fetchMovieCredits: PropTypes.func.isRequired,
|
||||
clearMovieCredits: PropTypes.func.isRequired,
|
||||
clearReleases: PropTypes.func.isRequired,
|
||||
cancelFetchReleases: PropTypes.func.isRequired,
|
||||
toggleMovieMonitored: PropTypes.func.isRequired,
|
||||
fetchQueueDetails: PropTypes.func.isRequired,
|
||||
clearQueueDetails: PropTypes.func.isRequired,
|
||||
fetchNetImportSchema: PropTypes.func.isRequired,
|
||||
executeCommand: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
@ -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;
|
Loading…
Reference in New Issue
Block a user