mirror of
https://github.com/Radarr/Radarr
synced 2026-05-08 19:50:25 +02:00
New: Rework Movie Details view
This commit is contained in:
parent
757cb9a956
commit
1d4db26f17
37 changed files with 494 additions and 434 deletions
|
|
@ -2,12 +2,15 @@ import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||||
|
import PageMenuButton from 'Components/Menu/PageMenuButton';
|
||||||
import Table from 'Components/Table/Table';
|
import Table from 'Components/Table/Table';
|
||||||
import TableBody from 'Components/Table/TableBody';
|
import TableBody from 'Components/Table/TableBody';
|
||||||
import { icons, sortDirections } from 'Helpers/Props';
|
import { align, icons, sortDirections } from 'Helpers/Props';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
|
import InteractiveSearchFilterModalConnector from './InteractiveSearchFilterModalConnector';
|
||||||
import InteractiveSearchRowConnector from './InteractiveSearchRowConnector';
|
import InteractiveSearchRowConnector from './InteractiveSearchRowConnector';
|
||||||
import styles from './InteractiveSearchContent.css';
|
import styles from './InteractiveSearch.css';
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
|
|
@ -22,20 +25,6 @@ const columns = [
|
||||||
isSortable: true,
|
isSortable: true,
|
||||||
isVisible: true
|
isVisible: true
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'releaseWeight',
|
|
||||||
label: React.createElement(Icon, { name: icons.DOWNLOAD }),
|
|
||||||
isSortable: true,
|
|
||||||
fixedSortDirection: sortDirections.ASCENDING,
|
|
||||||
isVisible: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'rejections',
|
|
||||||
label: React.createElement(Icon, { name: icons.DANGER }),
|
|
||||||
isSortable: true,
|
|
||||||
fixedSortDirection: sortDirections.ASCENDING,
|
|
||||||
isVisible: true
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: 'title',
|
name: 'title',
|
||||||
label: translate('Title'),
|
label: translate('Title'),
|
||||||
|
|
@ -99,10 +88,24 @@ const columns = [
|
||||||
label: React.createElement(Icon, { name: icons.FLAG }),
|
label: React.createElement(Icon, { name: icons.FLAG }),
|
||||||
isSortable: true,
|
isSortable: true,
|
||||||
isVisible: true
|
isVisible: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'rejections',
|
||||||
|
label: React.createElement(Icon, { name: icons.DANGER }),
|
||||||
|
isSortable: true,
|
||||||
|
fixedSortDirection: sortDirections.ASCENDING,
|
||||||
|
isVisible: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'releaseWeight',
|
||||||
|
label: React.createElement(Icon, { name: icons.DOWNLOAD }),
|
||||||
|
isSortable: true,
|
||||||
|
fixedSortDirection: sortDirections.ASCENDING,
|
||||||
|
isVisible: true
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
function InteractiveSearchContent(props) {
|
function InteractiveSearch(props) {
|
||||||
const {
|
const {
|
||||||
searchPayload,
|
searchPayload,
|
||||||
isFetching,
|
isFetching,
|
||||||
|
|
@ -110,44 +113,63 @@ function InteractiveSearchContent(props) {
|
||||||
error,
|
error,
|
||||||
totalReleasesCount,
|
totalReleasesCount,
|
||||||
items,
|
items,
|
||||||
|
selectedFilterKey,
|
||||||
|
filters,
|
||||||
|
customFilters,
|
||||||
sortKey,
|
sortKey,
|
||||||
sortDirection,
|
sortDirection,
|
||||||
longDateFormat,
|
longDateFormat,
|
||||||
timeFormat,
|
timeFormat,
|
||||||
onSortPress,
|
onSortPress,
|
||||||
|
onFilterSelect,
|
||||||
onGrabPress
|
onGrabPress
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
<div className={styles.filterMenuContainer}>
|
||||||
|
<FilterMenu
|
||||||
|
alignMenu={align.RIGHT}
|
||||||
|
selectedFilterKey={selectedFilterKey}
|
||||||
|
filters={filters}
|
||||||
|
customFilters={customFilters}
|
||||||
|
buttonComponent={PageMenuButton}
|
||||||
|
filterModalConnectorComponent={InteractiveSearchFilterModalConnector}
|
||||||
|
filterModalConnectorComponentProps={'movies'}
|
||||||
|
onFilterSelect={onFilterSelect}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{
|
{
|
||||||
isFetching &&
|
isFetching ? <LoadingIndicator /> : null
|
||||||
<LoadingIndicator />
|
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
!isFetching && !!error &&
|
!isFetching && error ?
|
||||||
<div className={styles.blankpad}>
|
<div>
|
||||||
{translate('UnableToLoadResultsIntSearch')}
|
{translate('UnableToLoadResultsIntSearch')}
|
||||||
</div>
|
</div> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
!isFetching && isPopulated && !totalReleasesCount &&
|
!isFetching && isPopulated && !totalReleasesCount ?
|
||||||
<div className={styles.blankpad}>
|
<div>
|
||||||
{translate('NoResultsFound')}
|
{translate('NoResultsFound')}
|
||||||
</div>
|
</div> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
!!totalReleasesCount && isPopulated && !items.length &&
|
!!totalReleasesCount && isPopulated && !items.length ?
|
||||||
<div className={styles.blankpad}>
|
<div>
|
||||||
{translate('AllResultsHiddenFilter')}
|
{translate('AllResultsHiddenFilter')}
|
||||||
</div>
|
</div> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
isPopulated && !!items.length &&
|
isPopulated && !!items.length ?
|
||||||
<Table
|
<Table
|
||||||
columns={columns}
|
columns={columns}
|
||||||
sortKey={sortKey}
|
sortKey={sortKey}
|
||||||
|
|
@ -170,32 +192,38 @@ function InteractiveSearchContent(props) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
totalReleasesCount !== items.length && !!items.length &&
|
totalReleasesCount !== items.length && !!items.length ?
|
||||||
<div className={styles.filteredMessage}>
|
<div className={styles.filteredMessage}>
|
||||||
{translate('SomeResultsHiddenFilter')}
|
{translate('SomeResultsHiddenFilter')}
|
||||||
</div>
|
</div> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
InteractiveSearchContent.propTypes = {
|
InteractiveSearch.propTypes = {
|
||||||
searchPayload: PropTypes.object.isRequired,
|
searchPayload: PropTypes.object.isRequired,
|
||||||
isFetching: PropTypes.bool.isRequired,
|
isFetching: PropTypes.bool.isRequired,
|
||||||
isPopulated: PropTypes.bool.isRequired,
|
isPopulated: PropTypes.bool.isRequired,
|
||||||
error: PropTypes.object,
|
error: PropTypes.object,
|
||||||
totalReleasesCount: PropTypes.number.isRequired,
|
totalReleasesCount: PropTypes.number.isRequired,
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||||
|
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
sortKey: PropTypes.string,
|
sortKey: PropTypes.string,
|
||||||
sortDirection: PropTypes.string,
|
sortDirection: PropTypes.string,
|
||||||
longDateFormat: PropTypes.string.isRequired,
|
longDateFormat: PropTypes.string.isRequired,
|
||||||
timeFormat: PropTypes.string.isRequired,
|
timeFormat: PropTypes.string.isRequired,
|
||||||
onSortPress: PropTypes.func.isRequired,
|
onSortPress: PropTypes.func.isRequired,
|
||||||
|
onFilterSelect: PropTypes.func.isRequired,
|
||||||
onGrabPress: PropTypes.func.isRequired
|
onGrabPress: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default InteractiveSearchContent;
|
export default InteractiveSearch;
|
||||||
|
|
@ -5,7 +5,7 @@ import { createSelector } from 'reselect';
|
||||||
import * as releaseActions from 'Store/Actions/releaseActions';
|
import * as releaseActions from 'Store/Actions/releaseActions';
|
||||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||||
import InteractiveSearchContent from './InteractiveSearchContent';
|
import InteractiveSearch from './InteractiveSearch';
|
||||||
|
|
||||||
function createMapStateToProps(appState) {
|
function createMapStateToProps(appState) {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
|
|
@ -48,7 +48,7 @@ function createMapDispatchToProps(dispatch, props) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
class InteractiveSearchContentConnector extends Component {
|
class InteractiveSearchConnector extends Component {
|
||||||
|
|
||||||
//
|
//
|
||||||
// Lifecycle
|
// Lifecycle
|
||||||
|
|
@ -79,18 +79,18 @@ class InteractiveSearchContentConnector extends Component {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
||||||
<InteractiveSearchContent
|
<InteractiveSearch
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
InteractiveSearchContentConnector.propTypes = {
|
InteractiveSearchConnector.propTypes = {
|
||||||
searchPayload: PropTypes.object.isRequired,
|
searchPayload: PropTypes.object.isRequired,
|
||||||
isPopulated: PropTypes.bool.isRequired,
|
isPopulated: PropTypes.bool.isRequired,
|
||||||
dispatchFetchReleases: PropTypes.func.isRequired,
|
dispatchFetchReleases: PropTypes.func.isRequired,
|
||||||
dispatchClearReleases: PropTypes.func.isRequired
|
dispatchClearReleases: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(createMapStateToProps, createMapDispatchToProps)(InteractiveSearchContentConnector);
|
export default connect(createMapStateToProps, createMapDispatchToProps)(InteractiveSearchConnector);
|
||||||
|
|
@ -1,15 +1,20 @@
|
||||||
.cell {
|
|
||||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
|
||||||
}
|
|
||||||
|
|
||||||
.protocol {
|
.protocol {
|
||||||
composes: cell;
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
width: 80px;
|
width: 80px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
.indexer {
|
.indexer {
|
||||||
composes: cell;
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
width: 85px;
|
width: 85px;
|
||||||
}
|
}
|
||||||
|
|
@ -17,7 +22,9 @@
|
||||||
.quality,
|
.quality,
|
||||||
.customFormat,
|
.customFormat,
|
||||||
.language {
|
.language {
|
||||||
composes: cell;
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.language {
|
.language {
|
||||||
|
|
@ -25,7 +32,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.customFormatScore {
|
.customFormatScore {
|
||||||
composes: cell;
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
width: 55px;
|
width: 55px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
|
@ -35,34 +42,26 @@
|
||||||
.rejected,
|
.rejected,
|
||||||
.indexerFlags,
|
.indexerFlags,
|
||||||
.download {
|
.download {
|
||||||
composes: cell;
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
width: 50px;
|
width: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.age,
|
.age,
|
||||||
.size {
|
.size {
|
||||||
composes: cell;
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.peers {
|
.peers {
|
||||||
composes: cell;
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
width: 75px;
|
width: 75px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
|
||||||
composes: cell;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title div {
|
|
||||||
overflow-wrap: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.history {
|
.history {
|
||||||
composes: cell;
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
width: 75px;
|
width: 75px;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -145,46 +145,6 @@ class InteractiveSearchRow extends Component {
|
||||||
{formatAge(age, ageHours, ageMinutes)}
|
{formatAge(age, ageHours, ageMinutes)}
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
|
||||||
<TableRowCell className={styles.download}>
|
|
||||||
<SpinnerIconButton
|
|
||||||
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
|
|
||||||
kind={grabError ? kinds.DANGER : kinds.DEFAULT}
|
|
||||||
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
|
|
||||||
isDisabled={isGrabbed}
|
|
||||||
isSpinning={isGrabbing}
|
|
||||||
onPress={downloadAllowed ? this.onGrabPress : this.onConfirmGrabPress}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
|
|
||||||
<TableRowCell className={styles.rejected}>
|
|
||||||
{
|
|
||||||
!!rejections.length &&
|
|
||||||
<Popover
|
|
||||||
anchor={
|
|
||||||
<Icon
|
|
||||||
name={icons.DANGER}
|
|
||||||
kind={kinds.DANGER}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
title={translate('ReleaseRejected')}
|
|
||||||
body={
|
|
||||||
<ul>
|
|
||||||
{
|
|
||||||
rejections.map((rejection, index) => {
|
|
||||||
return (
|
|
||||||
<li key={index}>
|
|
||||||
{rejection}
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
}
|
|
||||||
position={tooltipPositions.BOTTOM}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</TableRowCell>
|
|
||||||
|
|
||||||
<TableRowCell className={styles.title}>
|
<TableRowCell className={styles.title}>
|
||||||
<Link
|
<Link
|
||||||
to={infoUrl}
|
to={infoUrl}
|
||||||
|
|
@ -297,6 +257,46 @@ class InteractiveSearchRow extends Component {
|
||||||
}
|
}
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
|
||||||
|
<TableRowCell className={styles.rejected}>
|
||||||
|
{
|
||||||
|
!!rejections.length &&
|
||||||
|
<Popover
|
||||||
|
anchor={
|
||||||
|
<Icon
|
||||||
|
name={icons.DANGER}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
title={translate('ReleaseRejected')}
|
||||||
|
body={
|
||||||
|
<ul>
|
||||||
|
{
|
||||||
|
rejections.map((rejection, index) => {
|
||||||
|
return (
|
||||||
|
<li key={index}>
|
||||||
|
{rejection}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
}
|
||||||
|
position={tooltipPositions.LEFT}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
|
<TableRowCell className={styles.download}>
|
||||||
|
<SpinnerIconButton
|
||||||
|
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
|
||||||
|
kind={grabError ? kinds.DANGER : kinds.DEFAULT}
|
||||||
|
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
|
||||||
|
isDisabled={isGrabbed}
|
||||||
|
isSpinning={isGrabbing}
|
||||||
|
onPress={downloadAllowed ? this.onGrabPress : this.onConfirmGrabPress}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
isOpen={this.state.isConfirmGrabModalOpen}
|
isOpen={this.state.isConfirmGrabModalOpen}
|
||||||
kind={kinds.WARNING}
|
kind={kinds.WARNING}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import InteractiveSearchContentConnector from './InteractiveSearchContentConnector';
|
|
||||||
|
|
||||||
function InteractiveSearchTable(props) {
|
|
||||||
|
|
||||||
return (
|
|
||||||
<InteractiveSearchContentConnector
|
|
||||||
searchPayload={props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
InteractiveSearchTable.propTypes = {
|
|
||||||
};
|
|
||||||
|
|
||||||
export default InteractiveSearchTable;
|
|
||||||
|
|
@ -69,7 +69,8 @@ class MovieCastPoster extends Component {
|
||||||
|
|
||||||
const elementStyle = {
|
const elementStyle = {
|
||||||
width: `${posterWidth}px`,
|
width: `${posterWidth}px`,
|
||||||
height: `${posterHeight}px`
|
height: `${posterHeight}px`,
|
||||||
|
borderRadius: '5px'
|
||||||
};
|
};
|
||||||
|
|
||||||
const contentStyle = {
|
const contentStyle = {
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,8 @@ class MovieCrewPoster extends Component {
|
||||||
|
|
||||||
const elementStyle = {
|
const elementStyle = {
|
||||||
width: `${posterWidth}px`,
|
width: `${posterWidth}px`,
|
||||||
height: `${posterHeight}px`
|
height: `${posterHeight}px`,
|
||||||
|
borderRadius: '5px'
|
||||||
};
|
};
|
||||||
|
|
||||||
const contentStyle = {
|
const contentStyle = {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
$hoverScale: 1.05;
|
$hoverScale: 1.05;
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
|
border-radius: 5px;
|
||||||
transition: all 200ms ease-in;
|
transition: all 200ms ease-in;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,10 @@
|
||||||
flex: 1 0 auto;
|
flex: 1 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.movie {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,16 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { Grid, WindowScroller } from 'react-virtualized';
|
import { Navigation } from 'swiper';
|
||||||
import Measure from 'Components/Measure';
|
import { Swiper, SwiperSlide } from 'swiper/react';
|
||||||
import dimensions from 'Styles/Variables/dimensions';
|
import dimensions from 'Styles/Variables/dimensions';
|
||||||
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
|
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
|
||||||
import MovieCreditPosterConnector from './MovieCreditPosterConnector';
|
import MovieCreditPosterConnector from './MovieCreditPosterConnector';
|
||||||
import styles from './MovieCreditPosters.css';
|
import styles from './MovieCreditPosters.css';
|
||||||
|
|
||||||
|
// Import Swiper styles
|
||||||
|
import 'swiper/css';
|
||||||
|
import 'swiper/css/navigation';
|
||||||
|
|
||||||
// Poster container dimensions
|
// Poster container dimensions
|
||||||
const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
|
const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
|
||||||
const columnPaddingSmallScreen = parseInt(dimensions.movieIndexColumnPaddingSmallScreen);
|
const columnPaddingSmallScreen = parseInt(dimensions.movieIndexColumnPaddingSmallScreen);
|
||||||
|
|
@ -169,56 +173,50 @@ class MovieCreditPosters extends Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
items
|
items,
|
||||||
|
itemComponent
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
width,
|
posterWidth,
|
||||||
columnWidth,
|
posterHeight
|
||||||
columnCount,
|
|
||||||
rowHeight
|
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
const rowCount = Math.ceil(items.length / columnCount);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Measure
|
|
||||||
whitelist={['width']}
|
|
||||||
onMeasure={this.onMeasure}
|
|
||||||
>
|
|
||||||
<WindowScroller
|
|
||||||
scrollElement={undefined}
|
|
||||||
>
|
|
||||||
{({ height, registerChild, onChildScroll, scrollTop }) => {
|
|
||||||
if (!height) {
|
|
||||||
return <div />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
<div className={styles.sliderContainer}>
|
||||||
<div ref={registerChild}>
|
<Swiper
|
||||||
<Grid
|
slidesPerView='auto'
|
||||||
ref={this.setGridRef}
|
spaceBetween={10}
|
||||||
className={styles.grid}
|
slidesPerGroup={3}
|
||||||
autoHeight={true}
|
loop={false}
|
||||||
height={height}
|
loopFillGroupWithBlank={true}
|
||||||
columnCount={columnCount}
|
className="mySwiper"
|
||||||
columnWidth={columnWidth}
|
modules={[Navigation]}
|
||||||
rowCount={rowCount}
|
onInit={(swiper) => {
|
||||||
rowHeight={rowHeight}
|
swiper.params.navigation.prevEl = this._swiperPrevRef;
|
||||||
width={width}
|
swiper.params.navigation.nextEl = this._swiperNextRef;
|
||||||
onScroll={onChildScroll}
|
swiper.navigation.init();
|
||||||
scrollTop={scrollTop}
|
swiper.navigation.update();
|
||||||
overscanRowCount={2}
|
}}
|
||||||
cellRenderer={this.cellRenderer}
|
>
|
||||||
scrollToAlignment={'start'}
|
{items.map((credit) => (
|
||||||
isScrollingOptOut={true}
|
<SwiperSlide key={credit.tmdbId} style={{ width: posterWidth }}>
|
||||||
/>
|
<MovieCreditPosterConnector
|
||||||
</div>
|
key={credit.order}
|
||||||
);
|
component={itemComponent}
|
||||||
}
|
posterWidth={posterWidth}
|
||||||
}
|
posterHeight={posterHeight}
|
||||||
</WindowScroller>
|
tmdbId={credit.personTmdbId}
|
||||||
</Measure>
|
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 {
|
.header {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 375px;
|
height: 425px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.errorMessage {
|
.errorMessage {
|
||||||
|
|
@ -39,10 +39,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.poster {
|
.poster {
|
||||||
|
z-index: 2;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
margin-right: 35px;
|
margin-right: 35px;
|
||||||
width: 217px;
|
width: 250px;
|
||||||
height: 319px;
|
height: 368px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info {
|
.info {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { Tab, TabList, TabPanel, Tabs } from 'react-tabs';
|
|
||||||
import TextTruncate from 'react-text-truncate';
|
import TextTruncate from 'react-text-truncate';
|
||||||
|
import FieldSet from 'Components/FieldSet';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import ImdbRating from 'Components/ImdbRating';
|
import ImdbRating from 'Components/ImdbRating';
|
||||||
import InfoLabel from 'Components/InfoLabel';
|
import InfoLabel from 'Components/InfoLabel';
|
||||||
|
|
@ -22,12 +22,11 @@ import Popover from 'Components/Tooltip/Popover';
|
||||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||||
import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
|
import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
|
||||||
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
||||||
import InteractiveSearchFilterMenuConnector from 'InteractiveSearch/InteractiveSearchFilterMenuConnector';
|
|
||||||
import InteractiveSearchTable from 'InteractiveSearch/InteractiveSearchTable';
|
|
||||||
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
|
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
|
||||||
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
|
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
|
||||||
import MovieHistoryTable from 'Movie/History/MovieHistoryTable';
|
import MovieHistoryTable from 'Movie/History/MovieHistoryTable';
|
||||||
import MoviePoster from 'Movie/MoviePoster';
|
import MoviePoster from 'Movie/MoviePoster';
|
||||||
|
import MovieInteractiveSearchModalConnector from 'Movie/Search/MovieInteractiveSearchModalConnector';
|
||||||
import MovieFileEditorTable from 'MovieFile/Editor/MovieFileEditorTable';
|
import MovieFileEditorTable from 'MovieFile/Editor/MovieFileEditorTable';
|
||||||
import ExtraFileTable from 'MovieFile/Extras/ExtraFileTable';
|
import ExtraFileTable from 'MovieFile/Extras/ExtraFileTable';
|
||||||
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
|
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
|
||||||
|
|
@ -81,10 +80,10 @@ class MovieDetails extends Component {
|
||||||
isEditMovieModalOpen: false,
|
isEditMovieModalOpen: false,
|
||||||
isDeleteMovieModalOpen: false,
|
isDeleteMovieModalOpen: false,
|
||||||
isInteractiveImportModalOpen: false,
|
isInteractiveImportModalOpen: false,
|
||||||
|
isInteractiveSearchModalOpen: false,
|
||||||
allExpanded: false,
|
allExpanded: false,
|
||||||
allCollapsed: false,
|
allCollapsed: false,
|
||||||
expandedState: {},
|
expandedState: {},
|
||||||
selectedTabIndex: 0,
|
|
||||||
overviewHeight: 0,
|
overviewHeight: 0,
|
||||||
titleWidth: 0
|
titleWidth: 0
|
||||||
};
|
};
|
||||||
|
|
@ -137,6 +136,14 @@ class MovieDetails extends Component {
|
||||||
this.setState({ isEditMovieModalOpen: false });
|
this.setState({ isEditMovieModalOpen: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onInteractiveSearchPress = () => {
|
||||||
|
this.setState({ isInteractiveSearchModalOpen: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
onInteractiveSearchModalClose = () => {
|
||||||
|
this.setState({ isInteractiveSearchModalOpen: false });
|
||||||
|
};
|
||||||
|
|
||||||
onDeleteMoviePress = () => {
|
onDeleteMoviePress = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
isEditMovieModalOpen: false,
|
isEditMovieModalOpen: false,
|
||||||
|
|
@ -298,9 +305,9 @@ class MovieDetails extends Component {
|
||||||
isEditMovieModalOpen,
|
isEditMovieModalOpen,
|
||||||
isDeleteMovieModalOpen,
|
isDeleteMovieModalOpen,
|
||||||
isInteractiveImportModalOpen,
|
isInteractiveImportModalOpen,
|
||||||
|
isInteractiveSearchModalOpen,
|
||||||
overviewHeight,
|
overviewHeight,
|
||||||
titleWidth,
|
titleWidth
|
||||||
selectedTabIndex
|
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
const marqueeWidth = isSmallScreen ? titleWidth : (titleWidth - 150);
|
const marqueeWidth = isSmallScreen ? titleWidth : (titleWidth - 150);
|
||||||
|
|
@ -326,6 +333,14 @@ class MovieDetails extends Component {
|
||||||
onPress={onSearchPress}
|
onPress={onSearchPress}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<PageToolbarButton
|
||||||
|
label={translate('InteractiveSearch')}
|
||||||
|
iconName={icons.INTERACTIVE}
|
||||||
|
isSpinning={isSearching}
|
||||||
|
title={undefined}
|
||||||
|
onPress={this.onInteractiveSearchPress}
|
||||||
|
/>
|
||||||
|
|
||||||
<PageToolbarSeparator />
|
<PageToolbarSeparator />
|
||||||
|
|
||||||
<PageToolbarButton
|
<PageToolbarButton
|
||||||
|
|
@ -651,101 +666,39 @@ class MovieDetails extends Component {
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<Tabs selectedIndex={this.state.tabIndex} onSelect={this.onTabSelect}>
|
<FieldSet legend={translate('History')}>
|
||||||
<TabList
|
<MovieHistoryTable
|
||||||
className={styles.tabList}
|
movieId={id}
|
||||||
>
|
/>
|
||||||
<Tab
|
</FieldSet>
|
||||||
className={styles.tab}
|
|
||||||
selectedClassName={styles.selectedTab}
|
|
||||||
>
|
|
||||||
{translate('History')}
|
|
||||||
</Tab>
|
|
||||||
|
|
||||||
<Tab
|
<FieldSet legend={translate('Files')}>
|
||||||
className={styles.tab}
|
<MovieFileEditorTable
|
||||||
selectedClassName={styles.selectedTab}
|
movieId={id}
|
||||||
>
|
/>
|
||||||
{translate('Search')}
|
|
||||||
</Tab>
|
|
||||||
|
|
||||||
<Tab
|
<ExtraFileTable
|
||||||
className={styles.tab}
|
movieId={id}
|
||||||
selectedClassName={styles.selectedTab}
|
/>
|
||||||
>
|
</FieldSet>
|
||||||
{translate('Files')}
|
|
||||||
</Tab>
|
|
||||||
|
|
||||||
<Tab
|
<FieldSet legend={translate('Cast')}>
|
||||||
className={styles.tab}
|
<MovieCastPostersConnector
|
||||||
selectedClassName={styles.selectedTab}
|
isSmallScreen={isSmallScreen}
|
||||||
>
|
/>
|
||||||
{translate('Titles')}
|
</FieldSet>
|
||||||
</Tab>
|
|
||||||
|
|
||||||
<Tab
|
<FieldSet legend={translate('Crew')}>
|
||||||
className={styles.tab}
|
<MovieCrewPostersConnector
|
||||||
selectedClassName={styles.selectedTab}
|
isSmallScreen={isSmallScreen}
|
||||||
>
|
/>
|
||||||
{translate('Cast')}
|
</FieldSet>
|
||||||
</Tab>
|
|
||||||
|
|
||||||
<Tab
|
|
||||||
className={styles.tab}
|
|
||||||
selectedClassName={styles.selectedTab}
|
|
||||||
>
|
|
||||||
{translate('Crew')}
|
|
||||||
</Tab>
|
|
||||||
|
|
||||||
{
|
|
||||||
selectedTabIndex === 1 &&
|
|
||||||
<div className={styles.filterIcon}>
|
|
||||||
<InteractiveSearchFilterMenuConnector />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
</TabList>
|
|
||||||
|
|
||||||
<TabPanel>
|
|
||||||
<MovieHistoryTable
|
|
||||||
movieId={id}
|
|
||||||
/>
|
|
||||||
</TabPanel>
|
|
||||||
|
|
||||||
<TabPanel>
|
|
||||||
<InteractiveSearchTable
|
|
||||||
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('TitlesAndTranslations')}>
|
||||||
|
<MovieTitlesTable
|
||||||
|
movieId={id}
|
||||||
|
/>
|
||||||
|
</FieldSet>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<OrganizePreviewModalConnector
|
<OrganizePreviewModalConnector
|
||||||
|
|
@ -777,6 +730,12 @@ class MovieDetails extends Component {
|
||||||
showImportMode={false}
|
showImportMode={false}
|
||||||
onModalClose={this.onInteractiveImportModalClose}
|
onModalClose={this.onInteractiveImportModalClose}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<MovieInteractiveSearchModalConnector
|
||||||
|
isOpen={isInteractiveSearchModalOpen}
|
||||||
|
movieId={id}
|
||||||
|
onModalClose={this.onInteractiveSearchModalClose}
|
||||||
|
/>
|
||||||
</PageContentBody>
|
</PageContentBody>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ class MovieTitlesRow extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
MovieTitlesRow.propTypes = {
|
MovieTitlesRow.propTypes = {
|
||||||
id: PropTypes.number.isRequired,
|
id: PropTypes.string.isRequired,
|
||||||
title: PropTypes.string.isRequired,
|
title: PropTypes.string.isRequired,
|
||||||
language: PropTypes.object.isRequired,
|
language: PropTypes.object.isRequired,
|
||||||
sourceType: PropTypes.string.isRequired
|
sourceType: PropTypes.string.isRequired
|
||||||
|
|
|
||||||
9
frontend/src/Movie/Details/Titles/MovieTitlesTable.css
Normal file
9
frontend/src/Movie/Details/Titles/MovieTitlesTable.css
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
.container {
|
||||||
|
border: 1px solid var(--borderColor);
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: var(--inputBackgroundColor);
|
||||||
|
|
||||||
|
&:last-of-type {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import MovieTitlesTableContentConnector from './MovieTitlesTableContentConnector';
|
import MovieTitlesTableContentConnector from './MovieTitlesTableContentConnector';
|
||||||
|
import styles from './MovieTitlesTable.css';
|
||||||
|
|
||||||
function MovieTitlesTable(props) {
|
function MovieTitlesTable(props) {
|
||||||
const {
|
const {
|
||||||
|
|
@ -7,9 +8,11 @@ function MovieTitlesTable(props) {
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MovieTitlesTableContentConnector
|
<div className={styles.container}>
|
||||||
{...otherProps}
|
<MovieTitlesTableContentConnector
|
||||||
/>
|
{...otherProps}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
|
||||||
import Table from 'Components/Table/Table';
|
import Table from 'Components/Table/Table';
|
||||||
import TableBody from 'Components/Table/TableBody';
|
import TableBody from 'Components/Table/TableBody';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
|
|
@ -10,7 +9,7 @@ import styles from './MovieTitlesTableContent.css';
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
name: 'altTitle',
|
name: 'altTitle',
|
||||||
label: translate('AlternativeTitle'),
|
label: translate('Title'),
|
||||||
isVisible: true
|
isVisible: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -32,40 +31,25 @@ class MovieTitlesTableContent extends Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
isFetching,
|
titles
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
items
|
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const hasItems = !!items.length;
|
const hasItems = !!titles.length;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{
|
{
|
||||||
isFetching &&
|
!hasItems &&
|
||||||
<LoadingIndicator />
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!isFetching && !!error &&
|
|
||||||
<div className={styles.blankpad}>
|
|
||||||
{translate('UnableToLoadAltTitle')}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
isPopulated && !hasItems && !error &&
|
|
||||||
<div className={styles.blankpad}>
|
<div className={styles.blankpad}>
|
||||||
{translate('NoAltTitle')}
|
{translate('NoAltTitle')}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
isPopulated && hasItems && !error &&
|
hasItems &&
|
||||||
<Table columns={columns}>
|
<Table columns={columns}>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{
|
{
|
||||||
items.reverse().map((item) => {
|
titles.reverse().map((item) => {
|
||||||
return (
|
return (
|
||||||
<MovieTitlesRow
|
<MovieTitlesRow
|
||||||
key={item.id}
|
key={item.id}
|
||||||
|
|
@ -83,10 +67,7 @@ class MovieTitlesTableContent extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
MovieTitlesTableContent.propTypes = {
|
MovieTitlesTableContent.propTypes = {
|
||||||
isFetching: PropTypes.bool.isRequired,
|
titles: PropTypes.arrayOf(PropTypes.object).isRequired
|
||||||
isPopulated: PropTypes.bool.isRequired,
|
|
||||||
error: PropTypes.object,
|
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default MovieTitlesTableContent;
|
export default MovieTitlesTableContent;
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,40 @@ import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
|
import createMovieSelector from 'Store/Selectors/createMovieSelector';
|
||||||
import MovieTitlesTableContent from './MovieTitlesTableContent';
|
import MovieTitlesTableContent from './MovieTitlesTableContent';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state) => state.movies,
|
createMovieSelector(),
|
||||||
(movies) => {
|
(movie) => {
|
||||||
return movies;
|
let titles = [];
|
||||||
|
|
||||||
|
if (movie.alternateTitles) {
|
||||||
|
titles = movie.alternateTitles.map((title) => {
|
||||||
|
return {
|
||||||
|
id: `title_${title.id}`,
|
||||||
|
title: title.title,
|
||||||
|
language: title.language || 'Unknown',
|
||||||
|
sourceType: 'Alternative Title'
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (movie.translations) {
|
||||||
|
titles = titles.concat(movie.translations.map((title) => {
|
||||||
|
return {
|
||||||
|
id: `translation_${title.id}`,
|
||||||
|
title: title.title,
|
||||||
|
language: title.language || 'Unknown',
|
||||||
|
sourceType: 'Translation'
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
titles
|
||||||
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -23,14 +50,14 @@ class MovieTitlesTableContentConnector extends Component {
|
||||||
// Render
|
// Render
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const movie = this.props.items.filter((obj) => {
|
const {
|
||||||
return obj.id === this.props.movieId;
|
titles
|
||||||
});
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MovieTitlesTableContent
|
<MovieTitlesTableContent
|
||||||
{...this.props}
|
{...this.props}
|
||||||
items={movie[0].alternateTitles}
|
titles={titles}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -38,7 +65,7 @@ class MovieTitlesTableContentConnector extends Component {
|
||||||
|
|
||||||
MovieTitlesTableContentConnector.propTypes = {
|
MovieTitlesTableContentConnector.propTypes = {
|
||||||
movieId: PropTypes.number.isRequired,
|
movieId: PropTypes.number.isRequired,
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired
|
titles: PropTypes.arrayOf(PropTypes.object).isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(MovieTitlesTableContentConnector);
|
export default connect(createMapStateToProps, mapDispatchToProps)(MovieTitlesTableContentConnector);
|
||||||
|
|
|
||||||
9
frontend/src/Movie/History/MovieHistoryTable.css
Normal file
9
frontend/src/Movie/History/MovieHistoryTable.css
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
.container {
|
||||||
|
border: 1px solid var(--borderColor);
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: var(--inputBackgroundColor);
|
||||||
|
|
||||||
|
&:last-of-type {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import MovieHistoryTableContentConnector from './MovieHistoryTableContentConnector';
|
import MovieHistoryTableContentConnector from './MovieHistoryTableContentConnector';
|
||||||
|
import styles from './MovieHistoryTable.css';
|
||||||
|
|
||||||
function MovieHistoryTable(props) {
|
function MovieHistoryTable(props) {
|
||||||
const {
|
const {
|
||||||
|
|
@ -7,9 +8,11 @@ function MovieHistoryTable(props) {
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MovieHistoryTableContentConnector
|
<div className={styles.container}>
|
||||||
{...otherProps}
|
<MovieHistoryTableContentConnector
|
||||||
/>
|
{...otherProps}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
35
frontend/src/Movie/Search/MovieInteractiveSearchModal.js
Normal file
35
frontend/src/Movie/Search/MovieInteractiveSearchModal.js
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import { sizes } from 'Helpers/Props';
|
||||||
|
import MovieInteractiveSearchModalContent from './MovieInteractiveSearchModalContent';
|
||||||
|
|
||||||
|
function MovieInteractiveSearchModal(props) {
|
||||||
|
const {
|
||||||
|
isOpen,
|
||||||
|
movieId,
|
||||||
|
onModalClose
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
closeOnBackgroundClick={false}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
size={sizes.EXTRA_LARGE}
|
||||||
|
>
|
||||||
|
<MovieInteractiveSearchModalContent
|
||||||
|
movieId={movieId}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
MovieInteractiveSearchModal.propTypes = {
|
||||||
|
isOpen: PropTypes.bool.isRequired,
|
||||||
|
movieId: PropTypes.number.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MovieInteractiveSearchModal;
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { cancelFetchReleases, clearReleases } from 'Store/Actions/releaseActions';
|
||||||
|
import MovieInteractiveSearchModal from './MovieInteractiveSearchModal';
|
||||||
|
|
||||||
|
function createMapDispatchToProps(dispatch, props) {
|
||||||
|
return {
|
||||||
|
onModalClose() {
|
||||||
|
dispatch(cancelFetchReleases());
|
||||||
|
dispatch(clearReleases());
|
||||||
|
props.onModalClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(null, createMapDispatchToProps)(MovieInteractiveSearchModal);
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
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 { scrollDirections } from 'Helpers/Props';
|
||||||
|
import InteractiveSearchConnector from 'InteractiveSearch/InteractiveSearchConnector';
|
||||||
|
|
||||||
|
function MovieInteractiveSearchModalContent(props) {
|
||||||
|
const {
|
||||||
|
movieId,
|
||||||
|
onModalClose
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>
|
||||||
|
Interactive Search
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody scrollDirection={scrollDirections.BOTH}>
|
||||||
|
<InteractiveSearchConnector
|
||||||
|
searchPayload={{
|
||||||
|
movieId
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button onPress={onModalClose}>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
MovieInteractiveSearchModalContent.propTypes = {
|
||||||
|
movieId: PropTypes.number.isRequired,
|
||||||
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MovieInteractiveSearchModalContent;
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
.container {
|
.container {
|
||||||
margin-top: 20px;
|
|
||||||
border: 1px solid var(--borderColor);
|
border: 1px solid var(--borderColor);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background-color: var(--inputBackgroundColor);
|
background-color: var(--inputBackgroundColor);
|
||||||
|
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
using FizzWare.NBuilder;
|
|
||||||
using FluentAssertions;
|
|
||||||
using NUnit.Framework;
|
|
||||||
using NzbDrone.Core.Movies.AlternativeTitles;
|
|
||||||
using NzbDrone.Core.Test.Framework;
|
|
||||||
|
|
||||||
namespace NzbDrone.Core.Test.MovieTests.AlternativeTitleServiceTests
|
|
||||||
{
|
|
||||||
[TestFixture]
|
|
||||||
public class AlternativeTitleFixture : CoreTest
|
|
||||||
{
|
|
||||||
private AlternativeTitle CreateFakeTitle(SourceType source, int votes)
|
|
||||||
{
|
|
||||||
return Builder<AlternativeTitle>.CreateNew().With(t => t.SourceType = source).With(t => t.Votes = votes)
|
|
||||||
.Build();
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase(SourceType.TMDB, -1, true)]
|
|
||||||
[TestCase(SourceType.TMDB, 1000, true)]
|
|
||||||
[TestCase(SourceType.Mappings, 0, false)]
|
|
||||||
[TestCase(SourceType.Mappings, 4, true)]
|
|
||||||
[TestCase(SourceType.Mappings, -1, false)]
|
|
||||||
[TestCase(SourceType.Indexer, 0, true)]
|
|
||||||
[TestCase(SourceType.User, 0, true)]
|
|
||||||
public void should_be_trusted(SourceType source, int votes, bool trusted)
|
|
||||||
{
|
|
||||||
var fakeTitle = CreateFakeTitle(source, votes);
|
|
||||||
|
|
||||||
fakeTitle.IsTrusted().Should().Be(trusted);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
using FluentMigrator;
|
||||||
|
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Datastore.Migration
|
||||||
|
{
|
||||||
|
[Migration(216)]
|
||||||
|
public class clean_alt_titles : NzbDroneMigrationBase
|
||||||
|
{
|
||||||
|
protected override void MainDbUpgrade()
|
||||||
|
{
|
||||||
|
Delete.Column("SourceType").FromTable("AlternativeTitles");
|
||||||
|
Delete.Column("Votes").FromTable("AlternativeTitles");
|
||||||
|
Delete.Column("VoteCount").FromTable("AlternativeTitles");
|
||||||
|
Delete.Column("SourceId").FromTable("AlternativeTitles");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1028,6 +1028,7 @@
|
||||||
"Timeleft": "Time Left",
|
"Timeleft": "Time Left",
|
||||||
"Title": "Title",
|
"Title": "Title",
|
||||||
"Titles": "Titles",
|
"Titles": "Titles",
|
||||||
|
"TitlesAndTranslations": "Titles and Translations",
|
||||||
"TMDb": "TMDb",
|
"TMDb": "TMDb",
|
||||||
"TMDBId": "TMDb Id",
|
"TMDBId": "TMDb Id",
|
||||||
"TmdbIdHelpText": "The TMDb Id of the movie to exclude",
|
"TmdbIdHelpText": "The TMDb Id of the movie to exclude",
|
||||||
|
|
|
||||||
|
|
@ -570,7 +570,6 @@ private static AlternativeTitle MapAlternativeTitle(AlternativeTitleResource arg
|
||||||
var newAlternativeTitle = new AlternativeTitle
|
var newAlternativeTitle = new AlternativeTitle
|
||||||
{
|
{
|
||||||
Title = arg.Title,
|
Title = arg.Title,
|
||||||
SourceType = SourceType.TMDB,
|
|
||||||
CleanTitle = arg.Title.CleanMovieTitle(),
|
CleanTitle = arg.Title.CleanMovieTitle(),
|
||||||
Language = IsoLanguages.Find(arg.Language.ToLower())?.Language ?? Language.English
|
Language = IsoLanguages.Find(arg.Language.ToLower())?.Language ?? Language.English
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -6,39 +6,22 @@ namespace NzbDrone.Core.Movies.AlternativeTitles
|
||||||
{
|
{
|
||||||
public class AlternativeTitle : ModelBase
|
public class AlternativeTitle : ModelBase
|
||||||
{
|
{
|
||||||
public SourceType SourceType { get; set; }
|
|
||||||
public int MovieMetadataId { get; set; }
|
public int MovieMetadataId { get; set; }
|
||||||
public string Title { get; set; }
|
public string Title { get; set; }
|
||||||
public string CleanTitle { get; set; }
|
public string CleanTitle { get; set; }
|
||||||
public int SourceId { get; set; }
|
|
||||||
public int Votes { get; set; }
|
|
||||||
public int VoteCount { get; set; }
|
|
||||||
public Language Language { get; set; }
|
public Language Language { get; set; }
|
||||||
|
|
||||||
public AlternativeTitle()
|
public AlternativeTitle()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public AlternativeTitle(string title, SourceType sourceType = SourceType.TMDB, int sourceId = 0, Language language = null)
|
public AlternativeTitle(string title, int sourceId = 0, Language language = null)
|
||||||
{
|
{
|
||||||
Title = title;
|
Title = title;
|
||||||
CleanTitle = title.CleanMovieTitle();
|
CleanTitle = title.CleanMovieTitle();
|
||||||
SourceType = sourceType;
|
|
||||||
SourceId = sourceId;
|
|
||||||
Language = language ?? Language.English;
|
Language = language ?? Language.English;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsTrusted(int minVotes = 4)
|
|
||||||
{
|
|
||||||
switch (SourceType)
|
|
||||||
{
|
|
||||||
case SourceType.Mappings:
|
|
||||||
return Votes >= minVotes;
|
|
||||||
default:
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool Equals(object obj)
|
public override bool Equals(object obj)
|
||||||
{
|
{
|
||||||
var item = obj as AlternativeTitle;
|
var item = obj as AlternativeTitle;
|
||||||
|
|
@ -61,18 +44,4 @@ public override string ToString()
|
||||||
return Title;
|
return Title;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum SourceType
|
|
||||||
{
|
|
||||||
TMDB = 0,
|
|
||||||
Mappings = 1,
|
|
||||||
User = 2,
|
|
||||||
Indexer = 3
|
|
||||||
}
|
|
||||||
|
|
||||||
public class AlternativeYear
|
|
||||||
{
|
|
||||||
public int Year { get; set; }
|
|
||||||
public int SourceId { get; set; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,6 @@ namespace NzbDrone.Core.Movies.AlternativeTitles
|
||||||
{
|
{
|
||||||
public interface IAlternativeTitleRepository : IBasicRepository<AlternativeTitle>
|
public interface IAlternativeTitleRepository : IBasicRepository<AlternativeTitle>
|
||||||
{
|
{
|
||||||
AlternativeTitle FindBySourceId(int sourceId);
|
|
||||||
List<AlternativeTitle> FindBySourceIds(List<int> sourceIds);
|
|
||||||
List<AlternativeTitle> FindByMovieMetadataId(int movieId);
|
List<AlternativeTitle> FindByMovieMetadataId(int movieId);
|
||||||
void DeleteForMovies(List<int> movieIds);
|
void DeleteForMovies(List<int> movieIds);
|
||||||
}
|
}
|
||||||
|
|
@ -20,16 +18,6 @@ public AlternativeTitleRepository(IMainDatabase database, IEventAggregator event
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public AlternativeTitle FindBySourceId(int sourceId)
|
|
||||||
{
|
|
||||||
return Query(x => x.SourceId == sourceId).FirstOrDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<AlternativeTitle> FindBySourceIds(List<int> sourceIds)
|
|
||||||
{
|
|
||||||
return Query(x => sourceIds.Contains(x.SourceId));
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<AlternativeTitle> FindByMovieMetadataId(int movieId)
|
public List<AlternativeTitle> FindByMovieMetadataId(int movieId)
|
||||||
{
|
{
|
||||||
return Query(x => x.MovieMetadataId == movieId);
|
return Query(x => x.MovieMetadataId == movieId);
|
||||||
|
|
|
||||||
|
|
@ -35,13 +35,16 @@ public interface IMovieRepository : IBasicRepository<Movie>
|
||||||
public class MovieRepository : BasicRepository<Movie>, IMovieRepository
|
public class MovieRepository : BasicRepository<Movie>, IMovieRepository
|
||||||
{
|
{
|
||||||
private readonly IAlternativeTitleRepository _alternativeTitleRepository;
|
private readonly IAlternativeTitleRepository _alternativeTitleRepository;
|
||||||
|
private readonly IMovieTranslationRepository _movieTranslationRepository;
|
||||||
|
|
||||||
public MovieRepository(IMainDatabase database,
|
public MovieRepository(IMainDatabase database,
|
||||||
IAlternativeTitleRepository alternativeTitleRepository,
|
IAlternativeTitleRepository alternativeTitleRepository,
|
||||||
|
IMovieTranslationRepository movieTranslationRepository,
|
||||||
IEventAggregator eventAggregator)
|
IEventAggregator eventAggregator)
|
||||||
: base(database, eventAggregator)
|
: base(database, eventAggregator)
|
||||||
{
|
{
|
||||||
_alternativeTitleRepository = alternativeTitleRepository;
|
_alternativeTitleRepository = alternativeTitleRepository;
|
||||||
|
_movieTranslationRepository = movieTranslationRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override SqlBuilder Builder() => new SqlBuilder(_database.DatabaseType)
|
protected override SqlBuilder Builder() => new SqlBuilder(_database.DatabaseType)
|
||||||
|
|
@ -94,6 +97,10 @@ public override IEnumerable<Movie> All()
|
||||||
.GroupBy(x => x.MovieMetadataId)
|
.GroupBy(x => x.MovieMetadataId)
|
||||||
.ToDictionary(x => x.Key, y => y.ToList());
|
.ToDictionary(x => x.Key, y => y.ToList());
|
||||||
|
|
||||||
|
var translations = _movieTranslationRepository.All()
|
||||||
|
.GroupBy(x => x.MovieMetadataId)
|
||||||
|
.ToDictionary(x => x.Key, y => y.ToList());
|
||||||
|
|
||||||
return _database.QueryJoined<Movie, MovieMetadata>(
|
return _database.QueryJoined<Movie, MovieMetadata>(
|
||||||
builder,
|
builder,
|
||||||
(movie, metadata) =>
|
(movie, metadata) =>
|
||||||
|
|
@ -105,6 +112,11 @@ public override IEnumerable<Movie> All()
|
||||||
movie.MovieMetadata.Value.AlternativeTitles = altTitles;
|
movie.MovieMetadata.Value.AlternativeTitles = altTitles;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (translations.TryGetValue(movie.MovieMetadataId, out var trans))
|
||||||
|
{
|
||||||
|
movie.MovieMetadata.Value.Translations = trans;
|
||||||
|
}
|
||||||
|
|
||||||
return movie;
|
return movie;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,13 +15,9 @@ public AlternativeTitleResource()
|
||||||
// Todo: Sorters should be done completely on the client
|
// Todo: Sorters should be done completely on the client
|
||||||
// Todo: Is there an easy way to keep IgnoreArticlesWhenSorting in sync between, Series, History, Missing?
|
// Todo: Is there an easy way to keep IgnoreArticlesWhenSorting in sync between, Series, History, Missing?
|
||||||
// Todo: We should get the entire Profile instead of ID and Name separately
|
// Todo: We should get the entire Profile instead of ID and Name separately
|
||||||
public SourceType SourceType { get; set; }
|
|
||||||
public int MovieMetadataId { get; set; }
|
public int MovieMetadataId { get; set; }
|
||||||
public string Title { get; set; }
|
public string Title { get; set; }
|
||||||
public string CleanTitle { get; set; }
|
public string CleanTitle { get; set; }
|
||||||
public int SourceId { get; set; }
|
|
||||||
public int Votes { get; set; }
|
|
||||||
public int VoteCount { get; set; }
|
|
||||||
public Language Language { get; set; }
|
public Language Language { get; set; }
|
||||||
|
|
||||||
// TODO: Add series statistics as a property of the series (instead of individual properties)
|
// TODO: Add series statistics as a property of the series (instead of individual properties)
|
||||||
|
|
@ -39,12 +35,8 @@ public static AlternativeTitleResource ToResource(this AlternativeTitle model)
|
||||||
return new AlternativeTitleResource
|
return new AlternativeTitleResource
|
||||||
{
|
{
|
||||||
Id = model.Id,
|
Id = model.Id,
|
||||||
SourceType = model.SourceType,
|
|
||||||
MovieMetadataId = model.MovieMetadataId,
|
MovieMetadataId = model.MovieMetadataId,
|
||||||
Title = model.Title,
|
Title = model.Title,
|
||||||
SourceId = model.SourceId,
|
|
||||||
Votes = model.Votes,
|
|
||||||
VoteCount = model.VoteCount,
|
|
||||||
Language = model.Language
|
Language = model.Language
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -59,12 +51,8 @@ public static AlternativeTitle ToModel(this AlternativeTitleResource resource)
|
||||||
return new AlternativeTitle
|
return new AlternativeTitle
|
||||||
{
|
{
|
||||||
Id = resource.Id,
|
Id = resource.Id,
|
||||||
SourceType = resource.SourceType,
|
|
||||||
MovieMetadataId = resource.MovieMetadataId,
|
MovieMetadataId = resource.MovieMetadataId,
|
||||||
Title = resource.Title,
|
Title = resource.Title,
|
||||||
SourceId = resource.SourceId,
|
|
||||||
Votes = resource.Votes,
|
|
||||||
VoteCount = resource.VoteCount,
|
|
||||||
Language = resource.Language
|
Language = resource.Language
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,9 +18,6 @@ public AlternativeTitleResource()
|
||||||
public int MovieMetadataId { get; set; }
|
public int MovieMetadataId { get; set; }
|
||||||
public string Title { get; set; }
|
public string Title { get; set; }
|
||||||
public string CleanTitle { get; set; }
|
public string CleanTitle { get; set; }
|
||||||
public int SourceId { get; set; }
|
|
||||||
public int Votes { get; set; }
|
|
||||||
public int VoteCount { get; set; }
|
|
||||||
public Language Language { get; set; }
|
public Language Language { get; set; }
|
||||||
|
|
||||||
// TODO: Add series statistics as a property of the series (instead of individual properties)
|
// TODO: Add series statistics as a property of the series (instead of individual properties)
|
||||||
|
|
@ -38,12 +35,8 @@ public static AlternativeTitleResource ToResource(this AlternativeTitle model)
|
||||||
return new AlternativeTitleResource
|
return new AlternativeTitleResource
|
||||||
{
|
{
|
||||||
Id = model.Id,
|
Id = model.Id,
|
||||||
SourceType = model.SourceType,
|
|
||||||
MovieMetadataId = model.MovieMetadataId,
|
MovieMetadataId = model.MovieMetadataId,
|
||||||
Title = model.Title,
|
Title = model.Title,
|
||||||
SourceId = model.SourceId,
|
|
||||||
Votes = model.Votes,
|
|
||||||
VoteCount = model.VoteCount,
|
|
||||||
Language = model.Language
|
Language = model.Language
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -58,12 +51,8 @@ public static AlternativeTitle ToModel(this AlternativeTitleResource resource)
|
||||||
return new AlternativeTitle
|
return new AlternativeTitle
|
||||||
{
|
{
|
||||||
Id = resource.Id,
|
Id = resource.Id,
|
||||||
SourceType = resource.SourceType,
|
|
||||||
MovieMetadataId = resource.MovieMetadataId,
|
MovieMetadataId = resource.MovieMetadataId,
|
||||||
Title = resource.Title,
|
Title = resource.Title,
|
||||||
SourceId = resource.SourceId,
|
|
||||||
Votes = resource.Votes,
|
|
||||||
VoteCount = resource.VoteCount,
|
|
||||||
Language = resource.Language
|
Language = resource.Language
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ public MovieResource()
|
||||||
public string OriginalTitle { get; set; }
|
public string OriginalTitle { get; set; }
|
||||||
public Language OriginalLanguage { get; set; }
|
public Language OriginalLanguage { get; set; }
|
||||||
public List<AlternativeTitleResource> AlternateTitles { get; set; }
|
public List<AlternativeTitleResource> AlternateTitles { get; set; }
|
||||||
|
public List<MovieTranslationResource> Translations { get; set; }
|
||||||
public int? SecondaryYear { get; set; }
|
public int? SecondaryYear { get; set; }
|
||||||
public int SecondaryYearSourceId { get; set; }
|
public int SecondaryYearSourceId { get; set; }
|
||||||
public string SortTitle { get; set; }
|
public string SortTitle { get; set; }
|
||||||
|
|
@ -135,6 +136,7 @@ public static MovieResource ToResource(this Movie model, int availDelay, MovieTr
|
||||||
Added = model.Added,
|
Added = model.Added,
|
||||||
AddOptions = model.AddOptions,
|
AddOptions = model.AddOptions,
|
||||||
AlternateTitles = model.MovieMetadata.Value.AlternativeTitles.ToResource(),
|
AlternateTitles = model.MovieMetadata.Value.AlternativeTitles.ToResource(),
|
||||||
|
Translations = model.MovieMetadata.Value.Translations.ToResource(),
|
||||||
Ratings = model.MovieMetadata.Value.Ratings,
|
Ratings = model.MovieMetadata.Value.Ratings,
|
||||||
YouTubeTrailerId = model.MovieMetadata.Value.YouTubeTrailerId,
|
YouTubeTrailerId = model.MovieMetadata.Value.YouTubeTrailerId,
|
||||||
Studio = model.MovieMetadata.Value.Studio,
|
Studio = model.MovieMetadata.Value.Studio,
|
||||||
|
|
|
||||||
56
src/Radarr.Api.V4/Movies/MovieTranslationResource.cs
Normal file
56
src/Radarr.Api.V4/Movies/MovieTranslationResource.cs
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using NzbDrone.Core.Languages;
|
||||||
|
using NzbDrone.Core.Movies.Translations;
|
||||||
|
using Radarr.Http.REST;
|
||||||
|
|
||||||
|
namespace Radarr.Api.V4.Movies
|
||||||
|
{
|
||||||
|
public class MovieTranslationResource : RestResource
|
||||||
|
{
|
||||||
|
public int MovieMetadataId { get; set; }
|
||||||
|
public string Title { get; set; }
|
||||||
|
public string CleanTitle { get; set; }
|
||||||
|
public Language Language { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class MovieTranslationResourceMapper
|
||||||
|
{
|
||||||
|
public static MovieTranslationResource ToResource(this MovieTranslation model)
|
||||||
|
{
|
||||||
|
if (model == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new MovieTranslationResource
|
||||||
|
{
|
||||||
|
Id = model.Id,
|
||||||
|
MovieMetadataId = model.MovieMetadataId,
|
||||||
|
Title = model.Title,
|
||||||
|
Language = model.Language
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static MovieTranslation ToModel(this MovieTranslationResource resource)
|
||||||
|
{
|
||||||
|
if (resource == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new MovieTranslation
|
||||||
|
{
|
||||||
|
Id = resource.Id,
|
||||||
|
MovieMetadataId = resource.MovieMetadataId,
|
||||||
|
Title = resource.Title,
|
||||||
|
Language = resource.Language
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<MovieTranslationResource> ToResource(this IEnumerable<MovieTranslation> movies)
|
||||||
|
{
|
||||||
|
return movies.Select(ToResource).ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue