New: Rework Movie Details view

This commit is contained in:
Qstick 2022-03-16 21:14:09 -05:00
parent 757cb9a956
commit 1d4db26f17
37 changed files with 494 additions and 434 deletions

View file

@ -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;

View file

@ -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);

View file

@ -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;
} }

View file

@ -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}

View file

@ -1,16 +0,0 @@
import React from 'react';
import InteractiveSearchContentConnector from './InteractiveSearchContentConnector';
function InteractiveSearchTable(props) {
return (
<InteractiveSearchContentConnector
searchPayload={props}
/>
);
}
InteractiveSearchTable.propTypes = {
};
export default InteractiveSearchTable;

View file

@ -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 = {

View file

@ -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 = {

View file

@ -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 {

View file

@ -2,6 +2,10 @@
flex: 1 0 auto; flex: 1 0 auto;
} }
.movie {
padding: 10px;
}
.container { .container {
padding: 10px; padding: 10px;
} }

View file

@ -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>
); );
} }
} }

View file

@ -1,3 +0,0 @@
.alternateTitle {
white-space: nowrap;
}

View file

@ -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;

View file

@ -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 {

View file

@ -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>
); );

View file

@ -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

View file

@ -0,0 +1,9 @@
.container {
border: 1px solid var(--borderColor);
border-radius: 4px;
background-color: var(--inputBackgroundColor);
&:last-of-type {
margin-bottom: 0;
}
}

View file

@ -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>
); );
} }

View file

@ -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;

View file

@ -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);

View file

@ -0,0 +1,9 @@
.container {
border: 1px solid var(--borderColor);
border-radius: 4px;
background-color: var(--inputBackgroundColor);
&:last-of-type {
margin-bottom: 0;
}
}

View file

@ -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>
); );
} }

View 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;

View file

@ -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);

View file

@ -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;

View file

@ -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);

View file

@ -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);
}
}
}

View file

@ -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");
}
}
}

View file

@ -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",

View file

@ -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
}; };

View file

@ -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; }
}
} }

View file

@ -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);

View file

@ -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;
}); });
} }

View file

@ -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
}; };
} }

View file

@ -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
}; };
} }

View file

@ -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,

View 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();
}
}
}