mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2025-09-17 17:14:18 +02:00
New: Cast/Crew Tabs on Movie Details Page
This commit is contained in:
76
frontend/src/Movie/Details/Cast/MovieCastPoster.css
Normal file
76
frontend/src/Movie/Details/Cast/MovieCastPoster.css
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
123
frontend/src/Movie/Details/Cast/MovieCastPoster.js
Normal file
123
frontend/src/Movie/Details/Cast/MovieCastPoster.js
Normal file
@@ -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;
|
7
frontend/src/Movie/Details/Cast/MovieCastPosters.css
Normal file
7
frontend/src/Movie/Details/Cast/MovieCastPosters.css
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
.grid {
|
||||||
|
flex: 1 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
228
frontend/src/Movie/Details/Cast/MovieCastPosters.js
Normal file
228
frontend/src/Movie/Details/Cast/MovieCastPosters.js
Normal file
@@ -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;
|
25
frontend/src/Movie/Details/Cast/MovieCastPostersConnector.js
Normal file
25
frontend/src/Movie/Details/Cast/MovieCastPostersConnector.js
Normal file
@@ -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);
|
76
frontend/src/Movie/Details/Crew/MovieCrewPoster.css
Normal file
76
frontend/src/Movie/Details/Crew/MovieCrewPoster.css
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
123
frontend/src/Movie/Details/Crew/MovieCrewPoster.js
Normal file
123
frontend/src/Movie/Details/Crew/MovieCrewPoster.js
Normal file
@@ -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;
|
7
frontend/src/Movie/Details/Crew/MovieCrewPosters.css
Normal file
7
frontend/src/Movie/Details/Crew/MovieCrewPosters.css
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
.grid {
|
||||||
|
flex: 1 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
228
frontend/src/Movie/Details/Crew/MovieCrewPosters.js
Normal file
228
frontend/src/Movie/Details/Crew/MovieCrewPosters.js
Normal file
@@ -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;
|
25
frontend/src/Movie/Details/Crew/MovieCrewPostersConnector.js
Normal file
25
frontend/src/Movie/Details/Crew/MovieCrewPostersConnector.js
Normal file
@@ -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 EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
|
||||||
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
|
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
|
||||||
import MovieHistoryTable from 'Movie/History/MovieHistoryTable';
|
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 MovieAlternateTitles from './MovieAlternateTitles';
|
||||||
import MovieDetailsLinks from './MovieDetailsLinks';
|
import MovieDetailsLinks from './MovieDetailsLinks';
|
||||||
import InteractiveSearchTable from 'InteractiveSearch/InteractiveSearchTable';
|
import InteractiveSearchTable from 'InteractiveSearch/InteractiveSearchTable';
|
||||||
@@ -177,7 +179,9 @@ class MovieDetails extends Component {
|
|||||||
isSearching,
|
isSearching,
|
||||||
isFetching,
|
isFetching,
|
||||||
isPopulated,
|
isPopulated,
|
||||||
|
isSmallScreen,
|
||||||
movieFilesError,
|
movieFilesError,
|
||||||
|
moviePeopleError,
|
||||||
hasMovieFiles,
|
hasMovieFiles,
|
||||||
previousMovie,
|
previousMovie,
|
||||||
nextMovie,
|
nextMovie,
|
||||||
@@ -460,12 +464,12 @@ class MovieDetails extends Component {
|
|||||||
|
|
||||||
<div className={styles.contentContainer}>
|
<div className={styles.contentContainer}>
|
||||||
{
|
{
|
||||||
!isPopulated && !movieFilesError &&
|
!isPopulated && !movieFilesError && !moviePeopleError &&
|
||||||
<LoadingIndicator />
|
<LoadingIndicator />
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
!isFetching && movieFilesError &&
|
!isFetching && movieFilesError && !moviePeopleError &&
|
||||||
<div>Loading movie files failed</div>
|
<div>Loading movie files failed</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -501,6 +505,20 @@ class MovieDetails extends Component {
|
|||||||
Titles
|
Titles
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
||||||
|
<Tab
|
||||||
|
className={styles.tab}
|
||||||
|
selectedClassName={styles.selectedTab}
|
||||||
|
>
|
||||||
|
Cast
|
||||||
|
</Tab>
|
||||||
|
|
||||||
|
<Tab
|
||||||
|
className={styles.tab}
|
||||||
|
selectedClassName={styles.selectedTab}
|
||||||
|
>
|
||||||
|
Crew
|
||||||
|
</Tab>
|
||||||
|
|
||||||
{
|
{
|
||||||
selectedTabIndex === 1 &&
|
selectedTabIndex === 1 &&
|
||||||
<div className={styles.filterIcon}>
|
<div className={styles.filterIcon}>
|
||||||
@@ -533,6 +551,18 @@ class MovieDetails extends Component {
|
|||||||
movieId={id}
|
movieId={id}
|
||||||
/>
|
/>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
|
||||||
|
<TabPanel>
|
||||||
|
<MovieCastPostersConnector
|
||||||
|
isSmallScreen={isSmallScreen}
|
||||||
|
/>
|
||||||
|
</TabPanel>
|
||||||
|
|
||||||
|
<TabPanel>
|
||||||
|
<MovieCrewPostersConnector
|
||||||
|
isSmallScreen={isSmallScreen}
|
||||||
|
/>
|
||||||
|
</TabPanel>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -597,7 +627,9 @@ MovieDetails.propTypes = {
|
|||||||
isSearching: PropTypes.bool.isRequired,
|
isSearching: PropTypes.bool.isRequired,
|
||||||
isFetching: PropTypes.bool.isRequired,
|
isFetching: PropTypes.bool.isRequired,
|
||||||
isPopulated: PropTypes.bool.isRequired,
|
isPopulated: PropTypes.bool.isRequired,
|
||||||
|
isSmallScreen: PropTypes.bool.isRequired,
|
||||||
movieFilesError: PropTypes.object,
|
movieFilesError: PropTypes.object,
|
||||||
|
moviePeopleError: PropTypes.object,
|
||||||
hasMovieFiles: PropTypes.bool.isRequired,
|
hasMovieFiles: PropTypes.bool.isRequired,
|
||||||
previousMovie: PropTypes.object.isRequired,
|
previousMovie: PropTypes.object.isRequired,
|
||||||
nextMovie: PropTypes.object.isRequired,
|
nextMovie: PropTypes.object.isRequired,
|
||||||
|
@@ -7,7 +7,9 @@ import { findCommand, isCommandExecuting } from 'Utilities/Command';
|
|||||||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||||
import createAllMoviesSelector from 'Store/Selectors/createAllMoviesSelector';
|
import createAllMoviesSelector from 'Store/Selectors/createAllMoviesSelector';
|
||||||
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
||||||
|
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||||
import { fetchMovieFiles, clearMovieFiles } from 'Store/Actions/movieFileActions';
|
import { fetchMovieFiles, clearMovieFiles } from 'Store/Actions/movieFileActions';
|
||||||
|
import { fetchMoviePeople, clearMoviePeople } from 'Store/Actions/moviePeopleActions';
|
||||||
import { toggleMovieMonitored } from 'Store/Actions/movieActions';
|
import { toggleMovieMonitored } from 'Store/Actions/movieActions';
|
||||||
import { fetchQueueDetails, clearQueueDetails } from 'Store/Actions/queueActions';
|
import { fetchQueueDetails, clearQueueDetails } from 'Store/Actions/queueActions';
|
||||||
import { clearReleases, cancelFetchReleases } from 'Store/Actions/releaseActions';
|
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() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state, { titleSlug }) => titleSlug,
|
(state, { titleSlug }) => titleSlug,
|
||||||
selectMovieFiles,
|
selectMovieFiles,
|
||||||
|
selectMoviePeople,
|
||||||
createAllMoviesSelector(),
|
createAllMoviesSelector(),
|
||||||
createCommandsSelector(),
|
createCommandsSelector(),
|
||||||
(titleSlug, movieFiles, allMovies, commands) => {
|
createDimensionsSelector(),
|
||||||
|
(titleSlug, movieFiles, moviePeople, allMovies, commands, dimensions) => {
|
||||||
const sortedMovies = _.orderBy(allMovies, 'sortTitle');
|
const sortedMovies = _.orderBy(allMovies, 'sortTitle');
|
||||||
const movieIndex = _.findIndex(sortedMovies, { titleSlug });
|
const movieIndex = _.findIndex(sortedMovies, { titleSlug });
|
||||||
const movie = sortedMovies[movieIndex];
|
const movie = sortedMovies[movieIndex];
|
||||||
@@ -62,6 +83,12 @@ function createMapStateToProps() {
|
|||||||
sizeOnDisk
|
sizeOnDisk
|
||||||
} = movieFiles;
|
} = movieFiles;
|
||||||
|
|
||||||
|
const {
|
||||||
|
isMoviePeopleFetching,
|
||||||
|
isMoviePeoplePopulated,
|
||||||
|
moviePeopleError
|
||||||
|
} = moviePeople;
|
||||||
|
|
||||||
const previousMovie = sortedMovies[movieIndex - 1] || _.last(sortedMovies);
|
const previousMovie = sortedMovies[movieIndex - 1] || _.last(sortedMovies);
|
||||||
const nextMovie = sortedMovies[movieIndex + 1] || _.first(sortedMovies);
|
const nextMovie = sortedMovies[movieIndex + 1] || _.first(sortedMovies);
|
||||||
const isMovieRefreshing = isCommandExecuting(findCommand(commands, { name: commandNames.REFRESH_MOVIE, movieId: movie.id }));
|
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
|
isRenamingMovieCommand.body.movieIds.indexOf(movie.id) > -1
|
||||||
);
|
);
|
||||||
|
|
||||||
const isFetching = isMovieFilesFetching;
|
const isFetching = isMovieFilesFetching && isMoviePeopleFetching;
|
||||||
const isPopulated = isMovieFilesPopulated;
|
const isPopulated = isMovieFilesPopulated && isMoviePeoplePopulated;
|
||||||
const alternateTitles = _.reduce(movie.alternateTitles, (acc, alternateTitle) => {
|
const alternateTitles = _.reduce(movie.alternateTitles, (acc, alternateTitle) => {
|
||||||
acc.push(alternateTitle.title);
|
acc.push(alternateTitle.title);
|
||||||
return acc;
|
return acc;
|
||||||
@@ -98,10 +125,12 @@ function createMapStateToProps() {
|
|||||||
isFetching,
|
isFetching,
|
||||||
isPopulated,
|
isPopulated,
|
||||||
movieFilesError,
|
movieFilesError,
|
||||||
|
moviePeopleError,
|
||||||
hasMovieFiles,
|
hasMovieFiles,
|
||||||
sizeOnDisk,
|
sizeOnDisk,
|
||||||
previousMovie,
|
previousMovie,
|
||||||
nextMovie
|
nextMovie,
|
||||||
|
isSmallScreen: dimensions.isSmallScreen
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -110,6 +139,8 @@ function createMapStateToProps() {
|
|||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
fetchMovieFiles,
|
fetchMovieFiles,
|
||||||
clearMovieFiles,
|
clearMovieFiles,
|
||||||
|
fetchMoviePeople,
|
||||||
|
clearMoviePeople,
|
||||||
clearReleases,
|
clearReleases,
|
||||||
cancelFetchReleases,
|
cancelFetchReleases,
|
||||||
toggleMovieMonitored,
|
toggleMovieMonitored,
|
||||||
@@ -167,12 +198,14 @@ class MovieDetailsConnector extends Component {
|
|||||||
const movieId = this.props.id;
|
const movieId = this.props.id;
|
||||||
|
|
||||||
this.props.fetchMovieFiles({ movieId });
|
this.props.fetchMovieFiles({ movieId });
|
||||||
|
this.props.fetchMoviePeople({ movieId });
|
||||||
this.props.fetchQueueDetails({ movieId });
|
this.props.fetchQueueDetails({ movieId });
|
||||||
}
|
}
|
||||||
|
|
||||||
unpopulate = () => {
|
unpopulate = () => {
|
||||||
this.props.cancelFetchReleases();
|
this.props.cancelFetchReleases();
|
||||||
this.props.clearMovieFiles();
|
this.props.clearMovieFiles();
|
||||||
|
this.props.clearMoviePeople();
|
||||||
this.props.clearQueueDetails();
|
this.props.clearQueueDetails();
|
||||||
this.props.clearReleases();
|
this.props.clearReleases();
|
||||||
}
|
}
|
||||||
@@ -224,8 +257,11 @@ MovieDetailsConnector.propTypes = {
|
|||||||
isRefreshing: PropTypes.bool.isRequired,
|
isRefreshing: PropTypes.bool.isRequired,
|
||||||
isRenamingFiles: PropTypes.bool.isRequired,
|
isRenamingFiles: PropTypes.bool.isRequired,
|
||||||
isRenamingMovie: PropTypes.bool.isRequired,
|
isRenamingMovie: PropTypes.bool.isRequired,
|
||||||
|
isSmallScreen: PropTypes.bool.isRequired,
|
||||||
fetchMovieFiles: PropTypes.func.isRequired,
|
fetchMovieFiles: PropTypes.func.isRequired,
|
||||||
clearMovieFiles: PropTypes.func.isRequired,
|
clearMovieFiles: PropTypes.func.isRequired,
|
||||||
|
fetchMoviePeople: PropTypes.func.isRequired,
|
||||||
|
clearMoviePeople: PropTypes.func.isRequired,
|
||||||
clearReleases: PropTypes.func.isRequired,
|
clearReleases: PropTypes.func.isRequired,
|
||||||
cancelFetchReleases: PropTypes.func.isRequired,
|
cancelFetchReleases: PropTypes.func.isRequired,
|
||||||
toggleMovieMonitored: PropTypes.func.isRequired,
|
toggleMovieMonitored: PropTypes.func.isRequired,
|
||||||
|
@@ -5,6 +5,7 @@ import Table from 'Components/Table/Table';
|
|||||||
import TableBody from 'Components/Table/TableBody';
|
import TableBody from 'Components/Table/TableBody';
|
||||||
import styles from './MovieTitlesTableContent.css';
|
import styles from './MovieTitlesTableContent.css';
|
||||||
import MovieTitlesRow from './MovieTitlesRow';
|
import MovieTitlesRow from './MovieTitlesRow';
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
name: 'altTitle',
|
name: 'altTitle',
|
25
frontend/src/Movie/MovieHeadshot.js
Normal file
25
frontend/src/Movie/MovieHeadshot.js
Normal file
@@ -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 movies from './movieActions';
|
||||||
import * as movieHistory from './movieHistoryActions';
|
import * as movieHistory from './movieHistoryActions';
|
||||||
import * as movieIndex from './movieIndexActions';
|
import * as movieIndex from './movieIndexActions';
|
||||||
|
import * as movieCredits from './movieCreditsActions';
|
||||||
import * as settings from './settingsActions';
|
import * as settings from './settingsActions';
|
||||||
import * as system from './systemActions';
|
import * as system from './systemActions';
|
||||||
import * as tags from './tagActions';
|
import * as tags from './tagActions';
|
||||||
@@ -45,6 +46,7 @@ export default [
|
|||||||
movies,
|
movies,
|
||||||
movieHistory,
|
movieHistory,
|
||||||
movieIndex,
|
movieIndex,
|
||||||
|
movieCredits,
|
||||||
settings,
|
settings,
|
||||||
system,
|
system,
|
||||||
tags
|
tags
|
||||||
|
81
frontend/src/Store/Actions/movieCreditsActions.js
Normal file
81
frontend/src/Store/Actions/movieCreditsActions.js
Normal file
@@ -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);
|
Reference in New Issue
Block a user