New: Cast/Crew Tabs on Movie Details Page

pull/4072/head
Qstick 5 years ago
parent f2fffe5304
commit bdc1adb2ed

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

@ -0,0 +1,123 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { icons } from 'Helpers/Props';
import IconButton from 'Components/Link/IconButton';
import Label from 'Components/Label';
import MovieHeadshot from 'Movie/MovieHeadshot';
import styles from './MovieCastPoster.css';
class MovieCastPoster extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
hasPosterError: false,
isEditMovieModalOpen: false
};
}
//
// Listeners
onEditMoviePress = () => {
this.setState({ isEditMovieModalOpen: true });
}
onEditMovieModalClose = () => {
this.setState({ isEditMovieModalOpen: false });
}
onPosterLoad = () => {
if (this.state.hasPosterError) {
this.setState({ hasPosterError: false });
}
}
onPosterLoadError = () => {
if (!this.state.hasPosterError) {
this.setState({ hasPosterError: true });
}
}
//
// Render
render() {
const {
castName,
character,
images,
posterWidth,
posterHeight
} = this.props;
const {
hasPosterError
} = this.state;
const elementStyle = {
width: `${posterWidth}px`,
height: `${posterHeight}px`
};
return (
<div className={styles.content}>
<div className={styles.posterContainer}>
<Label className={styles.controls}>
<IconButton
className={styles.action}
name={icons.EDIT}
title="Edit movie"
onPress={this.onEditMoviePress}
/>
</Label>
<div
className={styles.poster}
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}>
{castName}
</div>
}
</div>
</div>
<div className={styles.title}>
{castName}
</div>
<div className={styles.title}>
{character}
</div>
</div>
);
}
}
MovieCastPoster.propTypes = {
castId: PropTypes.number.isRequired,
castName: PropTypes.string.isRequired,
character: PropTypes.string.isRequired,
images: PropTypes.arrayOf(PropTypes.object).isRequired,
posterWidth: PropTypes.number.isRequired,
posterHeight: PropTypes.number.isRequired
};
export default MovieCastPoster;

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

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

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

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

@ -0,0 +1,123 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { icons } from 'Helpers/Props';
import IconButton from 'Components/Link/IconButton';
import Label from 'Components/Label';
import MovieHeadshot from 'Movie/MovieHeadshot';
import styles from './MovieCrewPoster.css';
class MovieCrewPoster extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
hasPosterError: false,
isEditMovieModalOpen: false
};
}
//
// Listeners
onEditMoviePress = () => {
this.setState({ isEditMovieModalOpen: true });
}
onEditMovieModalClose = () => {
this.setState({ isEditMovieModalOpen: false });
}
onPosterLoad = () => {
if (this.state.hasPosterError) {
this.setState({ hasPosterError: false });
}
}
onPosterLoadError = () => {
if (!this.state.hasPosterError) {
this.setState({ hasPosterError: true });
}
}
//
// Render
render() {
const {
crewName,
job,
images,
posterWidth,
posterHeight
} = this.props;
const {
hasPosterError
} = this.state;
const elementStyle = {
width: `${posterWidth}px`,
height: `${posterHeight}px`
};
return (
<div className={styles.content}>
<div className={styles.posterContainer}>
<Label className={styles.controls}>
<IconButton
className={styles.action}
name={icons.EDIT}
title="Edit movie"
onPress={this.onEditMoviePress}
/>
</Label>
<div
className={styles.poster}
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}>
{crewName}
</div>
}
</div>
</div>
<div className={styles.title}>
{crewName}
</div>
<div className={styles.title}>
{job}
</div>
</div>
);
}
}
MovieCrewPoster.propTypes = {
crewId: PropTypes.number.isRequired,
crewName: PropTypes.string.isRequired,
job: PropTypes.string.isRequired,
images: PropTypes.arrayOf(PropTypes.object).isRequired,
posterWidth: PropTypes.number.isRequired,
posterHeight: PropTypes.number.isRequired
};
export default MovieCrewPoster;

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

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

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

@ -30,7 +30,9 @@ import MoviePoster from 'Movie/MoviePoster';
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
import MovieHistoryTable from 'Movie/History/MovieHistoryTable';
import MovieTitlesTable from 'Movie/Titles/MovieTitlesTable';
import MovieTitlesTable from './Titles/MovieTitlesTable';
import MovieCastPostersConnector from './Cast/MovieCastPostersConnector';
import MovieCrewPostersConnector from './Crew/MovieCrewPostersConnector';
import MovieAlternateTitles from './MovieAlternateTitles';
import MovieDetailsLinks from './MovieDetailsLinks';
import InteractiveSearchTable from 'InteractiveSearch/InteractiveSearchTable';
@ -177,7 +179,9 @@ class MovieDetails extends Component {
isSearching,
isFetching,
isPopulated,
isSmallScreen,
movieFilesError,
moviePeopleError,
hasMovieFiles,
previousMovie,
nextMovie,
@ -460,12 +464,12 @@ class MovieDetails extends Component {
<div className={styles.contentContainer}>
{
!isPopulated && !movieFilesError &&
!isPopulated && !movieFilesError && !moviePeopleError &&
<LoadingIndicator />
}
{
!isFetching && movieFilesError &&
!isFetching && movieFilesError && !moviePeopleError &&
<div>Loading movie files failed</div>
}
@ -501,6 +505,20 @@ class MovieDetails extends Component {
Titles
</Tab>
<Tab
className={styles.tab}
selectedClassName={styles.selectedTab}
>
Cast
</Tab>
<Tab
className={styles.tab}
selectedClassName={styles.selectedTab}
>
Crew
</Tab>
{
selectedTabIndex === 1 &&
<div className={styles.filterIcon}>
@ -533,6 +551,18 @@ class MovieDetails extends Component {
movieId={id}
/>
</TabPanel>
<TabPanel>
<MovieCastPostersConnector
isSmallScreen={isSmallScreen}
/>
</TabPanel>
<TabPanel>
<MovieCrewPostersConnector
isSmallScreen={isSmallScreen}
/>
</TabPanel>
</Tabs>
</div>
@ -597,7 +627,9 @@ MovieDetails.propTypes = {
isSearching: PropTypes.bool.isRequired,
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
isSmallScreen: PropTypes.bool.isRequired,
movieFilesError: PropTypes.object,
moviePeopleError: PropTypes.object,
hasMovieFiles: PropTypes.bool.isRequired,
previousMovie: PropTypes.object.isRequired,
nextMovie: PropTypes.object.isRequired,

@ -7,7 +7,9 @@ import { findCommand, isCommandExecuting } from 'Utilities/Command';
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
import createAllMoviesSelector from 'Store/Selectors/createAllMoviesSelector';
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import { fetchMovieFiles, clearMovieFiles } from 'Store/Actions/movieFileActions';
import { fetchMoviePeople, clearMoviePeople } from 'Store/Actions/moviePeopleActions';
import { toggleMovieMonitored } from 'Store/Actions/movieActions';
import { fetchQueueDetails, clearQueueDetails } from 'Store/Actions/queueActions';
import { clearReleases, cancelFetchReleases } from 'Store/Actions/releaseActions';
@ -39,13 +41,32 @@ const selectMovieFiles = createSelector(
}
);
const selectMoviePeople = createSelector(
(state) => state.moviePeople,
(moviePeople) => {
const {
isFetching,
isPopulated,
error
} = moviePeople;
return {
isMoviePeopleFetching: isFetching,
isMoviePeoplePopulated: isPopulated,
moviePeopleError: error
};
}
);
function createMapStateToProps() {
return createSelector(
(state, { titleSlug }) => titleSlug,
selectMovieFiles,
selectMoviePeople,
createAllMoviesSelector(),
createCommandsSelector(),
(titleSlug, movieFiles, allMovies, commands) => {
createDimensionsSelector(),
(titleSlug, movieFiles, moviePeople, allMovies, commands, dimensions) => {
const sortedMovies = _.orderBy(allMovies, 'sortTitle');
const movieIndex = _.findIndex(sortedMovies, { titleSlug });
const movie = sortedMovies[movieIndex];
@ -62,6 +83,12 @@ function createMapStateToProps() {
sizeOnDisk
} = movieFiles;
const {
isMoviePeopleFetching,
isMoviePeoplePopulated,
moviePeopleError
} = moviePeople;
const previousMovie = sortedMovies[movieIndex - 1] || _.last(sortedMovies);
const nextMovie = sortedMovies[movieIndex + 1] || _.first(sortedMovies);
const isMovieRefreshing = isCommandExecuting(findCommand(commands, { name: commandNames.REFRESH_MOVIE, movieId: movie.id }));
@ -79,8 +106,8 @@ function createMapStateToProps() {
isRenamingMovieCommand.body.movieIds.indexOf(movie.id) > -1
);
const isFetching = isMovieFilesFetching;
const isPopulated = isMovieFilesPopulated;
const isFetching = isMovieFilesFetching && isMoviePeopleFetching;
const isPopulated = isMovieFilesPopulated && isMoviePeoplePopulated;
const alternateTitles = _.reduce(movie.alternateTitles, (acc, alternateTitle) => {
acc.push(alternateTitle.title);
return acc;
@ -98,10 +125,12 @@ function createMapStateToProps() {
isFetching,
isPopulated,
movieFilesError,
moviePeopleError,
hasMovieFiles,
sizeOnDisk,
previousMovie,
nextMovie
nextMovie,
isSmallScreen: dimensions.isSmallScreen
};
}
);
@ -110,6 +139,8 @@ function createMapStateToProps() {
const mapDispatchToProps = {
fetchMovieFiles,
clearMovieFiles,
fetchMoviePeople,
clearMoviePeople,
clearReleases,
cancelFetchReleases,
toggleMovieMonitored,
@ -167,12 +198,14 @@ class MovieDetailsConnector extends Component {
const movieId = this.props.id;
this.props.fetchMovieFiles({ movieId });
this.props.fetchMoviePeople({ movieId });
this.props.fetchQueueDetails({ movieId });
}
unpopulate = () => {
this.props.cancelFetchReleases();
this.props.clearMovieFiles();
this.props.clearMoviePeople();
this.props.clearQueueDetails();
this.props.clearReleases();
}
@ -224,8 +257,11 @@ MovieDetailsConnector.propTypes = {
isRefreshing: PropTypes.bool.isRequired,
isRenamingFiles: PropTypes.bool.isRequired,
isRenamingMovie: PropTypes.bool.isRequired,
isSmallScreen: PropTypes.bool.isRequired,
fetchMovieFiles: PropTypes.func.isRequired,
clearMovieFiles: PropTypes.func.isRequired,
fetchMoviePeople: PropTypes.func.isRequired,
clearMoviePeople: PropTypes.func.isRequired,
clearReleases: PropTypes.func.isRequired,
cancelFetchReleases: PropTypes.func.isRequired,
toggleMovieMonitored: PropTypes.func.isRequired,

@ -5,6 +5,7 @@ import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import styles from './MovieTitlesTableContent.css';
import MovieTitlesRow from './MovieTitlesRow';
const columns = [
{
name: 'altTitle',

@ -0,0 +1,25 @@
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;

@ -19,6 +19,7 @@ import * as rootFolders from './rootFolderActions';
import * as movies from './movieActions';
import * as movieHistory from './movieHistoryActions';
import * as movieIndex from './movieIndexActions';
import * as movieCredits from './movieCreditsActions';
import * as settings from './settingsActions';
import * as system from './systemActions';
import * as tags from './tagActions';
@ -45,6 +46,7 @@ export default [
movies,
movieHistory,
movieIndex,
movieCredits,
settings,
system,
tags

@ -0,0 +1,81 @@
import { createAction } from 'redux-actions';
import { batchActions } from 'redux-batched-actions';
import createAjaxRequest from 'Utilities/createAjaxRequest';
import { createThunk, handleThunks } from 'Store/thunks';
import createHandleActions from './Creators/createHandleActions';
import { set, update } from './baseActions';
//
// Variables
export const section = 'movieCredits';
//
// State
export const defaultState = {
isFetching: false,
isPopulated: false,
error: null,
items: []
};
//
// Actions Types
export const FETCH_MOVIE_CREDITS = 'movieCredits/fetchMovieCredits';
export const CLEAR_MOVIE_CREDITS = 'movieCredits/clearMovieCredits';
//
// Action Creators
export const fetchMovieCredits = createThunk(FETCH_MOVIE_CREDITS);
export const clearMovieCredits = createAction(CLEAR_MOVIE_CREDITS);
//
// Action Handlers
export const actionHandlers = handleThunks({
[FETCH_MOVIE_CREDITS]: function(getState, payload, dispatch) {
dispatch(set({ section, isFetching: true }));
const promise = createAjaxRequest({
url: '/credit',
data: payload
}).request;
promise.done((data) => {
dispatch(batchActions([
update({ section, data }),
set({
section,
isFetching: false,
isPopulated: true,
error: null
})
]));
});
promise.fail((xhr) => {
dispatch(set({
section,
isFetching: false,
isPopulated: false,
error: xhr
}));
});
}
});
//
// Reducers
export const reducers = createHandleActions({
[CLEAR_MOVIE_CREDITS]: (state) => {
return Object.assign({}, state, defaultState);
}
}, defaultState, section);
Loading…
Cancel
Save