mirror of
https://github.com/Radarr/Radarr.git
synced 2024-11-20 18:02:44 +01:00
New: Rework Movie Details view
This commit is contained in:
parent
40eeb31a21
commit
8e0645670b
@ -63,6 +63,12 @@
|
||||
width: 1280px;
|
||||
}
|
||||
|
||||
.extraExtraLarge {
|
||||
composes: modal;
|
||||
|
||||
width: 1600px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointExtraLarge) {
|
||||
.modal.extraLarge {
|
||||
width: 90%;
|
||||
|
1
frontend/src/Components/Modal/Modal.css.d.ts
vendored
1
frontend/src/Components/Modal/Modal.css.d.ts
vendored
@ -1,6 +1,7 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'extraExtraLarge': string;
|
||||
'extraLarge': string;
|
||||
'large': string;
|
||||
'medium': string;
|
||||
|
@ -3,5 +3,6 @@ export const SMALL = 'small';
|
||||
export const MEDIUM = 'medium';
|
||||
export const LARGE = 'large';
|
||||
export const EXTRA_LARGE = 'extraLarge';
|
||||
export const EXTRA_EXTRA_LARGE = 'extraExtraLarge';
|
||||
|
||||
export const all = [EXTRA_SMALL, SMALL, MEDIUM, LARGE, EXTRA_LARGE];
|
||||
export const all = [EXTRA_SMALL, SMALL, MEDIUM, LARGE, EXTRA_LARGE, EXTRA_EXTRA_LARGE];
|
||||
|
@ -1,11 +1,8 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Label from 'Components/Label';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||
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 {
|
||||
@ -60,7 +57,7 @@ class MovieCastPoster extends Component {
|
||||
images,
|
||||
posterWidth,
|
||||
posterHeight,
|
||||
importListId
|
||||
importList
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
@ -69,36 +66,31 @@ class MovieCastPoster extends Component {
|
||||
|
||||
const elementStyle = {
|
||||
width: `${posterWidth}px`,
|
||||
height: `${posterHeight}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}>
|
||||
<Label className={styles.controls}>
|
||||
{
|
||||
importListId > 0 ?
|
||||
<IconButton
|
||||
className={styles.action}
|
||||
name={icons.EDIT}
|
||||
title={translate('EditPerson')}
|
||||
onPress={this.onEditImportListPress}
|
||||
/> :
|
||||
<IconButton
|
||||
className={styles.action}
|
||||
name={icons.ADD}
|
||||
title={translate('FollowPerson')}
|
||||
onPress={this.onAddImportListPress}
|
||||
/>
|
||||
}
|
||||
</Label>
|
||||
<div className={styles.controls}>
|
||||
<MonitorToggleButton
|
||||
className={styles.action}
|
||||
monitored={monitored}
|
||||
size={20}
|
||||
onPress={importListId > 0 ? this.onEditImportListPress : this.onAddImportListPress}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={elementStyle}
|
||||
@ -148,12 +140,8 @@ MovieCastPoster.propTypes = {
|
||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
posterWidth: PropTypes.number.isRequired,
|
||||
posterHeight: PropTypes.number.isRequired,
|
||||
importListId: PropTypes.number.isRequired,
|
||||
importList: PropTypes.object,
|
||||
onImportListSelect: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
MovieCastPoster.defaultProps = {
|
||||
importListId: 0
|
||||
};
|
||||
|
||||
export default MovieCastPoster;
|
||||
|
@ -1,11 +1,8 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Label from 'Components/Label';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||
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 {
|
||||
@ -60,7 +57,7 @@ class MovieCrewPoster extends Component {
|
||||
images,
|
||||
posterWidth,
|
||||
posterHeight,
|
||||
importListId
|
||||
importList
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
@ -69,36 +66,31 @@ class MovieCrewPoster extends Component {
|
||||
|
||||
const elementStyle = {
|
||||
width: `${posterWidth}px`,
|
||||
height: `${posterHeight}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}>
|
||||
<Label className={styles.controls}>
|
||||
{
|
||||
importListId > 0 ?
|
||||
<IconButton
|
||||
className={styles.action}
|
||||
name={icons.EDIT}
|
||||
title={translate('EditPerson')}
|
||||
onPress={this.onEditImportListPress}
|
||||
/> :
|
||||
<IconButton
|
||||
className={styles.action}
|
||||
name={icons.ADD}
|
||||
title={translate('FollowPerson')}
|
||||
onPress={this.onAddImportListPress}
|
||||
/>
|
||||
}
|
||||
</Label>
|
||||
<div className={styles.controls}>
|
||||
<MonitorToggleButton
|
||||
className={styles.action}
|
||||
monitored={monitored}
|
||||
size={20}
|
||||
onPress={importListId > 0 ? this.onEditImportListPress : this.onAddImportListPress}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={elementStyle}
|
||||
@ -148,12 +140,8 @@ MovieCrewPoster.propTypes = {
|
||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
posterWidth: PropTypes.number.isRequired,
|
||||
posterHeight: PropTypes.number.isRequired,
|
||||
importListId: PropTypes.number.isRequired,
|
||||
importList: PropTypes.object,
|
||||
onImportListSelect: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
MovieCrewPoster.defaultProps = {
|
||||
importListId: 0
|
||||
};
|
||||
|
||||
export default MovieCrewPoster;
|
||||
|
@ -18,7 +18,7 @@ function createMapStateToProps() {
|
||||
}, []);
|
||||
|
||||
return {
|
||||
items: crew
|
||||
items: _.uniqBy(crew, 'personName')
|
||||
};
|
||||
}
|
||||
);
|
||||
|
@ -1,17 +1,13 @@
|
||||
$hoverScale: 1.05;
|
||||
|
||||
.content {
|
||||
border-radius: '5px';
|
||||
transition: all 200ms ease-in;
|
||||
|
||||
&:hover {
|
||||
z-index: 2;
|
||||
box-shadow: 0 0 12px var(--black);
|
||||
transition: all 200ms ease-in;
|
||||
|
||||
.controls {
|
||||
opacity: 0.9;
|
||||
transition: opacity 200ms linear 150ms;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,22 +46,18 @@ $hoverScale: 1.05;
|
||||
|
||||
.controls {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
left: 10px;
|
||||
top: 10px;
|
||||
z-index: 3;
|
||||
border-radius: 4px;
|
||||
background-color: #707070;
|
||||
color: var(--white);
|
||||
font-size: $smallFontSize;
|
||||
opacity: 0;
|
||||
transition: opacity 0;
|
||||
}
|
||||
|
||||
.action {
|
||||
composes: button from '~Components/Link/IconButton.css';
|
||||
composes: toggleButton from '~Components/MonitorToggleButton.css';
|
||||
|
||||
width: 25px;
|
||||
color: var(--white);
|
||||
|
||||
&:hover {
|
||||
color: var(--radarrYellow);
|
||||
color: var(--iconButtonHoverLightColor);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,11 +1,19 @@
|
||||
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 createMovieCreditListSelector();
|
||||
return createSelector(
|
||||
createMovieCreditListSelector(),
|
||||
(importList) => {
|
||||
return {
|
||||
importList
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
@ -20,7 +28,7 @@ class MovieCreditPosterConnector extends Component {
|
||||
// Listeners
|
||||
|
||||
onImportListSelect = () => {
|
||||
this.props.selectImportListSchema({ implementation: 'TMDbPersonImport', presetName: undefined });
|
||||
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}` });
|
||||
};
|
||||
|
@ -2,6 +2,10 @@
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
.movie {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
interface CssExports {
|
||||
'container': string;
|
||||
'grid': string;
|
||||
'movie': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
|
@ -1,12 +1,15 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { Grid, WindowScroller } from 'react-virtualized';
|
||||
import Measure from 'Components/Measure';
|
||||
import { Navigation } from 'swiper';
|
||||
import { Swiper, SwiperSlide } from 'swiper/react';
|
||||
import dimensions from 'Styles/Variables/dimensions';
|
||||
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
|
||||
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);
|
||||
@ -65,39 +68,11 @@ class MovieCreditPosters extends Component {
|
||||
};
|
||||
|
||||
this._isInitialized = false;
|
||||
this._grid = null;
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const {
|
||||
items
|
||||
} = 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.items, items))) {
|
||||
// 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;
|
||||
@ -117,7 +92,10 @@ class MovieCreditPosters extends Component {
|
||||
});
|
||||
};
|
||||
|
||||
cellRenderer = ({ key, rowIndex, columnIndex, style }) => {
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
items,
|
||||
itemComponent
|
||||
@ -126,99 +104,44 @@ class MovieCreditPosters extends Component {
|
||||
const {
|
||||
posterWidth,
|
||||
posterHeight,
|
||||
columnCount
|
||||
} = this.state;
|
||||
|
||||
const movieIdx = rowIndex * columnCount + columnIndex;
|
||||
const movie = items[movieIdx];
|
||||
|
||||
if (!movie) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.container}
|
||||
key={key}
|
||||
style={style}
|
||||
>
|
||||
<MovieCreditPosterConnector
|
||||
key={movie.order}
|
||||
component={itemComponent}
|
||||
posterWidth={posterWidth}
|
||||
posterHeight={posterHeight}
|
||||
tmdbId={movie.personTmdbId}
|
||||
personName={movie.personName}
|
||||
job={movie.job}
|
||||
character={movie.character}
|
||||
images={movie.images}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onMeasure = ({ width }) => {
|
||||
this.calculateGrid(width, this.props.isSmallScreen);
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
items
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
width,
|
||||
columnWidth,
|
||||
columnCount,
|
||||
rowHeight
|
||||
} = this.state;
|
||||
|
||||
const rowCount = Math.ceil(items.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>
|
||||
<div className={styles.sliderContainer}>
|
||||
<Swiper
|
||||
slidesPerView='auto'
|
||||
spaceBetween={10}
|
||||
slidesPerGroup={3}
|
||||
loop={false}
|
||||
loopFillGroupWithBlank={true}
|
||||
className="mySwiper"
|
||||
modules={[Navigation]}
|
||||
onInit={(swiper) => {
|
||||
swiper.params.navigation.prevEl = this._swiperPrevRef;
|
||||
swiper.params.navigation.nextEl = this._swiperNextRef;
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +0,0 @@
|
||||
.alternateTitle {
|
||||
white-space: nowrap;
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import styles from './MovieAlternateTitles.css';
|
||||
|
||||
function MovieAlternateTitles({ alternateTitles }) {
|
||||
return (
|
||||
<ul>
|
||||
{
|
||||
alternateTitles.filter((x, i, a) => a.indexOf(x) === i).map((alternateTitle) => {
|
||||
return (
|
||||
<li
|
||||
key={alternateTitle}
|
||||
className={styles.alternateTitle}
|
||||
>
|
||||
{alternateTitle}
|
||||
</li>
|
||||
);
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
MovieAlternateTitles.propTypes = {
|
||||
alternateTitles: PropTypes.arrayOf(PropTypes.string).isRequired
|
||||
};
|
||||
|
||||
export default MovieAlternateTitles;
|
@ -5,7 +5,7 @@
|
||||
.header {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 375px;
|
||||
height: 425px;
|
||||
}
|
||||
|
||||
.errorMessage {
|
||||
@ -39,10 +39,11 @@
|
||||
}
|
||||
|
||||
.poster {
|
||||
z-index: 2;
|
||||
flex-shrink: 0;
|
||||
margin-right: 35px;
|
||||
width: 217px;
|
||||
height: 319px;
|
||||
width: 250px;
|
||||
height: 368px;
|
||||
}
|
||||
|
||||
.info {
|
||||
|
@ -1,9 +1,9 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { Tab, TabList, TabPanel, Tabs } from 'react-tabs';
|
||||
import TextTruncate from 'react-text-truncate';
|
||||
import Alert from 'Components/Alert';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Icon from 'Components/Icon';
|
||||
import ImdbRating from 'Components/ImdbRating';
|
||||
import InfoLabel from 'Components/InfoLabel';
|
||||
@ -25,7 +25,7 @@ import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
|
||||
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
||||
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
|
||||
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
|
||||
import MovieHistoryTable from 'Movie/History/MovieHistoryTable';
|
||||
import MovieHistoryModal from 'Movie/History/MovieHistoryModal';
|
||||
import MoviePoster from 'Movie/MoviePoster';
|
||||
import MovieInteractiveSearchModalConnector from 'Movie/Search/MovieInteractiveSearchModalConnector';
|
||||
import MovieFileEditorTable from 'MovieFile/Editor/MovieFileEditorTable';
|
||||
@ -78,10 +78,10 @@ class MovieDetails extends Component {
|
||||
isDeleteMovieModalOpen: false,
|
||||
isInteractiveImportModalOpen: false,
|
||||
isInteractiveSearchModalOpen: false,
|
||||
isMovieHistoryModalOpen: false,
|
||||
allExpanded: false,
|
||||
allCollapsed: false,
|
||||
expandedState: {},
|
||||
selectedTabIndex: 0,
|
||||
overviewHeight: 0,
|
||||
titleWidth: 0
|
||||
};
|
||||
@ -153,6 +153,14 @@ class MovieDetails extends Component {
|
||||
this.setState({ isDeleteMovieModalOpen: false });
|
||||
};
|
||||
|
||||
onMovieHistoryPress = () => {
|
||||
this.setState({ isMovieHistoryModalOpen: true });
|
||||
};
|
||||
|
||||
onMovieHistoryModalClose = () => {
|
||||
this.setState({ isMovieHistoryModalOpen: false });
|
||||
};
|
||||
|
||||
onExpandAllPress = () => {
|
||||
const {
|
||||
allExpanded,
|
||||
@ -304,9 +312,9 @@ class MovieDetails extends Component {
|
||||
isDeleteMovieModalOpen,
|
||||
isInteractiveImportModalOpen,
|
||||
isInteractiveSearchModalOpen,
|
||||
isMovieHistoryModalOpen,
|
||||
overviewHeight,
|
||||
titleWidth,
|
||||
selectedTabIndex
|
||||
titleWidth
|
||||
} = this.state;
|
||||
|
||||
const fanartUrl = getFanartUrl(images);
|
||||
@ -356,6 +364,13 @@ class MovieDetails extends Component {
|
||||
onPress={this.onInteractiveImportPress}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label={translate('History')}
|
||||
iconName={icons.HISTORY}
|
||||
isDisabled={!hasMovieFiles}
|
||||
onPress={this.onMovieHistoryPress}
|
||||
/>
|
||||
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<PageToolbarButton
|
||||
@ -671,81 +686,33 @@ class MovieDetails extends Component {
|
||||
null
|
||||
}
|
||||
|
||||
<Tabs selectedIndex={selectedTabIndex} onSelect={this.onTabSelect}>
|
||||
<TabList
|
||||
className={styles.tabList}
|
||||
>
|
||||
<Tab
|
||||
className={styles.tab}
|
||||
selectedClassName={styles.selectedTab}
|
||||
>
|
||||
{translate('History')}
|
||||
</Tab>
|
||||
<FieldSet legend={translate('Files')}>
|
||||
<MovieFileEditorTable
|
||||
movieId={id}
|
||||
/>
|
||||
|
||||
<Tab
|
||||
className={styles.tab}
|
||||
selectedClassName={styles.selectedTab}
|
||||
>
|
||||
{translate('Files')}
|
||||
</Tab>
|
||||
<ExtraFileTable
|
||||
movieId={id}
|
||||
/>
|
||||
</FieldSet>
|
||||
|
||||
<Tab
|
||||
className={styles.tab}
|
||||
selectedClassName={styles.selectedTab}
|
||||
>
|
||||
{translate('Titles')}
|
||||
</Tab>
|
||||
<FieldSet legend={translate('Cast')}>
|
||||
<MovieCastPostersConnector
|
||||
isSmallScreen={isSmallScreen}
|
||||
/>
|
||||
</FieldSet>
|
||||
|
||||
<Tab
|
||||
className={styles.tab}
|
||||
selectedClassName={styles.selectedTab}
|
||||
>
|
||||
{translate('Cast')}
|
||||
</Tab>
|
||||
|
||||
<Tab
|
||||
className={styles.tab}
|
||||
selectedClassName={styles.selectedTab}
|
||||
>
|
||||
{translate('Crew')}
|
||||
</Tab>
|
||||
|
||||
</TabList>
|
||||
|
||||
<TabPanel>
|
||||
<MovieHistoryTable
|
||||
movieId={id}
|
||||
/>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel>
|
||||
<MovieFileEditorTable
|
||||
movieId={id}
|
||||
/>
|
||||
<ExtraFileTable
|
||||
movieId={id}
|
||||
/>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel>
|
||||
<MovieTitlesTable
|
||||
movieId={id}
|
||||
/>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel>
|
||||
<MovieCastPostersConnector
|
||||
isSmallScreen={isSmallScreen}
|
||||
/>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel>
|
||||
<MovieCrewPostersConnector
|
||||
isSmallScreen={isSmallScreen}
|
||||
/>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
<FieldSet legend={translate('Crew')}>
|
||||
<MovieCrewPostersConnector
|
||||
isSmallScreen={isSmallScreen}
|
||||
/>
|
||||
</FieldSet>
|
||||
|
||||
<FieldSet legend={translate('Titles')}>
|
||||
<MovieTitlesTable
|
||||
movieId={id}
|
||||
/>
|
||||
</FieldSet>
|
||||
</div>
|
||||
|
||||
<OrganizePreviewModalConnector
|
||||
@ -761,6 +728,12 @@ class MovieDetails extends Component {
|
||||
onDeleteMoviePress={this.onDeleteMoviePress}
|
||||
/>
|
||||
|
||||
<MovieHistoryModal
|
||||
isOpen={isMovieHistoryModalOpen}
|
||||
movieId={id}
|
||||
onModalClose={this.onMovieHistoryModalClose}
|
||||
/>
|
||||
|
||||
<DeleteMovieModal
|
||||
isOpen={isDeleteMovieModalOpen}
|
||||
movieId={id}
|
||||
|
@ -11,7 +11,6 @@ import { toggleMovieMonitored } from 'Store/Actions/movieActions';
|
||||
import { clearMovieBlocklist, fetchMovieBlocklist } from 'Store/Actions/movieBlocklistActions';
|
||||
import { clearMovieCredits, fetchMovieCredits } from 'Store/Actions/movieCreditsActions';
|
||||
import { clearMovieFiles, fetchMovieFiles } from 'Store/Actions/movieFileActions';
|
||||
import { clearMovieHistory, fetchMovieHistory } from 'Store/Actions/movieHistoryActions';
|
||||
import { clearQueueDetails, fetchQueueDetails } from 'Store/Actions/queueActions';
|
||||
import { cancelFetchReleases, clearReleases } from 'Store/Actions/releaseActions';
|
||||
import { fetchImportListSchema } from 'Store/Actions/settingsActions';
|
||||
@ -182,12 +181,6 @@ function createMapDispatchToProps(dispatch, props) {
|
||||
dispatchClearMovieFiles() {
|
||||
dispatch(clearMovieFiles());
|
||||
},
|
||||
dispatchFetchMovieHistory({ movieId }) {
|
||||
dispatch(fetchMovieHistory({ movieId }));
|
||||
},
|
||||
dispatchClearMovieHistory() {
|
||||
dispatch(clearMovieHistory());
|
||||
},
|
||||
dispatchFetchMovieCredits({ movieId }) {
|
||||
dispatch(fetchMovieCredits({ movieId }));
|
||||
},
|
||||
@ -283,7 +276,6 @@ class MovieDetailsConnector extends Component {
|
||||
|
||||
this.props.dispatchFetchMovieFiles({ movieId });
|
||||
this.props.dispatchFetchMovieBlocklist({ movieId });
|
||||
this.props.dispatchFetchMovieHistory({ movieId });
|
||||
this.props.dispatchFetchExtraFiles({ movieId });
|
||||
this.props.dispatchFetchMovieCredits({ movieId });
|
||||
this.props.dispatchFetchQueueDetails({ movieId });
|
||||
@ -294,7 +286,6 @@ class MovieDetailsConnector extends Component {
|
||||
this.props.dispatchCancelFetchReleases();
|
||||
this.props.dispatchClearMovieBlocklist();
|
||||
this.props.dispatchClearMovieFiles();
|
||||
this.props.dispatchClearMovieHistory();
|
||||
this.props.dispatchClearExtraFiles();
|
||||
this.props.dispatchClearMovieCredits();
|
||||
this.props.dispatchClearQueueDetails();
|
||||
@ -351,8 +342,6 @@ MovieDetailsConnector.propTypes = {
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
dispatchFetchMovieFiles: PropTypes.func.isRequired,
|
||||
dispatchClearMovieFiles: PropTypes.func.isRequired,
|
||||
dispatchFetchMovieHistory: PropTypes.func.isRequired,
|
||||
dispatchClearMovieHistory: PropTypes.func.isRequired,
|
||||
dispatchFetchExtraFiles: PropTypes.func.isRequired,
|
||||
dispatchClearExtraFiles: PropTypes.func.isRequired,
|
||||
dispatchFetchMovieCredits: PropTypes.func.isRequired,
|
||||
|
@ -1,5 +1,4 @@
|
||||
.container {
|
||||
margin-top: 20px;
|
||||
border: 1px solid var(--borderColor);
|
||||
border-radius: 4px;
|
||||
background-color: var(--inputBackgroundColor);
|
@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import MovieTitlesTableContentConnector from './MovieTitlesTableContentConnector';
|
||||
import styles from './MovieTitlesTable.css';
|
||||
|
||||
function MovieTitlesTable(props) {
|
||||
const {
|
||||
@ -7,9 +8,11 @@ function MovieTitlesTable(props) {
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<MovieTitlesTableContentConnector
|
||||
{...otherProps}
|
||||
/>
|
||||
<div className={styles.container}>
|
||||
<MovieTitlesTableContentConnector
|
||||
{...otherProps}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
33
frontend/src/Movie/History/MovieHistoryModal.js
Normal file
33
frontend/src/Movie/History/MovieHistoryModal.js
Normal file
@ -0,0 +1,33 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import MovieHistoryModalContentConnector from './MovieHistoryModalContentConnector';
|
||||
|
||||
function MovieHistoryModal(props) {
|
||||
const {
|
||||
isOpen,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
size={sizes.EXTRA_LARGE}
|
||||
>
|
||||
<MovieHistoryModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
MovieHistoryModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default MovieHistoryModal;
|
142
frontend/src/Movie/History/MovieHistoryModalContent.js
Normal file
142
frontend/src/Movie/History/MovieHistoryModalContent.js
Normal file
@ -0,0 +1,142 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import Icon from 'Components/Icon';
|
||||
import Button from 'Components/Link/Button';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import MovieHistoryRowConnector from './MovieHistoryRowConnector';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
name: 'eventType',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'sourceTitle',
|
||||
label: () => translate('SourceTitle'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'languages',
|
||||
label: () => translate('Languages'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'quality',
|
||||
label: () => translate('Quality'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'customFormats',
|
||||
label: () => translate('CustomFormats'),
|
||||
isSortable: false,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'customFormatScore',
|
||||
label: React.createElement(Icon, {
|
||||
name: icons.SCORE,
|
||||
title: 'Custom format score'
|
||||
}),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'date',
|
||||
label: () => translate('Date'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'actions',
|
||||
label: () => translate('Actions'),
|
||||
isVisible: true
|
||||
}
|
||||
];
|
||||
|
||||
class MovieHistoryModalContent extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items,
|
||||
onMarkAsFailedPress,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
const hasItems = !!items.length;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{translate('History')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
{
|
||||
isFetching &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<Alert kind={kinds.DANGER}>{translate('HistoryLoadError')}</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && !hasItems && !error &&
|
||||
<div>{translate('NoHistory')}</div>
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && hasItems && !error &&
|
||||
<Table columns={columns}>
|
||||
<TableBody>
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<MovieHistoryRowConnector
|
||||
key={item.id}
|
||||
{...item}
|
||||
onMarkAsFailedPress={onMarkAsFailedPress}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>
|
||||
{translate('Close')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MovieHistoryModalContent.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onMarkAsFailedPress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default MovieHistoryModalContent;
|
@ -2,8 +2,8 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { movieHistoryMarkAsFailed } from 'Store/Actions/movieHistoryActions';
|
||||
import MovieHistoryTableContent from './MovieHistoryTableContent';
|
||||
import { clearMovieHistory, fetchMovieHistory, movieHistoryMarkAsFailed } from 'Store/Actions/movieHistoryActions';
|
||||
import MovieHistoryModalContent from './MovieHistoryModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
@ -15,10 +15,29 @@ function createMapStateToProps() {
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchMovieHistory,
|
||||
clearMovieHistory,
|
||||
movieHistoryMarkAsFailed
|
||||
};
|
||||
|
||||
class MovieHistoryTableContentConnector extends Component {
|
||||
class MovieHistoryModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
movieId
|
||||
} = this.props;
|
||||
|
||||
this.props.fetchMovieHistory({
|
||||
movieId
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.clearMovieHistory();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
@ -39,7 +58,7 @@ class MovieHistoryTableContentConnector extends Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<MovieHistoryTableContent
|
||||
<MovieHistoryModalContent
|
||||
{...this.props}
|
||||
onMarkAsFailedPress={this.onMarkAsFailedPress}
|
||||
/>
|
||||
@ -47,9 +66,11 @@ class MovieHistoryTableContentConnector extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
MovieHistoryTableContentConnector.propTypes = {
|
||||
MovieHistoryModalContentConnector.propTypes = {
|
||||
movieId: PropTypes.number.isRequired,
|
||||
fetchMovieHistory: PropTypes.func.isRequired,
|
||||
clearMovieHistory: PropTypes.func.isRequired,
|
||||
movieHistoryMarkAsFailed: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(MovieHistoryTableContentConnector);
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(MovieHistoryModalContentConnector);
|
@ -1,22 +0,0 @@
|
||||
import React from 'react';
|
||||
import MovieHistoryTableContentConnector from './MovieHistoryTableContentConnector';
|
||||
import styles from './MovieHistoryTable.css';
|
||||
|
||||
function MovieHistoryTable(props) {
|
||||
const {
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<MovieHistoryTableContentConnector
|
||||
{...otherProps}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
MovieHistoryTable.propTypes = {
|
||||
};
|
||||
|
||||
export default MovieHistoryTable;
|
@ -1,128 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import MovieHistoryRowConnector from './MovieHistoryRowConnector';
|
||||
import styles from './MovieHistoryTableContent.css';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
name: 'eventType',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'sourceTitle',
|
||||
label: () => translate('SourceTitle'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'languages',
|
||||
label: () => translate('Languages'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'quality',
|
||||
label: () => translate('Quality'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'customFormats',
|
||||
label: () => translate('CustomFormats'),
|
||||
isSortable: false,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'customFormatScore',
|
||||
label: React.createElement(Icon, {
|
||||
name: icons.SCORE,
|
||||
title: 'Custom format score'
|
||||
}),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'date',
|
||||
label: () => translate('Date'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'actions',
|
||||
label: React.createElement(IconButton, { name: icons.ADVANCED_SETTINGS }),
|
||||
isVisible: true
|
||||
}
|
||||
];
|
||||
|
||||
class MovieHistoryTableContent extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items,
|
||||
onMarkAsFailedPress
|
||||
} = this.props;
|
||||
|
||||
const hasItems = !!items.length;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{
|
||||
isFetching &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<div className={styles.blankpad}>
|
||||
{translate('UnableToLoadHistory')}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && !hasItems && !error &&
|
||||
<div className={styles.blankpad}>
|
||||
{translate('NoHistory')}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && hasItems && !error &&
|
||||
<Table columns={columns}>
|
||||
<TableBody>
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<MovieHistoryRowConnector
|
||||
key={item.id}
|
||||
{...item}
|
||||
onMarkAsFailedPress={onMarkAsFailedPress}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MovieHistoryTableContent.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onMarkAsFailedPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default MovieHistoryTableContent;
|
@ -16,7 +16,7 @@ function MovieInteractiveSearchModal(props) {
|
||||
isOpen={isOpen}
|
||||
closeOnBackgroundClick={false}
|
||||
onModalClose={onModalClose}
|
||||
size={sizes.EXTRA_LARGE}
|
||||
size={sizes.EXTRA_EXTRA_LARGE}
|
||||
>
|
||||
<MovieInteractiveSearchModalContent
|
||||
movieId={movieId}
|
||||
|
@ -1,5 +1,4 @@
|
||||
.container {
|
||||
margin-top: 20px;
|
||||
border: 1px solid var(--borderColor);
|
||||
border-radius: 4px;
|
||||
background-color: var(--inputBackgroundColor);
|
||||
|
@ -1,33 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
function createMovieCollectionListSelector() {
|
||||
return createSelector(
|
||||
(state, { tmdbId }) => tmdbId,
|
||||
(state) => state.settings.importLists.items,
|
||||
(tmdbId, importLists) => {
|
||||
const importListIds = _.reduce(importLists, (acc, list) => {
|
||||
if (list.implementation === 'TMDbCollectionImport') {
|
||||
const collectionIdField = list.fields.find((field) => {
|
||||
return field.name === 'collectionId';
|
||||
});
|
||||
|
||||
if (collectionIdField && parseInt(collectionIdField.value) === tmdbId) {
|
||||
acc.push(list);
|
||||
return acc;
|
||||
}
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
if (importListIds.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return importListIds[0];
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default createMovieCollectionListSelector;
|
@ -21,15 +21,11 @@ function createMovieCreditListSelector() {
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
let importListId = 0;
|
||||
|
||||
if (importListIds.length > 0) {
|
||||
importListId = importListIds[0].id;
|
||||
if (importListIds.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
importListId
|
||||
};
|
||||
return importListIds[0];
|
||||
}
|
||||
);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user