diff --git a/frontend/src/Movie/Details/Cast/MovieCastPoster.css b/frontend/src/Movie/Details/Cast/MovieCastPoster.css
new file mode 100644
index 000000000..3c0d27827
--- /dev/null
+++ b/frontend/src/Movie/Details/Cast/MovieCastPoster.css
@@ -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;
+ }
+}
diff --git a/frontend/src/Movie/Details/Cast/MovieCastPoster.js b/frontend/src/Movie/Details/Cast/MovieCastPoster.js
new file mode 100644
index 000000000..5237a72a4
--- /dev/null
+++ b/frontend/src/Movie/Details/Cast/MovieCastPoster.js
@@ -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 (
+
{
- !isPopulated && !movieFilesError &&
+ !isPopulated && !movieFilesError && !moviePeopleError &&
}
{
- !isFetching && movieFilesError &&
+ !isFetching && movieFilesError && !moviePeopleError &&
Loading movie files failed
}
@@ -501,6 +505,20 @@ class MovieDetails extends Component {
Titles
+
+ Cast
+
+
+
+ Crew
+
+
{
selectedTabIndex === 1 &&
@@ -533,6 +551,18 @@ class MovieDetails extends Component {
movieId={id}
/>
+
+
+
+
+
+
+
+
@@ -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,
diff --git a/frontend/src/Movie/Details/MovieDetailsConnector.js b/frontend/src/Movie/Details/MovieDetailsConnector.js
index 5c3eff61c..36dd4ac09 100644
--- a/frontend/src/Movie/Details/MovieDetailsConnector.js
+++ b/frontend/src/Movie/Details/MovieDetailsConnector.js
@@ -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,
diff --git a/frontend/src/Movie/Titles/MovieTitlesRow.js b/frontend/src/Movie/Details/Titles/MovieTitlesRow.js
similarity index 100%
rename from frontend/src/Movie/Titles/MovieTitlesRow.js
rename to frontend/src/Movie/Details/Titles/MovieTitlesRow.js
diff --git a/frontend/src/Movie/Titles/MovieTitlesTable.js b/frontend/src/Movie/Details/Titles/MovieTitlesTable.js
similarity index 100%
rename from frontend/src/Movie/Titles/MovieTitlesTable.js
rename to frontend/src/Movie/Details/Titles/MovieTitlesTable.js
diff --git a/frontend/src/Movie/Titles/MovieTitlesTableContent.css b/frontend/src/Movie/Details/Titles/MovieTitlesTableContent.css
similarity index 100%
rename from frontend/src/Movie/Titles/MovieTitlesTableContent.css
rename to frontend/src/Movie/Details/Titles/MovieTitlesTableContent.css
diff --git a/frontend/src/Movie/Titles/MovieTitlesTableContent.js b/frontend/src/Movie/Details/Titles/MovieTitlesTableContent.js
similarity index 99%
rename from frontend/src/Movie/Titles/MovieTitlesTableContent.js
rename to frontend/src/Movie/Details/Titles/MovieTitlesTableContent.js
index 1d00035f8..3492ad023 100644
--- a/frontend/src/Movie/Titles/MovieTitlesTableContent.js
+++ b/frontend/src/Movie/Details/Titles/MovieTitlesTableContent.js
@@ -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',
diff --git a/frontend/src/Movie/Titles/MovieTitlesTableContentConnector.js b/frontend/src/Movie/Details/Titles/MovieTitlesTableContentConnector.js
similarity index 100%
rename from frontend/src/Movie/Titles/MovieTitlesTableContentConnector.js
rename to frontend/src/Movie/Details/Titles/MovieTitlesTableContentConnector.js
diff --git a/frontend/src/Movie/MovieHeadshot.js b/frontend/src/Movie/MovieHeadshot.js
new file mode 100644
index 000000000..490d693bf
--- /dev/null
+++ b/frontend/src/Movie/MovieHeadshot.js
@@ -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 (
+
+ );
+}
+
+MovieHeadshot.propTypes = {
+ size: PropTypes.number.isRequired
+};
+
+MovieHeadshot.defaultProps = {
+ size: 250
+};
+
+export default MovieHeadshot;
diff --git a/frontend/src/Store/Actions/index.js b/frontend/src/Store/Actions/index.js
index d948cf7bf..03b67a0bc 100644
--- a/frontend/src/Store/Actions/index.js
+++ b/frontend/src/Store/Actions/index.js
@@ -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
diff --git a/frontend/src/Store/Actions/movieCreditsActions.js b/frontend/src/Store/Actions/movieCreditsActions.js
new file mode 100644
index 000000000..004571fd8
--- /dev/null
+++ b/frontend/src/Store/Actions/movieCreditsActions.js
@@ -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);