From 5975be369074c5d2edbe141aa84ece5e91ab1fff Mon Sep 17 00:00:00 2001 From: Bogdan Date: Sun, 15 Sep 2024 01:56:17 +0300 Subject: [PATCH] Fixed: Removing import lists for cast and crew from movie details Convert movie credits to TypeScript Switching to metadata based order for crew --- frontend/src/App/State/AppState.ts | 4 +- frontend/src/App/State/MovieCreditAppState.ts | 6 + .../Details/Credits/Cast/MovieCastPoster.js | 175 ----------------- .../Details/Credits/Cast/MovieCastPoster.tsx | 179 ++++++++++++++++++ .../Details/Credits/Cast/MovieCastPosters.tsx | 25 +++ .../Credits/Cast/MovieCastPostersConnector.js | 43 ----- .../Details/Credits/Crew/MovieCrewPoster.js | 175 ----------------- .../Details/Credits/Crew/MovieCrewPoster.tsx | 177 +++++++++++++++++ .../Details/Credits/Crew/MovieCrewPosters.tsx | 25 +++ .../Credits/Crew/MovieCrewPostersConnector.js | 68 ------- .../Details/Credits/MovieCreditPoster.tsx | 60 ++++++ .../Credits/MovieCreditPosterConnector.js | 66 ------- .../Details/Credits/MovieCreditPosters.js | 109 ----------- .../Details/Credits/MovieCreditPosters.tsx | 87 +++++++++ frontend/src/Movie/Details/MovieDetails.js | 8 +- frontend/src/Movie/Movie.ts | 2 +- frontend/src/Movie/MovieHeadshot.js | 25 --- frontend/src/Movie/MovieHeadshot.tsx | 23 +++ .../createMovieCreditImportListSelector.ts | 37 ++++ .../createMovieCreditListSelector.js | 33 ---- .../Selectors/createMovieCreditsSelector.ts | 23 +++ .../createMovieQualityProfileSelector.ts | 4 +- frontend/src/typings/MovieCredit.ts | 17 ++ .../SkyHook/Resource/CreditsResource.cs | 1 + .../MetadataSource/SkyHook/SkyHookProxy.cs | 1 + 25 files changed, 671 insertions(+), 702 deletions(-) create mode 100644 frontend/src/App/State/MovieCreditAppState.ts delete mode 100644 frontend/src/Movie/Details/Credits/Cast/MovieCastPoster.js create mode 100644 frontend/src/Movie/Details/Credits/Cast/MovieCastPoster.tsx create mode 100644 frontend/src/Movie/Details/Credits/Cast/MovieCastPosters.tsx delete mode 100644 frontend/src/Movie/Details/Credits/Cast/MovieCastPostersConnector.js delete mode 100644 frontend/src/Movie/Details/Credits/Crew/MovieCrewPoster.js create mode 100644 frontend/src/Movie/Details/Credits/Crew/MovieCrewPoster.tsx create mode 100644 frontend/src/Movie/Details/Credits/Crew/MovieCrewPosters.tsx delete mode 100644 frontend/src/Movie/Details/Credits/Crew/MovieCrewPostersConnector.js create mode 100644 frontend/src/Movie/Details/Credits/MovieCreditPoster.tsx delete mode 100644 frontend/src/Movie/Details/Credits/MovieCreditPosterConnector.js delete mode 100644 frontend/src/Movie/Details/Credits/MovieCreditPosters.js create mode 100644 frontend/src/Movie/Details/Credits/MovieCreditPosters.tsx delete mode 100644 frontend/src/Movie/MovieHeadshot.js create mode 100644 frontend/src/Movie/MovieHeadshot.tsx create mode 100644 frontend/src/Store/Selectors/createMovieCreditImportListSelector.ts delete mode 100644 frontend/src/Store/Selectors/createMovieCreditListSelector.js create mode 100644 frontend/src/Store/Selectors/createMovieCreditsSelector.ts create mode 100644 frontend/src/typings/MovieCredit.ts diff --git a/frontend/src/App/State/AppState.ts b/frontend/src/App/State/AppState.ts index 6f4e173fb..6d60772e0 100644 --- a/frontend/src/App/State/AppState.ts +++ b/frontend/src/App/State/AppState.ts @@ -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; diff --git a/frontend/src/App/State/MovieCreditAppState.ts b/frontend/src/App/State/MovieCreditAppState.ts new file mode 100644 index 000000000..424de2ca4 --- /dev/null +++ b/frontend/src/App/State/MovieCreditAppState.ts @@ -0,0 +1,6 @@ +import AppSectionState from 'App/State/AppSectionState'; +import MovieCredit from 'typings/MovieCredit'; + +interface MovieCreditAppState extends AppSectionState {} + +export default MovieCreditAppState; diff --git a/frontend/src/Movie/Details/Credits/Cast/MovieCastPoster.js b/frontend/src/Movie/Details/Credits/Cast/MovieCastPoster.js deleted file mode 100644 index 047c835dd..000000000 --- a/frontend/src/Movie/Details/Credits/Cast/MovieCastPoster.js +++ /dev/null @@ -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 ( -
-
-
- 0 ? this.onEditImportListPress : this.onAddImportListPress} - /> -
- - - -
- - - { - hasPosterError && -
- {personName} -
- } -
-
- -
- {personName} -
-
- {character} -
- - -
- ); - } -} - -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; diff --git a/frontend/src/Movie/Details/Credits/Cast/MovieCastPoster.tsx b/frontend/src/Movie/Details/Credits/Cast/MovieCastPoster.tsx new file mode 100644 index 000000000..cb6d3c1bc --- /dev/null +++ b/frontend/src/Movie/Details/Credits/Cast/MovieCastPoster.tsx @@ -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 { + 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 ( +
+
+
+ +
+ + + +
+ + + {hasPosterError && ( +
{personName}
+ )} +
+
+ +
+ {personName} +
+
+ {character} +
+ + + + +
+ ); +} + +export default MovieCastPoster; diff --git a/frontend/src/Movie/Details/Credits/Cast/MovieCastPosters.tsx b/frontend/src/Movie/Details/Credits/Cast/MovieCastPosters.tsx new file mode 100644 index 000000000..73bbd22d6 --- /dev/null +++ b/frontend/src/Movie/Details/Credits/Cast/MovieCastPosters.tsx @@ -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 ( + + ); +} + +export default MovieCastPosters; diff --git a/frontend/src/Movie/Details/Credits/Cast/MovieCastPostersConnector.js b/frontend/src/Movie/Details/Credits/Cast/MovieCastPostersConnector.js deleted file mode 100644 index 87f3d5ca0..000000000 --- a/frontend/src/Movie/Details/Credits/Cast/MovieCastPostersConnector.js +++ /dev/null @@ -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 ( - - ); - } -} - -export default connect(createMapStateToProps)(MovieCastPostersConnector); diff --git a/frontend/src/Movie/Details/Credits/Crew/MovieCrewPoster.js b/frontend/src/Movie/Details/Credits/Crew/MovieCrewPoster.js deleted file mode 100644 index 09f5f4d54..000000000 --- a/frontend/src/Movie/Details/Credits/Crew/MovieCrewPoster.js +++ /dev/null @@ -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 ( -
-
-
- 0 ? this.onEditImportListPress : this.onAddImportListPress} - /> -
- - - -
- - - { - hasPosterError && -
- {personName} -
- } -
-
- -
- {personName} -
-
- {job} -
- - -
- ); - } -} - -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; diff --git a/frontend/src/Movie/Details/Credits/Crew/MovieCrewPoster.tsx b/frontend/src/Movie/Details/Credits/Crew/MovieCrewPoster.tsx new file mode 100644 index 000000000..e211d0e2d --- /dev/null +++ b/frontend/src/Movie/Details/Credits/Crew/MovieCrewPoster.tsx @@ -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 { + 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 ( +
+
+
+ +
+ + + +
+ + + {hasPosterError && ( +
{personName}
+ )} +
+
+ +
+ {personName} +
+
{job}
+ + + + +
+ ); +} + +export default MovieCrewPoster; diff --git a/frontend/src/Movie/Details/Credits/Crew/MovieCrewPosters.tsx b/frontend/src/Movie/Details/Credits/Crew/MovieCrewPosters.tsx new file mode 100644 index 000000000..286798c8a --- /dev/null +++ b/frontend/src/Movie/Details/Credits/Crew/MovieCrewPosters.tsx @@ -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 ( + + ); +} + +export default MovieCrewPosters; diff --git a/frontend/src/Movie/Details/Credits/Crew/MovieCrewPostersConnector.js b/frontend/src/Movie/Details/Credits/Crew/MovieCrewPostersConnector.js deleted file mode 100644 index cc1b8ea53..000000000 --- a/frontend/src/Movie/Details/Credits/Crew/MovieCrewPostersConnector.js +++ /dev/null @@ -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 ( - - ); - } -} - -export default connect(createMapStateToProps)(MovieCrewPostersConnector); diff --git a/frontend/src/Movie/Details/Credits/MovieCreditPoster.tsx b/frontend/src/Movie/Details/Credits/MovieCreditPoster.tsx new file mode 100644 index 000000000..cf763a352 --- /dev/null +++ b/frontend/src/Movie/Details/Credits/MovieCreditPoster.tsx @@ -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 + | Omit +); + +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 ( + + ); +} + +export default MovieCreditPoster; diff --git a/frontend/src/Movie/Details/Credits/MovieCreditPosterConnector.js b/frontend/src/Movie/Details/Credits/MovieCreditPosterConnector.js deleted file mode 100644 index 7401a42ca..000000000 --- a/frontend/src/Movie/Details/Credits/MovieCreditPosterConnector.js +++ /dev/null @@ -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 ( - - ); - } -} - -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); diff --git a/frontend/src/Movie/Details/Credits/MovieCreditPosters.js b/frontend/src/Movie/Details/Credits/MovieCreditPosters.js deleted file mode 100644 index 84e0f68a2..000000000 --- a/frontend/src/Movie/Details/Credits/MovieCreditPosters.js +++ /dev/null @@ -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 ( - -
- { - swiper.navigation.init(); - swiper.navigation.update(); - }} - > - {items.map((credit) => ( - - - - ))} - -
- ); - } -} - -MovieCreditPosters.propTypes = { - items: PropTypes.arrayOf(PropTypes.object).isRequired, - itemComponent: PropTypes.elementType.isRequired, - isSmallScreen: PropTypes.bool.isRequired -}; - -export default MovieCreditPosters; diff --git a/frontend/src/Movie/Details/Credits/MovieCreditPosters.tsx b/frontend/src/Movie/Details/Credits/MovieCreditPosters.tsx new file mode 100644 index 000000000..1fe07595b --- /dev/null +++ b/frontend/src/Movie/Details/Credits/MovieCreditPosters.tsx @@ -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 ( +
+ + {items.map((credit) => ( + + + + ))} + +
+ ); +} + +export default MovieCreditPosters; diff --git a/frontend/src/Movie/Details/MovieDetails.js b/frontend/src/Movie/Details/MovieDetails.js index d90b190b3..9981dc90c 100644 --- a/frontend/src/Movie/Details/MovieDetails.js +++ b/frontend/src/Movie/Details/MovieDetails.js @@ -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 {
-
-
diff --git a/frontend/src/Movie/Movie.ts b/frontend/src/Movie/Movie.ts index ff98cc10a..1d2ca51fb 100644 --- a/frontend/src/Movie/Movie.ts +++ b/frontend/src/Movie/Movie.ts @@ -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; diff --git a/frontend/src/Movie/MovieHeadshot.js b/frontend/src/Movie/MovieHeadshot.js deleted file mode 100644 index 490d693bf..000000000 --- a/frontend/src/Movie/MovieHeadshot.js +++ /dev/null @@ -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 ( - - ); -} - -MovieHeadshot.propTypes = { - size: PropTypes.number.isRequired -}; - -MovieHeadshot.defaultProps = { - size: 250 -}; - -export default MovieHeadshot; diff --git a/frontend/src/Movie/MovieHeadshot.tsx b/frontend/src/Movie/MovieHeadshot.tsx new file mode 100644 index 000000000..f0670c4f7 --- /dev/null +++ b/frontend/src/Movie/MovieHeadshot.tsx @@ -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 { + size?: 250 | 500; +} + +function MovieHeadshot({ size = 250, ...otherProps }: MovieHeadshotProps) { + return ( + + ); +} + +export default MovieHeadshot; diff --git a/frontend/src/Store/Selectors/createMovieCreditImportListSelector.ts b/frontend/src/Store/Selectors/createMovieCreditImportListSelector.ts new file mode 100644 index 000000000..bbe2fbfc1 --- /dev/null +++ b/frontend/src/Store/Selectors/createMovieCreditImportListSelector.ts @@ -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; diff --git a/frontend/src/Store/Selectors/createMovieCreditListSelector.js b/frontend/src/Store/Selectors/createMovieCreditListSelector.js deleted file mode 100644 index 017485b93..000000000 --- a/frontend/src/Store/Selectors/createMovieCreditListSelector.js +++ /dev/null @@ -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; diff --git a/frontend/src/Store/Selectors/createMovieCreditsSelector.ts b/frontend/src/Store/Selectors/createMovieCreditsSelector.ts new file mode 100644 index 000000000..4d67c66dc --- /dev/null +++ b/frontend/src/Store/Selectors/createMovieCreditsSelector.ts @@ -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; diff --git a/frontend/src/Store/Selectors/createMovieQualityProfileSelector.ts b/frontend/src/Store/Selectors/createMovieQualityProfileSelector.ts index 45c1366e1..1997cdef2 100644 --- a/frontend/src/Store/Selectors/createMovieQualityProfileSelector.ts +++ b/frontend/src/Store/Selectors/createMovieQualityProfileSelector.ts @@ -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( diff --git a/frontend/src/typings/MovieCredit.ts b/frontend/src/typings/MovieCredit.ts new file mode 100644 index 000000000..83abab79e --- /dev/null +++ b/frontend/src/typings/MovieCredit.ts @@ -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; diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/CreditsResource.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/CreditsResource.cs index 8e0dd62f2..e4fe68572 100644 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/CreditsResource.cs +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/Resource/CreditsResource.cs @@ -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; } diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs index 4050622c0..db6fd6188 100644 --- a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs +++ b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs @@ -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,