mirror of
https://github.com/Radarr/Radarr.git
synced 2026-04-18 21:35:51 -04:00
Fixed: Removing import lists for cast and crew from movie details
Convert movie credits to TypeScript Switching to metadata based order for crew
This commit is contained in:
@@ -1,9 +1,10 @@
|
|||||||
import InteractiveImportAppState from 'App/State/InteractiveImportAppState';
|
|
||||||
import BlocklistAppState from './BlocklistAppState';
|
import BlocklistAppState from './BlocklistAppState';
|
||||||
import CalendarAppState from './CalendarAppState';
|
import CalendarAppState from './CalendarAppState';
|
||||||
import CommandAppState from './CommandAppState';
|
import CommandAppState from './CommandAppState';
|
||||||
import HistoryAppState from './HistoryAppState';
|
import HistoryAppState from './HistoryAppState';
|
||||||
|
import InteractiveImportAppState from './InteractiveImportAppState';
|
||||||
import MovieCollectionAppState from './MovieCollectionAppState';
|
import MovieCollectionAppState from './MovieCollectionAppState';
|
||||||
|
import MovieCreditAppState from './MovieCreditAppState';
|
||||||
import MovieFilesAppState from './MovieFilesAppState';
|
import MovieFilesAppState from './MovieFilesAppState';
|
||||||
import MoviesAppState, { MovieIndexAppState } from './MoviesAppState';
|
import MoviesAppState, { MovieIndexAppState } from './MoviesAppState';
|
||||||
import ParseAppState from './ParseAppState';
|
import ParseAppState from './ParseAppState';
|
||||||
@@ -64,6 +65,7 @@ interface AppState {
|
|||||||
history: HistoryAppState;
|
history: HistoryAppState;
|
||||||
interactiveImport: InteractiveImportAppState;
|
interactiveImport: InteractiveImportAppState;
|
||||||
movieCollections: MovieCollectionAppState;
|
movieCollections: MovieCollectionAppState;
|
||||||
|
movieCredits: MovieCreditAppState;
|
||||||
movieFiles: MovieFilesAppState;
|
movieFiles: MovieFilesAppState;
|
||||||
movieIndex: MovieIndexAppState;
|
movieIndex: MovieIndexAppState;
|
||||||
movies: MoviesAppState;
|
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 formatBytes from 'Utilities/Number/formatBytes';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import MovieCollectionLabelConnector from './../MovieCollectionLabelConnector';
|
import MovieCollectionLabelConnector from './../MovieCollectionLabelConnector';
|
||||||
import MovieCastPostersConnector from './Credits/Cast/MovieCastPostersConnector';
|
import MovieCastPosters from './Credits/Cast/MovieCastPosters';
|
||||||
import MovieCrewPostersConnector from './Credits/Crew/MovieCrewPostersConnector';
|
import MovieCrewPosters from './Credits/Crew/MovieCrewPosters';
|
||||||
import MovieDetailsLinks from './MovieDetailsLinks';
|
import MovieDetailsLinks from './MovieDetailsLinks';
|
||||||
import MovieReleaseDates from './MovieReleaseDates';
|
import MovieReleaseDates from './MovieReleaseDates';
|
||||||
import MovieStatusLabel from './MovieStatusLabel';
|
import MovieStatusLabel from './MovieStatusLabel';
|
||||||
@@ -685,13 +685,13 @@ class MovieDetails extends Component {
|
|||||||
</FieldSet>
|
</FieldSet>
|
||||||
|
|
||||||
<FieldSet legend={translate('Cast')}>
|
<FieldSet legend={translate('Cast')}>
|
||||||
<MovieCastPostersConnector
|
<MovieCastPosters
|
||||||
isSmallScreen={isSmallScreen}
|
isSmallScreen={isSmallScreen}
|
||||||
/>
|
/>
|
||||||
</FieldSet>
|
</FieldSet>
|
||||||
|
|
||||||
<FieldSet legend={translate('Crew')}>
|
<FieldSet legend={translate('Crew')}>
|
||||||
<MovieCrewPostersConnector
|
<MovieCrewPosters
|
||||||
isSmallScreen={isSmallScreen}
|
isSmallScreen={isSmallScreen}
|
||||||
/>
|
/>
|
||||||
</FieldSet>
|
</FieldSet>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export type MovieStatus =
|
|||||||
| 'released'
|
| 'released'
|
||||||
| 'deleted';
|
| 'deleted';
|
||||||
|
|
||||||
export type CoverType = 'poster' | 'fanart';
|
export type CoverType = 'poster' | 'fanart' | 'headshot';
|
||||||
|
|
||||||
export interface Image {
|
export interface Image {
|
||||||
coverType: CoverType;
|
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 { createSelector } from 'reselect';
|
||||||
import appState from 'App/State/AppState';
|
import AppState from 'App/State/AppState';
|
||||||
import Movie from 'Movie/Movie';
|
import Movie from 'Movie/Movie';
|
||||||
import { createMovieSelectorForHook } from './createMovieSelector';
|
import { createMovieSelectorForHook } from './createMovieSelector';
|
||||||
|
|
||||||
function createMovieQualityProfileSelector(movieId: number) {
|
function createMovieQualityProfileSelector(movieId: number) {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state: appState) => state.settings.qualityProfiles.items,
|
(state: AppState) => state.settings.qualityProfiles.items,
|
||||||
createMovieSelectorForHook(movieId),
|
createMovieSelectorForHook(movieId),
|
||||||
(qualityProfiles, movie = {} as Movie) => {
|
(qualityProfiles, movie = {} as Movie) => {
|
||||||
return qualityProfiles.find(
|
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 class CrewResource
|
||||||
{
|
{
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
|
public int Order { get; set; }
|
||||||
public string Job { get; set; }
|
public string Job { get; set; }
|
||||||
public string Department { get; set; }
|
public string Department { get; set; }
|
||||||
public int TmdbId { get; set; }
|
public int TmdbId { get; set; }
|
||||||
|
|||||||
@@ -586,6 +586,7 @@ namespace NzbDrone.Core.MetadataSource.SkyHook
|
|||||||
Name = arg.Name,
|
Name = arg.Name,
|
||||||
Department = arg.Department,
|
Department = arg.Department,
|
||||||
Job = arg.Job,
|
Job = arg.Job,
|
||||||
|
Order = arg.Order,
|
||||||
CreditTmdbId = arg.CreditId,
|
CreditTmdbId = arg.CreditId,
|
||||||
PersonTmdbId = arg.TmdbId,
|
PersonTmdbId = arg.TmdbId,
|
||||||
Type = CreditType.Crew,
|
Type = CreditType.Crew,
|
||||||
|
|||||||
Reference in New Issue
Block a user