diff --git a/frontend/src/InteractiveSearch/InteractiveSearchContent.css b/frontend/src/InteractiveSearch/InteractiveSearch.css
similarity index 100%
rename from frontend/src/InteractiveSearch/InteractiveSearchContent.css
rename to frontend/src/InteractiveSearch/InteractiveSearch.css
diff --git a/frontend/src/InteractiveSearch/InteractiveSearchContent.js b/frontend/src/InteractiveSearch/InteractiveSearch.js
similarity index 69%
rename from frontend/src/InteractiveSearch/InteractiveSearchContent.js
rename to frontend/src/InteractiveSearch/InteractiveSearch.js
index 45053793cc..82cbdf8754 100644
--- a/frontend/src/InteractiveSearch/InteractiveSearchContent.js
+++ b/frontend/src/InteractiveSearch/InteractiveSearch.js
@@ -2,12 +2,15 @@ import PropTypes from 'prop-types';
import React from 'react';
import Icon from 'Components/Icon';
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 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 InteractiveSearchFilterModalConnector from './InteractiveSearchFilterModalConnector';
import InteractiveSearchRowConnector from './InteractiveSearchRowConnector';
-import styles from './InteractiveSearchContent.css';
+import styles from './InteractiveSearch.css';
const columns = [
{
@@ -22,20 +25,6 @@ const columns = [
isSortable: 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',
label: translate('Title'),
@@ -99,10 +88,24 @@ const columns = [
label: React.createElement(Icon, { name: icons.FLAG }),
isSortable: 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 {
searchPayload,
isFetching,
@@ -110,44 +113,63 @@ function InteractiveSearchContent(props) {
error,
totalReleasesCount,
items,
+ selectedFilterKey,
+ filters,
+ customFilters,
sortKey,
sortDirection,
longDateFormat,
timeFormat,
onSortPress,
+ onFilterSelect,
onGrabPress
} = props;
return (
+
+
+
+
{
- isFetching &&
-
+ isFetching ?
: null
}
{
- !isFetching && !!error &&
-
+ !isFetching && error ?
+
{translate('UnableToLoadResultsIntSearch')}
-
+
:
+ null
}
{
- !isFetching && isPopulated && !totalReleasesCount &&
-
+ !isFetching && isPopulated && !totalReleasesCount ?
+
{translate('NoResultsFound')}
-
+
:
+ null
}
{
- !!totalReleasesCount && isPopulated && !items.length &&
-
+ !!totalReleasesCount && isPopulated && !items.length ?
+
{translate('AllResultsHiddenFilter')}
-
+
:
+ null
}
{
- isPopulated && !!items.length &&
+ isPopulated && !!items.length ?
+ :
+ null
}
{
- totalReleasesCount !== items.length && !!items.length &&
+ totalReleasesCount !== items.length && !!items.length ?
{translate('SomeResultsHiddenFilter')}
-
+
:
+ null
}
);
}
-InteractiveSearchContent.propTypes = {
+InteractiveSearch.propTypes = {
searchPayload: PropTypes.object.isRequired,
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
totalReleasesCount: PropTypes.number.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,
sortDirection: PropTypes.string,
longDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired,
onSortPress: PropTypes.func.isRequired,
+ onFilterSelect: PropTypes.func.isRequired,
onGrabPress: PropTypes.func.isRequired
};
-export default InteractiveSearchContent;
+export default InteractiveSearch;
diff --git a/frontend/src/InteractiveSearch/InteractiveSearchContentConnector.js b/frontend/src/InteractiveSearch/InteractiveSearchConnector.js
similarity index 90%
rename from frontend/src/InteractiveSearch/InteractiveSearchContentConnector.js
rename to frontend/src/InteractiveSearch/InteractiveSearchConnector.js
index f4432d9e38..7b8cd2c044 100644
--- a/frontend/src/InteractiveSearch/InteractiveSearchContentConnector.js
+++ b/frontend/src/InteractiveSearch/InteractiveSearchConnector.js
@@ -5,7 +5,7 @@ import { createSelector } from 'reselect';
import * as releaseActions from 'Store/Actions/releaseActions';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
-import InteractiveSearchContent from './InteractiveSearchContent';
+import InteractiveSearch from './InteractiveSearch';
function createMapStateToProps(appState) {
return createSelector(
@@ -48,7 +48,7 @@ function createMapDispatchToProps(dispatch, props) {
};
}
-class InteractiveSearchContentConnector extends Component {
+class InteractiveSearchConnector extends Component {
//
// Lifecycle
@@ -79,18 +79,18 @@ class InteractiveSearchContentConnector extends Component {
return (
-
);
}
}
-InteractiveSearchContentConnector.propTypes = {
+InteractiveSearchConnector.propTypes = {
searchPayload: PropTypes.object.isRequired,
isPopulated: PropTypes.bool.isRequired,
dispatchFetchReleases: PropTypes.func.isRequired,
dispatchClearReleases: PropTypes.func.isRequired
};
-export default connect(createMapStateToProps, createMapDispatchToProps)(InteractiveSearchContentConnector);
+export default connect(createMapStateToProps, createMapDispatchToProps)(InteractiveSearchConnector);
diff --git a/frontend/src/InteractiveSearch/InteractiveSearchRow.css b/frontend/src/InteractiveSearch/InteractiveSearchRow.css
index 6545102caa..1cf5f0e26f 100644
--- a/frontend/src/InteractiveSearch/InteractiveSearchRow.css
+++ b/frontend/src/InteractiveSearch/InteractiveSearchRow.css
@@ -1,15 +1,20 @@
-.cell {
- composes: cell from '~Components/Table/Cells/TableRowCell.css';
-}
-
.protocol {
- composes: cell;
+ composes: cell from '~Components/Table/Cells/TableRowCell.css';
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 {
- composes: cell;
+ composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 85px;
}
@@ -17,7 +22,9 @@
.quality,
.customFormat,
.language {
- composes: cell;
+ composes: cell from '~Components/Table/Cells/TableRowCell.css';
+
+ text-align: center;
}
.language {
@@ -25,7 +32,7 @@
}
.customFormatScore {
- composes: cell;
+ composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 55px;
font-weight: bold;
@@ -35,34 +42,26 @@
.rejected,
.indexerFlags,
.download {
- composes: cell;
+ composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 50px;
}
.age,
.size {
- composes: cell;
+ composes: cell from '~Components/Table/Cells/TableRowCell.css';
white-space: nowrap;
}
.peers {
- composes: cell;
+ composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 75px;
}
-.title {
- composes: cell;
-}
-
-.title div {
- overflow-wrap: break-word;
-}
-
.history {
- composes: cell;
+ composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 75px;
}
diff --git a/frontend/src/InteractiveSearch/InteractiveSearchRow.js b/frontend/src/InteractiveSearch/InteractiveSearchRow.js
index 5b7cc6c6f8..92699e8ac8 100644
--- a/frontend/src/InteractiveSearch/InteractiveSearchRow.js
+++ b/frontend/src/InteractiveSearch/InteractiveSearchRow.js
@@ -145,46 +145,6 @@ class InteractiveSearchRow extends Component {
{formatAge(age, ageHours, ageMinutes)}
-
-
-
-
-
- {
- !!rejections.length &&
-
- }
- title={translate('ReleaseRejected')}
- body={
-
- {
- rejections.map((rejection, index) => {
- return (
- -
- {rejection}
-
- );
- })
- }
-
- }
- position={tooltipPositions.BOTTOM}
- />
- }
-
-
+
+ {
+ !!rejections.length &&
+
+ }
+ title={translate('ReleaseRejected')}
+ body={
+
+ {
+ rejections.map((rejection, index) => {
+ return (
+ -
+ {rejection}
+
+ );
+ })
+ }
+
+ }
+ position={tooltipPositions.LEFT}
+ />
+ }
+
+
+
+
+
+
- );
-}
-
-InteractiveSearchTable.propTypes = {
-};
-
-export default InteractiveSearchTable;
diff --git a/frontend/src/Movie/Details/Credits/Cast/MovieCastPoster.js b/frontend/src/Movie/Details/Credits/Cast/MovieCastPoster.js
index 1898e094c6..028b0a807b 100644
--- a/frontend/src/Movie/Details/Credits/Cast/MovieCastPoster.js
+++ b/frontend/src/Movie/Details/Credits/Cast/MovieCastPoster.js
@@ -69,7 +69,8 @@ class MovieCastPoster extends Component {
const elementStyle = {
width: `${posterWidth}px`,
- height: `${posterHeight}px`
+ height: `${posterHeight}px`,
+ borderRadius: '5px'
};
const contentStyle = {
diff --git a/frontend/src/Movie/Details/Credits/Crew/MovieCrewPoster.js b/frontend/src/Movie/Details/Credits/Crew/MovieCrewPoster.js
index 7831114a27..f271311719 100644
--- a/frontend/src/Movie/Details/Credits/Crew/MovieCrewPoster.js
+++ b/frontend/src/Movie/Details/Credits/Crew/MovieCrewPoster.js
@@ -69,7 +69,8 @@ class MovieCrewPoster extends Component {
const elementStyle = {
width: `${posterWidth}px`,
- height: `${posterHeight}px`
+ height: `${posterHeight}px`,
+ borderRadius: '5px'
};
const contentStyle = {
diff --git a/frontend/src/Movie/Details/Credits/MovieCreditPoster.css b/frontend/src/Movie/Details/Credits/MovieCreditPoster.css
index 9636b2e088..c4ab53a28e 100644
--- a/frontend/src/Movie/Details/Credits/MovieCreditPoster.css
+++ b/frontend/src/Movie/Details/Credits/MovieCreditPoster.css
@@ -1,6 +1,7 @@
$hoverScale: 1.05;
.content {
+ border-radius: 5px;
transition: all 200ms ease-in;
&:hover {
diff --git a/frontend/src/Movie/Details/Credits/MovieCreditPosters.css b/frontend/src/Movie/Details/Credits/MovieCreditPosters.css
index d80f951a0d..2bd05a5e01 100644
--- a/frontend/src/Movie/Details/Credits/MovieCreditPosters.css
+++ b/frontend/src/Movie/Details/Credits/MovieCreditPosters.css
@@ -2,6 +2,10 @@
flex: 1 0 auto;
}
+.movie {
+ padding: 10px;
+}
+
.container {
padding: 10px;
}
diff --git a/frontend/src/Movie/Details/Credits/MovieCreditPosters.js b/frontend/src/Movie/Details/Credits/MovieCreditPosters.js
index 7815da3ca9..31d205e8b1 100644
--- a/frontend/src/Movie/Details/Credits/MovieCreditPosters.js
+++ b/frontend/src/Movie/Details/Credits/MovieCreditPosters.js
@@ -1,12 +1,16 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
-import { Grid, WindowScroller } from 'react-virtualized';
-import Measure from 'Components/Measure';
+import { Navigation } from 'swiper';
+import { Swiper, SwiperSlide } from 'swiper/react';
import dimensions from 'Styles/Variables/dimensions';
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
import MovieCreditPosterConnector from './MovieCreditPosterConnector';
import styles from './MovieCreditPosters.css';
+// Import Swiper styles
+import 'swiper/css';
+import 'swiper/css/navigation';
+
// Poster container dimensions
const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
const columnPaddingSmallScreen = parseInt(dimensions.movieIndexColumnPaddingSmallScreen);
@@ -169,56 +173,50 @@ class MovieCreditPosters extends Component {
render() {
const {
- items
+ items,
+ itemComponent
} = this.props;
const {
- width,
- columnWidth,
- columnCount,
- rowHeight
+ posterWidth,
+ posterHeight
} = this.state;
- const rowCount = Math.ceil(items.length / columnCount);
-
return (
-
-
- {({ height, registerChild, onChildScroll, scrollTop }) => {
- if (!height) {
- return ;
- }
- return (
-
-
-
- );
- }
- }
-
-
+
+ {
+ swiper.params.navigation.prevEl = this._swiperPrevRef;
+ swiper.params.navigation.nextEl = this._swiperNextRef;
+ swiper.navigation.init();
+ swiper.navigation.update();
+ }}
+ >
+ {items.map((credit) => (
+
+
+
+ ))}
+
+
);
}
}
diff --git a/frontend/src/Movie/Details/MovieAlternateTitles.css b/frontend/src/Movie/Details/MovieAlternateTitles.css
deleted file mode 100644
index 1af1ae68b0..0000000000
--- a/frontend/src/Movie/Details/MovieAlternateTitles.css
+++ /dev/null
@@ -1,3 +0,0 @@
-.alternateTitle {
- white-space: nowrap;
-}
diff --git a/frontend/src/Movie/Details/MovieAlternateTitles.js b/frontend/src/Movie/Details/MovieAlternateTitles.js
deleted file mode 100644
index 5b0fdaeaab..0000000000
--- a/frontend/src/Movie/Details/MovieAlternateTitles.js
+++ /dev/null
@@ -1,28 +0,0 @@
-import PropTypes from 'prop-types';
-import React from 'react';
-import styles from './MovieAlternateTitles.css';
-
-function MovieAlternateTitles({ alternateTitles }) {
- return (
-
- {
- alternateTitles.filter((x, i, a) => a.indexOf(x) === i).map((alternateTitle) => {
- return (
- -
- {alternateTitle}
-
- );
- })
- }
-
- );
-}
-
-MovieAlternateTitles.propTypes = {
- alternateTitles: PropTypes.arrayOf(PropTypes.string).isRequired
-};
-
-export default MovieAlternateTitles;
diff --git a/frontend/src/Movie/Details/MovieDetails.css b/frontend/src/Movie/Details/MovieDetails.css
index c6dbac0171..38078540b9 100644
--- a/frontend/src/Movie/Details/MovieDetails.css
+++ b/frontend/src/Movie/Details/MovieDetails.css
@@ -5,7 +5,7 @@
.header {
position: relative;
width: 100%;
- height: 375px;
+ height: 425px;
}
.errorMessage {
@@ -39,10 +39,11 @@
}
.poster {
+ z-index: 2;
flex-shrink: 0;
margin-right: 35px;
- width: 217px;
- height: 319px;
+ width: 250px;
+ height: 368px;
}
.info {
diff --git a/frontend/src/Movie/Details/MovieDetails.js b/frontend/src/Movie/Details/MovieDetails.js
index e453892b7d..aaa44324f8 100644
--- a/frontend/src/Movie/Details/MovieDetails.js
+++ b/frontend/src/Movie/Details/MovieDetails.js
@@ -1,8 +1,8 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
-import { Tab, TabList, TabPanel, Tabs } from 'react-tabs';
import TextTruncate from 'react-text-truncate';
+import FieldSet from 'Components/FieldSet';
import Icon from 'Components/Icon';
import ImdbRating from 'Components/ImdbRating';
import InfoLabel from 'Components/InfoLabel';
@@ -22,12 +22,11 @@ import Popover from 'Components/Tooltip/Popover';
import Tooltip from 'Components/Tooltip/Tooltip';
import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
-import InteractiveSearchFilterMenuConnector from 'InteractiveSearch/InteractiveSearchFilterMenuConnector';
-import InteractiveSearchTable from 'InteractiveSearch/InteractiveSearchTable';
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
import MovieHistoryTable from 'Movie/History/MovieHistoryTable';
import MoviePoster from 'Movie/MoviePoster';
+import MovieInteractiveSearchModalConnector from 'Movie/Search/MovieInteractiveSearchModalConnector';
import MovieFileEditorTable from 'MovieFile/Editor/MovieFileEditorTable';
import ExtraFileTable from 'MovieFile/Extras/ExtraFileTable';
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
@@ -81,10 +80,10 @@ class MovieDetails extends Component {
isEditMovieModalOpen: false,
isDeleteMovieModalOpen: false,
isInteractiveImportModalOpen: false,
+ isInteractiveSearchModalOpen: false,
allExpanded: false,
allCollapsed: false,
expandedState: {},
- selectedTabIndex: 0,
overviewHeight: 0,
titleWidth: 0
};
@@ -137,6 +136,14 @@ class MovieDetails extends Component {
this.setState({ isEditMovieModalOpen: false });
};
+ onInteractiveSearchPress = () => {
+ this.setState({ isInteractiveSearchModalOpen: true });
+ };
+
+ onInteractiveSearchModalClose = () => {
+ this.setState({ isInteractiveSearchModalOpen: false });
+ };
+
onDeleteMoviePress = () => {
this.setState({
isEditMovieModalOpen: false,
@@ -298,9 +305,9 @@ class MovieDetails extends Component {
isEditMovieModalOpen,
isDeleteMovieModalOpen,
isInteractiveImportModalOpen,
+ isInteractiveSearchModalOpen,
overviewHeight,
- titleWidth,
- selectedTabIndex
+ titleWidth
} = this.state;
const marqueeWidth = isSmallScreen ? titleWidth : (titleWidth - 150);
@@ -326,6 +333,14 @@ class MovieDetails extends Component {
onPress={onSearchPress}
/>
+
+
}
-
-
-
- {translate('History')}
-
+
-
- {translate('Search')}
-
+
-
- {translate('Titles')}
-
+
-
- {translate('Cast')}
-
-
-
- {translate('Crew')}
-
-
- {
- selectedTabIndex === 1 &&
-
-
-
- }
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
);
diff --git a/frontend/src/Movie/Details/Titles/MovieTitlesRow.js b/frontend/src/Movie/Details/Titles/MovieTitlesRow.js
index ea12bbd97f..eebc9364da 100644
--- a/frontend/src/Movie/Details/Titles/MovieTitlesRow.js
+++ b/frontend/src/Movie/Details/Titles/MovieTitlesRow.js
@@ -43,7 +43,7 @@ class MovieTitlesRow extends Component {
}
MovieTitlesRow.propTypes = {
- id: PropTypes.number.isRequired,
+ id: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
language: PropTypes.object.isRequired,
sourceType: PropTypes.string.isRequired
diff --git a/frontend/src/Movie/Details/Titles/MovieTitlesTable.css b/frontend/src/Movie/Details/Titles/MovieTitlesTable.css
new file mode 100644
index 0000000000..788a53cc30
--- /dev/null
+++ b/frontend/src/Movie/Details/Titles/MovieTitlesTable.css
@@ -0,0 +1,9 @@
+.container {
+ border: 1px solid var(--borderColor);
+ border-radius: 4px;
+ background-color: var(--inputBackgroundColor);
+
+ &:last-of-type {
+ margin-bottom: 0;
+ }
+}
diff --git a/frontend/src/Movie/Details/Titles/MovieTitlesTable.js b/frontend/src/Movie/Details/Titles/MovieTitlesTable.js
index 9223a7585f..1309de5197 100644
--- a/frontend/src/Movie/Details/Titles/MovieTitlesTable.js
+++ b/frontend/src/Movie/Details/Titles/MovieTitlesTable.js
@@ -1,5 +1,6 @@
import React from 'react';
import MovieTitlesTableContentConnector from './MovieTitlesTableContentConnector';
+import styles from './MovieTitlesTable.css';
function MovieTitlesTable(props) {
const {
@@ -7,9 +8,11 @@ function MovieTitlesTable(props) {
} = props;
return (
-
+
+
+
);
}
diff --git a/frontend/src/Movie/Details/Titles/MovieTitlesTableContent.js b/frontend/src/Movie/Details/Titles/MovieTitlesTableContent.js
index ae0e8011bd..366bf20f7d 100644
--- a/frontend/src/Movie/Details/Titles/MovieTitlesTableContent.js
+++ b/frontend/src/Movie/Details/Titles/MovieTitlesTableContent.js
@@ -1,6 +1,5 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
-import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import translate from 'Utilities/String/translate';
@@ -10,7 +9,7 @@ import styles from './MovieTitlesTableContent.css';
const columns = [
{
name: 'altTitle',
- label: translate('AlternativeTitle'),
+ label: translate('Title'),
isVisible: true
},
{
@@ -32,40 +31,25 @@ class MovieTitlesTableContent extends Component {
render() {
const {
- isFetching,
- isPopulated,
- error,
- items
+ titles
} = this.props;
- const hasItems = !!items.length;
+ const hasItems = !!titles.length;
return (
{
- isFetching &&
-
- }
-
- {
- !isFetching && !!error &&
-
- {translate('UnableToLoadAltTitle')}
-
- }
-
- {
- isPopulated && !hasItems && !error &&
+ !hasItems &&
{translate('NoAltTitle')}
}
{
- isPopulated && hasItems && !error &&
+ hasItems &&
{
- items.reverse().map((item) => {
+ titles.reverse().map((item) => {
return (
state.movies,
- (movies) => {
- return movies;
+ createMovieSelector(),
+ (movie) => {
+ 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() {
- const movie = this.props.items.filter((obj) => {
- return obj.id === this.props.movieId;
- });
+ const {
+ titles
+ } = this.props;
return (
);
}
@@ -38,7 +65,7 @@ class MovieTitlesTableContentConnector extends Component {
MovieTitlesTableContentConnector.propTypes = {
movieId: PropTypes.number.isRequired,
- items: PropTypes.arrayOf(PropTypes.object).isRequired
+ titles: PropTypes.arrayOf(PropTypes.object).isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(MovieTitlesTableContentConnector);
diff --git a/frontend/src/Movie/History/MovieHistoryTable.css b/frontend/src/Movie/History/MovieHistoryTable.css
new file mode 100644
index 0000000000..788a53cc30
--- /dev/null
+++ b/frontend/src/Movie/History/MovieHistoryTable.css
@@ -0,0 +1,9 @@
+.container {
+ border: 1px solid var(--borderColor);
+ border-radius: 4px;
+ background-color: var(--inputBackgroundColor);
+
+ &:last-of-type {
+ margin-bottom: 0;
+ }
+}
diff --git a/frontend/src/Movie/History/MovieHistoryTable.js b/frontend/src/Movie/History/MovieHistoryTable.js
index f5cfd2404e..e07bfa5619 100644
--- a/frontend/src/Movie/History/MovieHistoryTable.js
+++ b/frontend/src/Movie/History/MovieHistoryTable.js
@@ -1,5 +1,6 @@
import React from 'react';
import MovieHistoryTableContentConnector from './MovieHistoryTableContentConnector';
+import styles from './MovieHistoryTable.css';
function MovieHistoryTable(props) {
const {
@@ -7,9 +8,11 @@ function MovieHistoryTable(props) {
} = props;
return (
-
+
+
+
);
}
diff --git a/frontend/src/Movie/Search/MovieInteractiveSearchModal.js b/frontend/src/Movie/Search/MovieInteractiveSearchModal.js
new file mode 100644
index 0000000000..ec8987dfa4
--- /dev/null
+++ b/frontend/src/Movie/Search/MovieInteractiveSearchModal.js
@@ -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 (
+
+
+
+ );
+}
+
+MovieInteractiveSearchModal.propTypes = {
+ isOpen: PropTypes.bool.isRequired,
+ movieId: PropTypes.number.isRequired,
+ onModalClose: PropTypes.func.isRequired
+};
+
+export default MovieInteractiveSearchModal;
diff --git a/frontend/src/Movie/Search/MovieInteractiveSearchModalConnector.js b/frontend/src/Movie/Search/MovieInteractiveSearchModalConnector.js
new file mode 100644
index 0000000000..f00b1cb4d5
--- /dev/null
+++ b/frontend/src/Movie/Search/MovieInteractiveSearchModalConnector.js
@@ -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);
diff --git a/frontend/src/Movie/Search/MovieInteractiveSearchModalContent.js b/frontend/src/Movie/Search/MovieInteractiveSearchModalContent.js
new file mode 100644
index 0000000000..ff85b10ed3
--- /dev/null
+++ b/frontend/src/Movie/Search/MovieInteractiveSearchModalContent.js
@@ -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 (
+
+
+ Interactive Search
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+MovieInteractiveSearchModalContent.propTypes = {
+ movieId: PropTypes.number.isRequired,
+ onModalClose: PropTypes.func.isRequired
+};
+
+export default MovieInteractiveSearchModalContent;
diff --git a/frontend/src/MovieFile/Editor/MovieFileEditorTable.css b/frontend/src/MovieFile/Editor/MovieFileEditorTable.css
index e01af32bf6..788a53cc30 100644
--- a/frontend/src/MovieFile/Editor/MovieFileEditorTable.css
+++ b/frontend/src/MovieFile/Editor/MovieFileEditorTable.css
@@ -1,5 +1,4 @@
.container {
- margin-top: 20px;
border: 1px solid var(--borderColor);
border-radius: 4px;
background-color: var(--inputBackgroundColor);
diff --git a/src/NzbDrone.Core.Test/MovieTests/AlternativeTitleServiceTests/AlternativeTitleFixture.cs b/src/NzbDrone.Core.Test/MovieTests/AlternativeTitleServiceTests/AlternativeTitleFixture.cs
deleted file mode 100644
index 31b9edbf05..0000000000
--- a/src/NzbDrone.Core.Test/MovieTests/AlternativeTitleServiceTests/AlternativeTitleFixture.cs
+++ /dev/null
@@ -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.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);
- }
- }
-}
diff --git a/src/NzbDrone.Core/Datastore/Migration/216_clean_alt_titles.cs b/src/NzbDrone.Core/Datastore/Migration/216_clean_alt_titles.cs
new file mode 100644
index 0000000000..5f6d498366
--- /dev/null
+++ b/src/NzbDrone.Core/Datastore/Migration/216_clean_alt_titles.cs
@@ -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");
+ }
+ }
+}
diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json
index 48f1ab0f3f..e093348536 100644
--- a/src/NzbDrone.Core/Localization/Core/en.json
+++ b/src/NzbDrone.Core/Localization/Core/en.json
@@ -1028,6 +1028,7 @@
"Timeleft": "Time Left",
"Title": "Title",
"Titles": "Titles",
+ "TitlesAndTranslations": "Titles and Translations",
"TMDb": "TMDb",
"TMDBId": "TMDb Id",
"TmdbIdHelpText": "The TMDb Id of the movie to exclude",
diff --git a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs
index 4703c0110d..4c776f94dc 100644
--- a/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs
+++ b/src/NzbDrone.Core/MetadataSource/SkyHook/SkyHookProxy.cs
@@ -570,7 +570,6 @@ private static AlternativeTitle MapAlternativeTitle(AlternativeTitleResource arg
var newAlternativeTitle = new AlternativeTitle
{
Title = arg.Title,
- SourceType = SourceType.TMDB,
CleanTitle = arg.Title.CleanMovieTitle(),
Language = IsoLanguages.Find(arg.Language.ToLower())?.Language ?? Language.English
};
diff --git a/src/NzbDrone.Core/Movies/AlternativeTitles/AlternativeTitle.cs b/src/NzbDrone.Core/Movies/AlternativeTitles/AlternativeTitle.cs
index c3c5e2cd73..9279906e35 100644
--- a/src/NzbDrone.Core/Movies/AlternativeTitles/AlternativeTitle.cs
+++ b/src/NzbDrone.Core/Movies/AlternativeTitles/AlternativeTitle.cs
@@ -6,39 +6,22 @@ namespace NzbDrone.Core.Movies.AlternativeTitles
{
public class AlternativeTitle : ModelBase
{
- public SourceType SourceType { get; set; }
public int MovieMetadataId { get; set; }
public string Title { 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 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;
CleanTitle = title.CleanMovieTitle();
- SourceType = sourceType;
- SourceId = sourceId;
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)
{
var item = obj as AlternativeTitle;
@@ -61,18 +44,4 @@ public override string ToString()
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; }
- }
}
diff --git a/src/NzbDrone.Core/Movies/AlternativeTitles/AlternativeTitleRepository.cs b/src/NzbDrone.Core/Movies/AlternativeTitles/AlternativeTitleRepository.cs
index 413d058db3..7d234b02a7 100644
--- a/src/NzbDrone.Core/Movies/AlternativeTitles/AlternativeTitleRepository.cs
+++ b/src/NzbDrone.Core/Movies/AlternativeTitles/AlternativeTitleRepository.cs
@@ -7,8 +7,6 @@ namespace NzbDrone.Core.Movies.AlternativeTitles
{
public interface IAlternativeTitleRepository : IBasicRepository
{
- AlternativeTitle FindBySourceId(int sourceId);
- List FindBySourceIds(List sourceIds);
List FindByMovieMetadataId(int movieId);
void DeleteForMovies(List 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 FindBySourceIds(List sourceIds)
- {
- return Query(x => sourceIds.Contains(x.SourceId));
- }
-
public List FindByMovieMetadataId(int movieId)
{
return Query(x => x.MovieMetadataId == movieId);
diff --git a/src/NzbDrone.Core/Movies/MovieRepository.cs b/src/NzbDrone.Core/Movies/MovieRepository.cs
index ff7d388005..55ff99ce51 100644
--- a/src/NzbDrone.Core/Movies/MovieRepository.cs
+++ b/src/NzbDrone.Core/Movies/MovieRepository.cs
@@ -35,13 +35,16 @@ public interface IMovieRepository : IBasicRepository
public class MovieRepository : BasicRepository, IMovieRepository
{
private readonly IAlternativeTitleRepository _alternativeTitleRepository;
+ private readonly IMovieTranslationRepository _movieTranslationRepository;
public MovieRepository(IMainDatabase database,
IAlternativeTitleRepository alternativeTitleRepository,
+ IMovieTranslationRepository movieTranslationRepository,
IEventAggregator eventAggregator)
: base(database, eventAggregator)
{
_alternativeTitleRepository = alternativeTitleRepository;
+ _movieTranslationRepository = movieTranslationRepository;
}
protected override SqlBuilder Builder() => new SqlBuilder(_database.DatabaseType)
@@ -94,6 +97,10 @@ public override IEnumerable All()
.GroupBy(x => x.MovieMetadataId)
.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(
builder,
(movie, metadata) =>
@@ -105,6 +112,11 @@ public override IEnumerable All()
movie.MovieMetadata.Value.AlternativeTitles = altTitles;
}
+ if (translations.TryGetValue(movie.MovieMetadataId, out var trans))
+ {
+ movie.MovieMetadata.Value.Translations = trans;
+ }
+
return movie;
});
}
diff --git a/src/Radarr.Api.V3/Movies/AlternativeTitleResource.cs b/src/Radarr.Api.V3/Movies/AlternativeTitleResource.cs
index 1456ee8887..3bcdbe5233 100644
--- a/src/Radarr.Api.V3/Movies/AlternativeTitleResource.cs
+++ b/src/Radarr.Api.V3/Movies/AlternativeTitleResource.cs
@@ -15,13 +15,9 @@ public AlternativeTitleResource()
// 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: We should get the entire Profile instead of ID and Name separately
- public SourceType SourceType { get; set; }
public int MovieMetadataId { get; set; }
public string Title { 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; }
// 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
{
Id = model.Id,
- SourceType = model.SourceType,
MovieMetadataId = model.MovieMetadataId,
Title = model.Title,
- SourceId = model.SourceId,
- Votes = model.Votes,
- VoteCount = model.VoteCount,
Language = model.Language
};
}
@@ -59,12 +51,8 @@ public static AlternativeTitle ToModel(this AlternativeTitleResource resource)
return new AlternativeTitle
{
Id = resource.Id,
- SourceType = resource.SourceType,
MovieMetadataId = resource.MovieMetadataId,
Title = resource.Title,
- SourceId = resource.SourceId,
- Votes = resource.Votes,
- VoteCount = resource.VoteCount,
Language = resource.Language
};
}
diff --git a/src/Radarr.Api.V4/Movies/AlternativeTitleResource.cs b/src/Radarr.Api.V4/Movies/AlternativeTitleResource.cs
index a2e25d95fd..906c9a33f3 100644
--- a/src/Radarr.Api.V4/Movies/AlternativeTitleResource.cs
+++ b/src/Radarr.Api.V4/Movies/AlternativeTitleResource.cs
@@ -18,9 +18,6 @@ public AlternativeTitleResource()
public int MovieMetadataId { get; set; }
public string Title { 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; }
// 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
{
Id = model.Id,
- SourceType = model.SourceType,
MovieMetadataId = model.MovieMetadataId,
Title = model.Title,
- SourceId = model.SourceId,
- Votes = model.Votes,
- VoteCount = model.VoteCount,
Language = model.Language
};
}
@@ -58,12 +51,8 @@ public static AlternativeTitle ToModel(this AlternativeTitleResource resource)
return new AlternativeTitle
{
Id = resource.Id,
- SourceType = resource.SourceType,
MovieMetadataId = resource.MovieMetadataId,
Title = resource.Title,
- SourceId = resource.SourceId,
- Votes = resource.Votes,
- VoteCount = resource.VoteCount,
Language = resource.Language
};
}
diff --git a/src/Radarr.Api.V4/Movies/MovieResource.cs b/src/Radarr.Api.V4/Movies/MovieResource.cs
index 29d774dbe3..d6b126a4db 100644
--- a/src/Radarr.Api.V4/Movies/MovieResource.cs
+++ b/src/Radarr.Api.V4/Movies/MovieResource.cs
@@ -30,6 +30,7 @@ public MovieResource()
public string OriginalTitle { get; set; }
public Language OriginalLanguage { get; set; }
public List AlternateTitles { get; set; }
+ public List Translations { get; set; }
public int? SecondaryYear { get; set; }
public int SecondaryYearSourceId { get; set; }
public string SortTitle { get; set; }
@@ -135,6 +136,7 @@ public static MovieResource ToResource(this Movie model, int availDelay, MovieTr
Added = model.Added,
AddOptions = model.AddOptions,
AlternateTitles = model.MovieMetadata.Value.AlternativeTitles.ToResource(),
+ Translations = model.MovieMetadata.Value.Translations.ToResource(),
Ratings = model.MovieMetadata.Value.Ratings,
YouTubeTrailerId = model.MovieMetadata.Value.YouTubeTrailerId,
Studio = model.MovieMetadata.Value.Studio,
diff --git a/src/Radarr.Api.V4/Movies/MovieTranslationResource.cs b/src/Radarr.Api.V4/Movies/MovieTranslationResource.cs
new file mode 100644
index 0000000000..e884d0f6a3
--- /dev/null
+++ b/src/Radarr.Api.V4/Movies/MovieTranslationResource.cs
@@ -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 ToResource(this IEnumerable movies)
+ {
+ return movies.Select(ToResource).ToList();
+ }
+ }
+}