Fixed: Removing import lists for cast and crew from movie details

Convert movie credits to TypeScript

Switching to metadata based order for crew
pull/10418/head^2
Bogdan 7 months ago
parent 6095819005
commit 5975be3690

@ -1,9 +1,10 @@
import InteractiveImportAppState from 'App/State/InteractiveImportAppState';
import BlocklistAppState from './BlocklistAppState';
import CalendarAppState from './CalendarAppState';
import CommandAppState from './CommandAppState';
import HistoryAppState from './HistoryAppState';
import InteractiveImportAppState from './InteractiveImportAppState';
import MovieCollectionAppState from './MovieCollectionAppState';
import MovieCreditAppState from './MovieCreditAppState';
import MovieFilesAppState from './MovieFilesAppState';
import MoviesAppState, { MovieIndexAppState } from './MoviesAppState';
import ParseAppState from './ParseAppState';
@ -64,6 +65,7 @@ interface AppState {
history: HistoryAppState;
interactiveImport: InteractiveImportAppState;
movieCollections: MovieCollectionAppState;
movieCredits: MovieCreditAppState;
movieFiles: MovieFilesAppState;
movieIndex: MovieIndexAppState;
movies: MoviesAppState;

@ -0,0 +1,6 @@
import AppSectionState from 'App/State/AppSectionState';
import MovieCredit from 'typings/MovieCredit';
interface MovieCreditAppState extends AppSectionState<MovieCredit> {}
export default MovieCreditAppState;

@ -1,175 +0,0 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Icon from 'Components/Icon';
import Label from 'Components/Label';
import Link from 'Components/Link/Link';
import MonitorToggleButton from 'Components/MonitorToggleButton';
import Popover from 'Components/Tooltip/Popover';
import { icons, kinds, sizes } from 'Helpers/Props';
import MovieHeadshot from 'Movie/MovieHeadshot';
import EditImportListModalConnector from 'Settings/ImportLists/ImportLists/EditImportListModalConnector';
import translate from 'Utilities/String/translate';
import styles from '../MovieCreditPoster.css';
class MovieCastPoster extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
hasPosterError: false,
isEditImportListModalOpen: false
};
}
//
// Listeners
onEditImportListPress = () => {
this.setState({ isEditImportListModalOpen: true });
};
onAddImportListPress = () => {
this.props.onImportListSelect();
this.setState({ isEditImportListModalOpen: true });
};
onEditImportListModalClose = () => {
this.setState({ isEditImportListModalOpen: false });
};
onPosterLoad = () => {
if (this.state.hasPosterError) {
this.setState({ hasPosterError: false });
}
};
onPosterLoadError = () => {
if (!this.state.hasPosterError) {
this.setState({ hasPosterError: true });
}
};
//
// Render
render() {
const {
tmdbId,
personName,
character,
images,
posterWidth,
posterHeight,
importList
} = this.props;
const {
hasPosterError
} = this.state;
const elementStyle = {
width: `${posterWidth}px`,
height: `${posterHeight}px`,
borderRadius: '5px'
};
const contentStyle = {
width: `${posterWidth}px`
};
const monitored = importList !== undefined && importList.enabled && importList.enableAuto;
const importListId = importList ? importList.id : 0;
return (
<div
className={styles.content}
style={contentStyle}
>
<div className={styles.posterContainer}>
<div className={styles.toggleMonitoredContainer}>
<MonitorToggleButton
className={styles.monitorToggleButton}
monitored={monitored}
size={20}
onPress={importListId > 0 ? this.onEditImportListPress : this.onAddImportListPress}
/>
</div>
<Label className={styles.controls}>
<span className={styles.externalLinks}>
<Popover
anchor={<Icon name={icons.EXTERNAL_LINK} size={12} />}
title={translate('Links')}
body={
<Link to={`https://www.themoviedb.org/person/${tmdbId}`}>
<Label
className={styles.externalLinkLabel}
kind={kinds.INFO}
size={sizes.LARGE}
>
{translate('TMDb')}
</Label>
</Link>
}
/>
</span>
</Label>
<div
style={elementStyle}
>
<MovieHeadshot
className={styles.poster}
style={elementStyle}
images={images}
size={250}
lazy={false}
overflow={true}
onError={this.onPosterLoadError}
onLoad={this.onPosterLoad}
/>
{
hasPosterError &&
<div className={styles.overlayTitle}>
{personName}
</div>
}
</div>
</div>
<div className={classNames(styles.title, 'swiper-no-swiping')}>
{personName}
</div>
<div className={classNames(styles.title, 'swiper-no-swiping')}>
{character}
</div>
<EditImportListModalConnector
id={importListId}
isOpen={this.state.isEditImportListModalOpen}
onModalClose={this.onEditImportListModalClose}
onDeleteImportListPress={this.onDeleteImportListPress}
/>
</div>
);
}
}
MovieCastPoster.propTypes = {
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,
importList: PropTypes.object,
onImportListSelect: PropTypes.func.isRequired
};
export default MovieCastPoster;

@ -0,0 +1,179 @@
import classNames from 'classnames';
import React, { useCallback, useState } from 'react';
import { useDispatch } from 'react-redux';
import Icon from 'Components/Icon';
import Label from 'Components/Label';
import Link from 'Components/Link/Link';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import MonitorToggleButton from 'Components/MonitorToggleButton';
import Popover from 'Components/Tooltip/Popover';
import useModalOpenState from 'Helpers/Hooks/useModalOpenState';
import { icons, kinds, sizes } from 'Helpers/Props';
import MovieHeadshot from 'Movie/MovieHeadshot';
import EditImportListModalConnector from 'Settings/ImportLists/ImportLists/EditImportListModalConnector';
import { deleteImportList } from 'Store/Actions/Settings/importLists';
import ImportList from 'typings/ImportList';
import MovieCredit from 'typings/MovieCredit';
import translate from 'Utilities/String/translate';
import styles from '../MovieCreditPoster.css';
export interface MovieCastPosterProps
extends Pick<MovieCredit, 'personName' | 'images' | 'character'> {
tmdbId: number;
posterWidth: number;
posterHeight: number;
importList?: ImportList;
onImportListSelect(): void;
}
function MovieCastPoster(props: MovieCastPosterProps) {
const {
tmdbId,
personName,
character,
images = [],
posterWidth,
posterHeight,
importList,
onImportListSelect,
} = props;
const importListId = importList?.id ?? 0;
const dispatch = useDispatch();
const [hasPosterError, setHasPosterError] = useState(false);
const [
isEditImportListModalOpen,
setEditImportListModalOpen,
setEditImportListModalClosed,
] = useModalOpenState(false);
const [
isDeleteImportListModalOpen,
setDeleteImportListModalOpen,
setDeleteImportListModalClosed,
] = useModalOpenState(false);
const handlePosterLoadError = useCallback(() => {
setHasPosterError(true);
}, [setHasPosterError]);
const handlePosterLoad = useCallback(() => {
setHasPosterError(false);
}, [setHasPosterError]);
const handleManageImportListPress = useCallback(() => {
if (importListId === 0) {
onImportListSelect();
}
setEditImportListModalOpen();
}, [importListId, onImportListSelect, setEditImportListModalOpen]);
const handleDeleteImportListConfirmed = useCallback(() => {
dispatch(deleteImportList({ id: importListId }));
setEditImportListModalClosed();
setDeleteImportListModalClosed();
}, [
importListId,
setEditImportListModalClosed,
setDeleteImportListModalClosed,
dispatch,
]);
const elementStyle = {
width: `${posterWidth}px`,
height: `${posterHeight}px`,
borderRadius: '5px',
};
const contentStyle = {
width: `${posterWidth}px`,
};
const monitored =
importList?.enabled === true && importList?.enableAuto === true;
return (
<div className={styles.content} style={contentStyle}>
<div className={styles.posterContainer}>
<div className={styles.toggleMonitoredContainer}>
<MonitorToggleButton
className={styles.monitorToggleButton}
monitored={monitored}
size={20}
onPress={handleManageImportListPress}
/>
</div>
<Label className={styles.controls}>
<span className={styles.externalLinks}>
<Popover
anchor={<Icon name={icons.EXTERNAL_LINK} size={12} />}
title={translate('Links')}
body={
<Link to={`https://www.themoviedb.org/person/${tmdbId}`}>
<Label
className={styles.externalLinkLabel}
kind={kinds.INFO}
size={sizes.LARGE}
>
{translate('TMDb')}
</Label>
</Link>
}
/>
</span>
</Label>
<div style={elementStyle}>
<MovieHeadshot
className={styles.poster}
style={elementStyle}
images={images}
size={250}
lazy={false}
overflow={true}
onError={handlePosterLoadError}
onLoad={handlePosterLoad}
/>
{hasPosterError && (
<div className={styles.overlayTitle}>{personName}</div>
)}
</div>
</div>
<div className={classNames(styles.title, 'swiper-no-swiping')}>
{personName}
</div>
<div className={classNames(styles.title, 'swiper-no-swiping')}>
{character}
</div>
<EditImportListModalConnector
id={importListId}
isOpen={isEditImportListModalOpen}
onModalClose={setEditImportListModalClosed}
onDeleteImportListPress={setDeleteImportListModalOpen}
/>
<ConfirmModal
isOpen={isDeleteImportListModalOpen}
kind={kinds.DANGER}
title={translate('DeleteImportList')}
message={translate('DeleteImportListMessageText', {
name: importList?.name ?? personName,
})}
confirmLabel={translate('Delete')}
onConfirm={handleDeleteImportListConfirmed}
onCancel={setDeleteImportListModalClosed}
/>
</div>
);
}
export default MovieCastPoster;

@ -0,0 +1,25 @@
import React from 'react';
import { useSelector } from 'react-redux';
import createMovieCreditsSelector from 'Store/Selectors/createMovieCreditsSelector';
import MovieCreditPosters from '../MovieCreditPosters';
import MovieCastPoster from './MovieCastPoster';
interface MovieCastPostersProps {
isSmallScreen: boolean;
}
function MovieCastPosters({ isSmallScreen }: MovieCastPostersProps) {
const { items: castCredits } = useSelector(
createMovieCreditsSelector('cast')
);
return (
<MovieCreditPosters
items={castCredits}
itemComponent={MovieCastPoster}
isSmallScreen={isSmallScreen}
/>
);
}
export default MovieCastPosters;

@ -1,43 +0,0 @@
import _ from 'lodash';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import MovieCreditPosters from '../MovieCreditPosters';
import MovieCastPoster from './MovieCastPoster';
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
};
}
);
}
class MovieCastPostersConnector extends Component {
//
// Render
render() {
return (
<MovieCreditPosters
{...this.props}
itemComponent={MovieCastPoster}
/>
);
}
}
export default connect(createMapStateToProps)(MovieCastPostersConnector);

@ -1,175 +0,0 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Icon from 'Components/Icon';
import Label from 'Components/Label';
import Link from 'Components/Link/Link';
import MonitorToggleButton from 'Components/MonitorToggleButton';
import Popover from 'Components/Tooltip/Popover';
import { icons, kinds, sizes } from 'Helpers/Props';
import MovieHeadshot from 'Movie/MovieHeadshot';
import EditImportListModalConnector from 'Settings/ImportLists/ImportLists/EditImportListModalConnector';
import translate from 'Utilities/String/translate';
import styles from '../MovieCreditPoster.css';
class MovieCrewPoster extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
hasPosterError: false,
isEditImportListModalOpen: false
};
}
//
// Listeners
onEditImportListPress = () => {
this.setState({ isEditImportListModalOpen: true });
};
onAddImportListPress = () => {
this.props.onImportListSelect();
this.setState({ isEditImportListModalOpen: true });
};
onEditImportListModalClose = () => {
this.setState({ isEditImportListModalOpen: false });
};
onPosterLoad = () => {
if (this.state.hasPosterError) {
this.setState({ hasPosterError: false });
}
};
onPosterLoadError = () => {
if (!this.state.hasPosterError) {
this.setState({ hasPosterError: true });
}
};
//
// Render
render() {
const {
tmdbId,
personName,
job,
images,
posterWidth,
posterHeight,
importList
} = this.props;
const {
hasPosterError
} = this.state;
const elementStyle = {
width: `${posterWidth}px`,
height: `${posterHeight}px`,
borderRadius: '5px'
};
const contentStyle = {
width: `${posterWidth}px`
};
const monitored = importList !== undefined && importList.enabled && importList.enableAuto;
const importListId = importList ? importList.id : 0;
return (
<div
className={styles.content}
style={contentStyle}
>
<div className={styles.posterContainer}>
<div className={styles.toggleMonitoredContainer}>
<MonitorToggleButton
className={styles.monitorToggleButton}
monitored={monitored}
size={20}
onPress={importListId > 0 ? this.onEditImportListPress : this.onAddImportListPress}
/>
</div>
<Label className={styles.controls}>
<span className={styles.externalLinks}>
<Popover
anchor={<Icon name={icons.EXTERNAL_LINK} size={12} />}
title={translate('Links')}
body={
<Link to={`https://www.themoviedb.org/person/${tmdbId}`}>
<Label
className={styles.externalLinkLabel}
kind={kinds.INFO}
size={sizes.LARGE}
>
{translate('TMDb')}
</Label>
</Link>
}
/>
</span>
</Label>
<div
style={elementStyle}
>
<MovieHeadshot
className={styles.poster}
style={elementStyle}
images={images}
size={250}
lazy={false}
overflow={true}
onError={this.onPosterLoadError}
onLoad={this.onPosterLoad}
/>
{
hasPosterError &&
<div className={styles.overlayTitle}>
{personName}
</div>
}
</div>
</div>
<div className={classNames(styles.title, 'swiper-no-swiping')}>
{personName}
</div>
<div className={classNames(styles.title, 'swiper-no-swiping')}>
{job}
</div>
<EditImportListModalConnector
id={importListId}
isOpen={this.state.isEditImportListModalOpen}
onModalClose={this.onEditImportListModalClose}
onDeleteImportListPress={this.onDeleteImportListPress}
/>
</div>
);
}
}
MovieCrewPoster.propTypes = {
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,
importList: PropTypes.object,
onImportListSelect: PropTypes.func.isRequired
};
export default MovieCrewPoster;

@ -0,0 +1,177 @@
import classNames from 'classnames';
import React, { useCallback, useState } from 'react';
import { useDispatch } from 'react-redux';
import Icon from 'Components/Icon';
import Label from 'Components/Label';
import Link from 'Components/Link/Link';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import MonitorToggleButton from 'Components/MonitorToggleButton';
import Popover from 'Components/Tooltip/Popover';
import useModalOpenState from 'Helpers/Hooks/useModalOpenState';
import { icons, kinds, sizes } from 'Helpers/Props';
import MovieHeadshot from 'Movie/MovieHeadshot';
import EditImportListModalConnector from 'Settings/ImportLists/ImportLists/EditImportListModalConnector';
import { deleteImportList } from 'Store/Actions/Settings/importLists';
import ImportList from 'typings/ImportList';
import MovieCredit from 'typings/MovieCredit';
import translate from 'Utilities/String/translate';
import styles from '../MovieCreditPoster.css';
export interface MovieCrewPosterProps
extends Pick<MovieCredit, 'personName' | 'images' | 'job'> {
tmdbId: number;
posterWidth: number;
posterHeight: number;
importList?: ImportList;
onImportListSelect(): void;
}
function MovieCrewPoster(props: MovieCrewPosterProps) {
const {
tmdbId,
personName,
job,
images = [],
posterWidth,
posterHeight,
importList,
onImportListSelect,
} = props;
const importListId = importList?.id ?? 0;
const dispatch = useDispatch();
const [hasPosterError, setHasPosterError] = useState(false);
const [
isEditImportListModalOpen,
setEditImportListModalOpen,
setEditImportListModalClosed,
] = useModalOpenState(false);
const [
isDeleteImportListModalOpen,
setDeleteImportListModalOpen,
setDeleteImportListModalClosed,
] = useModalOpenState(false);
const handlePosterLoadError = useCallback(() => {
setHasPosterError(true);
}, [setHasPosterError]);
const handlePosterLoad = useCallback(() => {
setHasPosterError(false);
}, [setHasPosterError]);
const handleManageImportListPress = useCallback(() => {
if (importListId === 0) {
onImportListSelect();
}
setEditImportListModalOpen();
}, [importListId, onImportListSelect, setEditImportListModalOpen]);
const handleDeleteImportListConfirmed = useCallback(() => {
dispatch(deleteImportList({ id: importListId }));
setEditImportListModalClosed();
setDeleteImportListModalClosed();
}, [
importListId,
setEditImportListModalClosed,
setDeleteImportListModalClosed,
dispatch,
]);
const elementStyle = {
width: `${posterWidth}px`,
height: `${posterHeight}px`,
borderRadius: '5px',
};
const contentStyle = {
width: `${posterWidth}px`,
};
const monitored =
importList?.enabled === true && importList?.enableAuto === true;
return (
<div className={styles.content} style={contentStyle}>
<div className={styles.posterContainer}>
<div className={styles.toggleMonitoredContainer}>
<MonitorToggleButton
className={styles.monitorToggleButton}
monitored={monitored}
size={20}
onPress={handleManageImportListPress}
/>
</div>
<Label className={styles.controls}>
<span className={styles.externalLinks}>
<Popover
anchor={<Icon name={icons.EXTERNAL_LINK} size={12} />}
title={translate('Links')}
body={
<Link to={`https://www.themoviedb.org/person/${tmdbId}`}>
<Label
className={styles.externalLinkLabel}
kind={kinds.INFO}
size={sizes.LARGE}
>
{translate('TMDb')}
</Label>
</Link>
}
/>
</span>
</Label>
<div style={elementStyle}>
<MovieHeadshot
className={styles.poster}
style={elementStyle}
images={images}
size={250}
lazy={false}
overflow={true}
onError={handlePosterLoadError}
onLoad={handlePosterLoad}
/>
{hasPosterError && (
<div className={styles.overlayTitle}>{personName}</div>
)}
</div>
</div>
<div className={classNames(styles.title, 'swiper-no-swiping')}>
{personName}
</div>
<div className={classNames(styles.title, 'swiper-no-swiping')}>{job}</div>
<EditImportListModalConnector
id={importListId}
isOpen={isEditImportListModalOpen}
onModalClose={setEditImportListModalClosed}
onDeleteImportListPress={setDeleteImportListModalOpen}
/>
<ConfirmModal
isOpen={isDeleteImportListModalOpen}
kind={kinds.DANGER}
title={translate('DeleteImportList')}
message={translate('DeleteImportListMessageText', {
name: importList?.name ?? personName,
})}
confirmLabel={translate('Delete')}
onConfirm={handleDeleteImportListConfirmed}
onCancel={setDeleteImportListModalClosed}
/>
</div>
);
}
export default MovieCrewPoster;

@ -0,0 +1,25 @@
import React from 'react';
import { useSelector } from 'react-redux';
import createMovieCreditsSelector from 'Store/Selectors/createMovieCreditsSelector';
import MovieCreditPosters from '../MovieCreditPosters';
import MovieCrewPoster from './MovieCrewPoster';
interface MovieCrewPostersProps {
isSmallScreen: boolean;
}
function MovieCrewPosters({ isSmallScreen }: MovieCrewPostersProps) {
const { items: crewCredits } = useSelector(
createMovieCreditsSelector('crew')
);
return (
<MovieCreditPosters
items={crewCredits}
itemComponent={MovieCrewPoster}
isSmallScreen={isSmallScreen}
/>
);
}
export default MovieCrewPosters;

@ -1,68 +0,0 @@
import _ from 'lodash';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import MovieCreditPosters from '../MovieCreditPosters';
import MovieCrewPoster from './MovieCrewPoster';
function crewSort(a, b) {
const jobOrder = ['Director', 'Writer', 'Producer', 'Executive Producer', 'Director of Photography'];
const indexA = jobOrder.indexOf(a.job);
const indexB = jobOrder.indexOf(b.job);
if (indexA === -1 && indexB === -1) {
return 0;
} else if (indexA === -1) {
return 1;
} else if (indexB === -1) {
return -1;
}
if (indexA < indexB) {
return -1;
} else if (indexA > indexB) {
return 1;
}
return 0;
}
function createMapStateToProps() {
return createSelector(
(state) => state.movieCredits.items,
(credits) => {
const crew = _.reduce(credits, (acc, credit) => {
if (credit.type === 'crew') {
acc.push(credit);
}
return acc;
}, []);
const sortedCrew = crew.sort(crewSort);
return {
items: _.uniqBy(sortedCrew, 'personName')
};
}
);
}
class MovieCrewPostersConnector extends Component {
//
// Render
render() {
return (
<MovieCreditPosters
{...this.props}
itemComponent={MovieCrewPoster}
/>
);
}
}
export default connect(createMapStateToProps)(MovieCrewPostersConnector);

@ -0,0 +1,60 @@
import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import {
selectImportListSchema,
setImportListFieldValue,
setImportListValue,
} from 'Store/Actions/settingsActions';
import createMovieCreditImportListSelector from 'Store/Selectors/createMovieCreditImportListSelector';
import { MovieCastPosterProps } from './Cast/MovieCastPoster';
import { MovieCrewPosterProps } from './Crew/MovieCrewPoster';
type MovieCreditPosterProps = {
component: React.ElementType;
} & (
| Omit<MovieCrewPosterProps, 'onImportListSelect'>
| Omit<MovieCastPosterProps, 'onImportListSelect'>
);
function MovieCreditPoster({
component: ItemComponent,
tmdbId,
personName,
...otherProps
}: MovieCreditPosterProps) {
const importList = useSelector(createMovieCreditImportListSelector(tmdbId));
const dispatch = useDispatch();
const handleImportListSelect = useCallback(() => {
dispatch(
selectImportListSchema({
implementation: 'TMDbPersonImport',
implementationName: 'TMDb Person',
presetName: undefined,
})
);
dispatch(
// @ts-expect-error 'setImportListFieldValue' isn't typed yet
setImportListFieldValue({ name: 'personId', value: tmdbId.toString() })
);
dispatch(
// @ts-expect-error 'setImportListValue' isn't typed yet
setImportListValue({ name: 'name', value: `${personName} - ${tmdbId}` })
);
}, [dispatch, tmdbId, personName]);
return (
<ItemComponent
{...otherProps}
tmdbId={tmdbId}
personName={personName}
importList={importList}
onImportListSelect={handleImportListSelect}
/>
);
}
export default MovieCreditPoster;

@ -1,66 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { selectImportListSchema, setImportListFieldValue, setImportListValue } from 'Store/Actions/settingsActions';
import createMovieCreditListSelector from 'Store/Selectors/createMovieCreditListSelector';
function createMapStateToProps() {
return createSelector(
createMovieCreditListSelector(),
(importList) => {
return {
importList
};
}
);
}
const mapDispatchToProps = {
selectImportListSchema,
setImportListFieldValue,
setImportListValue
};
class MovieCreditPosterConnector extends Component {
//
// Listeners
onImportListSelect = () => {
this.props.selectImportListSchema({ implementation: 'TMDbPersonImport', implementationName: 'TMDb Person', presetName: undefined });
this.props.setImportListFieldValue({ name: 'personId', value: this.props.tmdbId.toString() });
this.props.setImportListValue({ 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}
onImportListSelect={this.onImportListSelect}
/>
);
}
}
MovieCreditPosterConnector.propTypes = {
tmdbId: PropTypes.number.isRequired,
personName: PropTypes.string.isRequired,
component: PropTypes.elementType.isRequired,
selectImportListSchema: PropTypes.func.isRequired,
setImportListFieldValue: PropTypes.func.isRequired,
setImportListValue: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(MovieCreditPosterConnector);

@ -1,109 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { Navigation } from 'swiper';
import { Swiper, SwiperSlide } from 'swiper/react';
import dimensions from 'Styles/Variables/dimensions';
import MovieCreditPosterConnector from './MovieCreditPosterConnector';
import styles from './MovieCreditPosters.css';
// Import Swiper styles
import 'swiper/css';
import 'swiper/css/navigation';
// Poster container dimensions
const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
const columnPaddingSmallScreen = parseInt(dimensions.movieIndexColumnPaddingSmallScreen);
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);
}
class MovieCreditPosters 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)
};
}
//
// Render
render() {
const {
items,
itemComponent,
isSmallScreen
} = this.props;
const {
posterWidth,
posterHeight,
rowHeight
} = this.state;
return (
<div className={styles.sliderContainer}>
<Swiper
slidesPerView='auto'
spaceBetween={10}
slidesPerGroup={isSmallScreen ? 1 : 3}
navigation={true}
loop={false}
loopFillGroupWithBlank={true}
className="mySwiper"
modules={[Navigation]}
onInit={(swiper) => {
swiper.navigation.init();
swiper.navigation.update();
}}
>
{items.map((credit) => (
<SwiperSlide key={credit.id} style={{ width: posterWidth, height: rowHeight }}>
<MovieCreditPosterConnector
key={credit.id}
component={itemComponent}
posterWidth={posterWidth}
posterHeight={posterHeight}
tmdbId={credit.personTmdbId}
personName={credit.personName}
job={credit.job}
character={credit.character}
images={credit.images}
/>
</SwiperSlide>
))}
</Swiper>
</div>
);
}
}
MovieCreditPosters.propTypes = {
items: PropTypes.arrayOf(PropTypes.object).isRequired,
itemComponent: PropTypes.elementType.isRequired,
isSmallScreen: PropTypes.bool.isRequired
};
export default MovieCreditPosters;

@ -0,0 +1,87 @@
import React, { useCallback, useMemo } from 'react';
import { Navigation } from 'swiper';
import { Swiper, SwiperSlide } from 'swiper/react';
import { Swiper as SwiperClass } from 'swiper/types';
import dimensions from 'Styles/Variables/dimensions';
import MovieCredit from 'typings/MovieCredit';
import MovieCreditPoster from './MovieCreditPoster';
import styles from './MovieCreditPosters.css';
// Import Swiper styles
import 'swiper/css';
import 'swiper/css/navigation';
// Poster container dimensions
const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
const columnPaddingSmallScreen = parseInt(
dimensions.movieIndexColumnPaddingSmallScreen
);
interface MovieCreditPostersProps {
items: MovieCredit[];
itemComponent: React.ElementType;
isSmallScreen: boolean;
}
function MovieCreditPosters(props: MovieCreditPostersProps) {
const { items, itemComponent, isSmallScreen } = props;
const posterWidth = 162;
const posterHeight = 238;
const rowHeight = useMemo(() => {
const titleHeight = 19;
const characterHeight = 19;
const heights = [
posterHeight,
titleHeight,
characterHeight,
isSmallScreen ? columnPaddingSmallScreen : columnPadding,
];
return heights.reduce((acc, height) => acc + height, 0);
}, [posterHeight, isSmallScreen]);
const handleSwiperInit = useCallback((swiper: SwiperClass) => {
swiper.navigation.init();
swiper.navigation.update();
}, []);
return (
<div className={styles.sliderContainer}>
<Swiper
slidesPerView="auto"
spaceBetween={10}
slidesPerGroup={isSmallScreen ? 1 : 3}
navigation={true}
loop={false}
loopFillGroupWithBlank={true}
className="mySwiper"
modules={[Navigation]}
onInit={handleSwiperInit}
>
{items.map((credit) => (
<SwiperSlide
key={credit.id}
style={{ width: posterWidth, height: rowHeight }}
>
<MovieCreditPoster
key={credit.id}
component={itemComponent}
posterWidth={posterWidth}
posterHeight={posterHeight}
tmdbId={credit.personTmdbId}
personName={credit.personName}
images={credit.images}
job={credit.job}
character={credit.character}
/>
</SwiperSlide>
))}
</Swiper>
</div>
);
}
export default MovieCreditPosters;

@ -38,8 +38,8 @@ import formatRuntime from 'Utilities/Date/formatRuntime';
import formatBytes from 'Utilities/Number/formatBytes';
import translate from 'Utilities/String/translate';
import MovieCollectionLabelConnector from './../MovieCollectionLabelConnector';
import MovieCastPostersConnector from './Credits/Cast/MovieCastPostersConnector';
import MovieCrewPostersConnector from './Credits/Crew/MovieCrewPostersConnector';
import MovieCastPosters from './Credits/Cast/MovieCastPosters';
import MovieCrewPosters from './Credits/Crew/MovieCrewPosters';
import MovieDetailsLinks from './MovieDetailsLinks';
import MovieReleaseDates from './MovieReleaseDates';
import MovieStatusLabel from './MovieStatusLabel';
@ -685,13 +685,13 @@ class MovieDetails extends Component {
</FieldSet>
<FieldSet legend={translate('Cast')}>
<MovieCastPostersConnector
<MovieCastPosters
isSmallScreen={isSmallScreen}
/>
</FieldSet>
<FieldSet legend={translate('Crew')}>
<MovieCrewPostersConnector
<MovieCrewPosters
isSmallScreen={isSmallScreen}
/>
</FieldSet>

@ -9,7 +9,7 @@ export type MovieStatus =
| 'released'
| 'deleted';
export type CoverType = 'poster' | 'fanart';
export type CoverType = 'poster' | 'fanart' | 'headshot';
export interface Image {
coverType: CoverType;

@ -1,25 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import MovieImage from './MovieImage';
const posterPlaceholder = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAKgAAAD3CAMAAAC+Te+kAAAAZlBMVEUvLi8vLy8vLzAvMDAwLy8wLzAwMDAwMDEwMTExMDAxMDExMTExMTIxMjIyMjIyMjMyMzMzMjMzMzMzMzQzNDQ0NDQ0NDU0NTU1NTU1NTY1NjY2NTY2NjY2Njc2Nzc3Njc3Nzc3NziHChLWAAAAAWJLR0QAiAUdSAAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB+MKCgEdHeShUbsAAALZSURBVHja7dxNcuwgDEZR1qAVmP1vMrNUJe91GfTzCSpXo575lAymjYWGXRIDKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKNA/AZ3fcTR0/owjofNDnAadnwPoPnS+xTXQeQZ0rkQ/dC4H0Gzo7ITO3bgGOnug/2PcAF3Mczt0fUj0QncG7znQBupw3PkWqh8qpkagpnyqjuArkkxaC02kRqGypCZANVYFdJZCdy9WTRVB5znQ6qTmjFFBWnOhdg20Lqnp0CpqAbRmAJRAK5JaA32zngTNvv910OSkVkJTs1oLtWugeTkNQZ/nkT2rotBHldUwNE6VQTVWGTQ6AHKggqGaBS23JkKf0hUgE1qa01Ro5fzPhoapR0HtCGg4q0poSCqFRgaAFhqxqqEr1EOgmdJaqHdaHQq1I6CunPZAHdY2aIJUBN2V9kE3H1Wd0BXrNVA7BLpgdUCtALo8pZqhdgd0Z6OyE7q1pdoH3dv7tS7o7iZ1E3R/N70Huuz795cQao65vvkqooT+vEgDdPcbj2s3zxTv9Qt/7cuhdgfUo2yAOplyqNuphfqZSqhFmEJo0HkcdPZCo0rRymRxpwSawHR+YtyBZihfvi+nQO0OqCmcYahGqYPGS4qCUJkzBpUpJdCkordyaFZxXi1UUpaZAJ2XQFOLh8ug2XXjVdD0+vYiqLIO3w1VH8EogtoxUPnpGxe04zyTA1p57i4T2nTmbnnnUuLMg1afYE2C1h+1zYEKjlknQLtPg9tb3YzU+dL054qOBb8cvcz3DlqBZhUmhdrnKo9j+pR0rkN5UHkznZHPtJIYN2TTCe1poTUyk9nWPO0bt8Ys7Ug34mlUMONtPUXMaEdXnXN1MnUzN2Z9q3Lr8XQN1DaLQJpXpiamZwltYdIUHShQoECBAgUKFChQoECBAgUKFChQoECBAgUKFChQoECBAgUKFCjQ+vgCff/mEp/vtiIAAAAASUVORK5CYII=';
function MovieHeadshot(props) {
return (
<MovieImage
{...props}
coverType="headshot"
placeholder={posterPlaceholder}
/>
);
}
MovieHeadshot.propTypes = {
size: PropTypes.number.isRequired
};
MovieHeadshot.defaultProps = {
size: 250
};
export default MovieHeadshot;

@ -0,0 +1,23 @@
import React from 'react';
import MovieImage, { MovieImageProps } from './MovieImage';
const posterPlaceholder =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAKgAAAD3CAMAAAC+Te+kAAAAZlBMVEUvLi8vLy8vLzAvMDAwLy8wLzAwMDAwMDEwMTExMDAxMDExMTExMTIxMjIyMjIyMjMyMzMzMjMzMzMzMzQzNDQ0NDQ0NDU0NTU1NTU1NTY1NjY2NTY2NjY2Njc2Nzc3Njc3Nzc3NziHChLWAAAAAWJLR0QAiAUdSAAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB+MKCgEdHeShUbsAAALZSURBVHja7dxNcuwgDEZR1qAVmP1vMrNUJe91GfTzCSpXo575lAymjYWGXRIDKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKNA/AZ3fcTR0/owjofNDnAadnwPoPnS+xTXQeQZ0rkQ/dC4H0Gzo7ITO3bgGOnug/2PcAF3Mczt0fUj0QncG7znQBupw3PkWqh8qpkagpnyqjuArkkxaC02kRqGypCZANVYFdJZCdy9WTRVB5znQ6qTmjFFBWnOhdg20Lqnp0CpqAbRmAJRAK5JaA32zngTNvv910OSkVkJTs1oLtWugeTkNQZ/nkT2rotBHldUwNE6VQTVWGTQ6AHKggqGaBS23JkKf0hUgE1qa01Ro5fzPhoapR0HtCGg4q0poSCqFRgaAFhqxqqEr1EOgmdJaqHdaHQq1I6CunPZAHdY2aIJUBN2V9kE3H1Wd0BXrNVA7BLpgdUCtALo8pZqhdgd0Z6OyE7q1pdoH3dv7tS7o7iZ1E3R/N70Huuz795cQao65vvkqooT+vEgDdPcbj2s3zxTv9Qt/7cuhdgfUo2yAOplyqNuphfqZSqhFmEJo0HkcdPZCo0rRymRxpwSawHR+YtyBZihfvi+nQO0OqCmcYahGqYPGS4qCUJkzBpUpJdCkordyaFZxXi1UUpaZAJ2XQFOLh8ug2XXjVdD0+vYiqLIO3w1VH8EogtoxUPnpGxe04zyTA1p57i4T2nTmbnnnUuLMg1afYE2C1h+1zYEKjlknQLtPg9tb3YzU+dL054qOBb8cvcz3DlqBZhUmhdrnKo9j+pR0rkN5UHkznZHPtJIYN2TTCe1poTUyk9nWPO0bt8Ys7Ug34mlUMONtPUXMaEdXnXN1MnUzN2Z9q3Lr8XQN1DaLQJpXpiamZwltYdIUHShQoECBAgUKFChQoECBAgUKFChQoECBAgUKFChQoECBAgUKFCjQ+vgCff/mEp/vtiIAAAAASUVORK5CYII=';
interface MovieHeadshotProps
extends Omit<MovieImageProps, 'coverType' | 'placeholder'> {
size?: 250 | 500;
}
function MovieHeadshot({ size = 250, ...otherProps }: MovieHeadshotProps) {
return (
<MovieImage
{...otherProps}
size={size}
coverType="headshot"
placeholder={posterPlaceholder}
/>
);
}
export default MovieHeadshot;

@ -0,0 +1,37 @@
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import ImportList from 'typings/ImportList';
function createMovieCreditImportListSelector(tmdbId: number) {
return createSelector(
(state: AppState) => state.settings.importLists.items,
(importLists) => {
const importListIds = importLists.reduce(
(acc: ImportList[], importList) => {
if (importList.implementation === 'TMDbPersonImport') {
const personIdValue = importList.fields.find(
(field) => field.name === 'personId'
)?.value as string | null;
if (personIdValue && parseInt(personIdValue) === tmdbId) {
acc.push(importList);
return acc;
}
}
return acc;
},
[]
);
if (importListIds.length === 0) {
return undefined;
}
return importListIds[0];
}
);
}
export default createMovieCreditImportListSelector;

@ -1,33 +0,0 @@
import _ from 'lodash';
import { createSelector } from 'reselect';
function createMovieCreditListSelector() {
return createSelector(
(state, { tmdbId }) => tmdbId,
(state) => state.settings.importLists.items,
(tmdbId, importLists) => {
const importListIds = _.reduce(importLists, (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;
}, []);
if (importListIds.length === 0) {
return undefined;
}
return importListIds[0];
}
);
}
export default createMovieCreditListSelector;

@ -0,0 +1,23 @@
import _ from 'lodash';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import { MovieCreditType } from 'typings/MovieCredit';
function createMovieCreditsSelector(movieCreditType: MovieCreditType) {
return createSelector(
(state: AppState) => state.movieCredits.items,
(movieCredits) => {
const credits = movieCredits.filter(
({ type }) => type === movieCreditType
);
const sortedCredits = credits.sort((a, b) => a.order - b.order);
return {
items: _.uniqBy(sortedCredits, 'personName'),
};
}
);
}
export default createMovieCreditsSelector;

@ -1,11 +1,11 @@
import { createSelector } from 'reselect';
import appState from 'App/State/AppState';
import AppState from 'App/State/AppState';
import Movie from 'Movie/Movie';
import { createMovieSelectorForHook } from './createMovieSelector';
function createMovieQualityProfileSelector(movieId: number) {
return createSelector(
(state: appState) => state.settings.qualityProfiles.items,
(state: AppState) => state.settings.qualityProfiles.items,
createMovieSelectorForHook(movieId),
(qualityProfiles, movie = {} as Movie) => {
return qualityProfiles.find(

@ -0,0 +1,17 @@
import ModelBase from 'App/ModelBase';
import { Image } from 'Movie/Movie';
export type MovieCreditType = 'cast' | 'crew';
interface MovieCredit extends ModelBase {
personTmdbId: number;
personName: string;
images: Image[];
type: MovieCreditType;
department: string;
job: string;
character: string;
order: number;
}
export default MovieCredit;

@ -21,6 +21,7 @@ namespace NzbDrone.Core.MetadataSource.SkyHook.Resource
public class CrewResource
{
public string Name { get; set; }
public int Order { get; set; }
public string Job { get; set; }
public string Department { get; set; }
public int TmdbId { get; set; }

@ -586,6 +586,7 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
Name = arg.Name,
Department = arg.Department,
Job = arg.Job,
Order = arg.Order,
CreditTmdbId = arg.CreditId,
PersonTmdbId = arg.TmdbId,
Type = CreditType.Crew,

Loading…
Cancel
Save